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

Использование шаблона проектирования MVVM

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

Цель работы: освоить методику создания приложений с использованием MVVM

Приложение, построенное по шаблону MVVM

В этой лабораторной работе мы рассмотрим пример приложения, построенного с использованием шаблона MVVM. Наш пример основан на примере "Model-View-ViewModel Sample" "Пример приложения, построенного по шаблону модель-представление-модель представления"), http://code.msdn.microsoft.com/Model-View-ViewModel-Sample-8cb92fd9.

В приложении (рис. 23.1.) реализуется механизм наблюдения за игровыми достижениями.

(рис 23.1) Проект приложения, созданного с использованием шаблона проектирования MVVM

На главной странице имеются два списка. В первом выводятся сведения о собранных элементах (Items), во втором – данные о пройденных уровнях. Общая структура проекта, которую вы можете видеть на вышеприведенном рисунке, такова:

В папке Models расположена модель данных (Accomplishment.cs)

В папке ViewModel расположена модель представления, ViewModel.cs

В папке View расположены два представления, ItemView.xaml и LevelView.xaml это – пользовательские элементы управления, настроенные на использование их совместно с системой привязки данных.

В файле StateUtilities.cs объявлен вспомогательный класс, который используется для хранения сведений о том, запущено ли приложение из списка приложений (или с рабочего стола), или оно активировано после того, как пользователь недавно вышел из него.

Страница MainPage.xaml содержит представления, механизмы привязки данных из модели представления и другие вспомогательные механизмы.

В Листинге 23.1 приведен код файла Accomplishment.cs, то есть – модели данных.

using System;
using System.ComponentModel;

namespace sdkMVVMCS.Model
{
    public class Accomplishment : INotifyPropertyChanged
    {
        // Название достижения.
        public string Name { get; set; }

        // Тип достижения, Item (элемент) или Level (уровень).
        public string Type { get; set; }

        // Количество собранных элементов.
        private int _count;
        public int Count
        {
            get
            {
                return _count;
            }
            set
            {
                _count = value;
                RaisePropertyChanged("Count");
            }
        }

        // Завершен ли уровень
        private bool _completed;
        public bool Completed
        {
            get
            {
                return _completed;
            }
            set
            {
                _completed = value;
                RaisePropertyChanged("Completed");
            }
        }


        public event PropertyChangedEventHandler PropertyChanged;

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

        // Создание копии сведений о достижениях для сохранения.
        // Объект участвует в системе привязки данных, данная копия не участвует
        public Accomplishment GetCopy()
        {
            Accomplishment copy = (Accomplishment)this.MemberwiseClone();
            return copy;
        }
    }
}

Модель представлена в виде класса Accomplishment, он реализует интерфейс INotifyPropertyChanged и событие PropertyChanged, что позволяет ему сообщать об изменениях данных, что, в итоге, приводит к обновлению пользовательского интерфейса. То есть, данный класс готов к использованию его в системе привязки данных.

Название достижения (поле Name) и его тип (Type) не меняются при работе, а сведения о количестве собранных элементов (Count) и о том, завершен ли уровень (Completed) могут меняться.

В Листинге 23.2 приведен код файла ViewModel.cs. Это – модель представления, в которой используется коллекция Accomplishment, используемая для привязки данных.

using System;
using System.Windows;
using System.Collections.ObjectModel;
using System.IO.IsolatedStorage;
using sdkMVVMCS.Model;

namespace sdkMVVMCS.ViewModelNS
{
    public class ViewModel
    {
        public ObservableCollection<Accomplishment> Accomplishments { get; set; }


        public void GetAccomplishments()
        {
           //Если в изолированном хранилище имеются параметры
            if (IsolatedStorageSettings.ApplicationSettings.Count > 0)
            {
                // Получаем достижения из хрранилища
                GetSavedAccomplishments();
            }
            else
            {
                //Загружаем список достижений по умолчанию
                GetDefaultAccomplishments();
            }
        }

        //Загрузка списка по умолчанию
        public void GetDefaultAccomplishments()
        {
            ObservableCollection<Accomplishment> a = new ObservableCollection<Accomplishment>();

            // Элементы, которые нужно собрать
            a.Add(new Accomplishment() { Name = "Potions", Type = "Item" });
            a.Add(new Accomplishment() { Name = "Coins", Type = "Item" });
            a.Add(new Accomplishment() { Name = "Hearts", Type = "Item" });
            a.Add(new Accomplishment() { Name = "Swords", Type = "Item" });
            a.Add(new Accomplishment() { Name = "Shields", Type = "Item" });

            // Уровни, которые нужно пройти
            a.Add(new Accomplishment() { Name = "Level 1", Type = "Level" });
            a.Add(new Accomplishment() { Name = "Level 2", Type = "Level" });
            a.Add(new Accomplishment() { Name = "Level 3", Type = "Level" });

            Accomplishments = a;
            MessageBox.Show("Загружен список достижений, заданный по умолчанию");
        }

        //Загрузка списка из хранилища
        public void GetSavedAccomplishments()
        {
            ObservableCollection<Accomplishment> a = new ObservableCollection<Accomplishment>();

            foreach (Object o in IsolatedStorageSettings.ApplicationSettings.Values)
            {
                a.Add((Accomplishment)o);
            }

            Accomplishments = a;
            MessageBox.Show("Загружен список достижений из хранилища");
        }

       //Сохранение списка
        public void SaveAccomplishments()
        {
            IsolatedStorageSettings settings = IsolatedStorageSettings.ApplicationSettings;

            foreach (Accomplishment a in Accomplishments)
            {
                if (settings.Contains(a.Name))
                {
                    settings[a.Name] = a;
                }
                else
                {
                    settings.Add(a.Name, a.GetCopy());
                }
            }

            settings.Save();
            MessageBox.Show("Сохранение достижений завершено");
        }
    }
}

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

В Листинге 23.3 приведен XAML-код файла ItemView.xaml. Его .CS-файл содержит лишь стандартный конструктор, инициализирующий элементы. Это – представление для вывода данных о собранных элементах.

<UserControl x:Class="sdkMVVMCS.View.ItemView"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    FontFamily="{StaticResource PhoneFontFamilyNormal}"
    FontSize="{StaticResource PhoneFontSizeNormal}"
    Foreground="{StaticResource PhoneForegroundBrush}"
    d:DesignHeight="480" d:DesignWidth="480">
    
    <Grid x:Name="LayoutRoot">
        <ListBox ItemsSource="{Binding}">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <Grid>
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition Width="200"/>
                            <ColumnDefinition Width="80"/>
                            <ColumnDefinition Width="100"/>
                        </Grid.ColumnDefinitions>
                        <TextBlock x:Name="Item" Text="{Binding Path=Name, Mode=OneWay}" 
Grid.Column="0" HorizontalAlignment="Left" 
VerticalAlignment="Center" />
                        <TextBox x:Name="Count" Text="{Binding Path=Count, Mode=TwoWay}" 
Grid.Column="1" TextAlignment="Center" 
InputScope="Number"/>
                        <TextBlock x:Name="Check" Text="{Binding Path=Count, Mode=OneWay}" 
Grid.Column="2" HorizontalAlignment="Center" 
VerticalAlignment="Center" />
                    </Grid>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>
    </Grid>
</UserControl>

Здесь всё готово к привязке данных. В списке, который определен в коде, присутствует шаблон данных, содержащий три элемента:

Item – текстовый блок, содержит название элемента (Name), использует одностороннюю привязку данных (OneWay), в процессе работы с приложением не меняется.

Count – текстовое поле, в которое пользователю нужно вводить сведения о собранных элементах. Для его привязки к источнику данных (Count) используется двусторонняя (TwoWay) привязка, для ввода данных в это поле используется цифровая экранная клавиатура (InputScope="Number").

Check – текстовый блок, привязан к источнику данных (Count), то есть, к тому же, что и текстовое поле, используется для проверки того, что данные, введенные в текстовое поле были переданы в модель и обновили её состояние. После ввода данных в текстовое поле здесь отображается введенное в него число. Здесь используется односторонняя привязка.

В Листинге 23.4 приведен код файла LevelView.xaml, представления, которое используется для ввода и отображения сведений о том, завершен ли уровень.

<UserControl x:Class="sdkMVVMCS.View.LevelView"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"

    xmlns:src="clr-namespace:sdkMVVMCS.View"             

    mc:Ignorable="d"
    FontFamily="{StaticResource PhoneFontFamilyNormal}"
    FontSize="{StaticResource PhoneFontSizeNormal}"
    Foreground="{StaticResource PhoneForegroundBrush}"
    d:DesignHeight="480" d:DesignWidth="480">

    <UserControl.Resources>
        <src:BoolOpposite x:Key="BoolOpposite"/>
    </UserControl.Resources>

    <Grid x:Name="LayoutRoot">
        <ListBox ItemsSource="{Binding}">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <Grid>
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition Width="200"/>
                            <ColumnDefinition Width="80"/>
                            <ColumnDefinition Width="100"/>
                        </Grid.ColumnDefinitions>
                        <TextBlock x:Name="Level" Text="{Binding Path=Name, Mode=OneWay}" Grid.Column="0" 
HorizontalAlignment="Left" VerticalAlignment="Center"/>
                        <CheckBox x:Name="Completed" IsChecked="{Binding Path=Completed, Mode=TwoWay}" Grid.Column="1" H
orizontalAlignment="Center" IsEnabled="{Binding Path=Completed, Converter={StaticResource BoolOpposite}}"/>
                        <TextBlock x:Name="Check" Text="{Binding Path=Completed, Mode=OneWay}" Grid.Column="2" 
HorizontalAlignment="Center" VerticalAlignment="Center"/>
                    </Grid>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>
    </Grid>
</UserControl>

Здесь так же есть несколько полей, в объявлении привязок данных присутствуют некоторые особенности:

Level – текстовый блок, содержит название уровня (Name), использует одностороннюю привязку данных (OneWay), в процессе работы с приложением не меняется.

Check – текстовый блок, использует одностороннюю привязку к свойству Completed, применяется для контроля правильности работы системы двусторонней привязки.

Completed – элемент управления Checkbox (флаг), его свойство IsChecked, определяющее то, установлен флаг или снят (то есть, интерпретируется ли его состояние как "истина", или "ложь"), привязано к свойству Completed (оно используется для хранения данных о том, завершен уровень или нет) с помощью двусторонней привязки данных. По умолчанию это свойство устанавливается в значение false, получая такое значение в свойство IsChecked, элемент управления выводится с неустановленным флагом. При установке флага мы не только хотим передать новое значение в модель, но и модифицировать свойство IsEnabled того же самого элемента. Если флаг не установлен (уровень не пройден), это свойство должно быть установлено в значение true – то есть, с флагом можно взаимодействовать и устанавливать его. А если флаг установлен – уровень пройден, его нужно заблокировать, запретив изменения и установив в false его свойство IsEnabled. Можно заметить, что если флаг не установлен (хранит значение false), он должен быть доступен для редактирования (IsEnabled равняется true), если флаг установлен (хранит true), он должен быть заблокирован (IsEnabled равняется false). Для того, чтобы получить подобную логику, можно воспользоваться конвертером значений. Конвертеры – это классы, реализующие интерфейс IValueConverter.

Конвертер BoolOpposite добавлен в качестве ресурса в XAML-файле LevelView.xaml. Его класс определен в файле LevelView.xaml.cs, Листинг 12.5, у него есть два метода – Convert, он преобразует данные при передаче из источника (модели) в приёмник (свойство элемента управления CheckBox) и ConvertBack – данные преобразуются при передаче из приёмника в источник, то есть, выполняет обратное преобразование. В данном случае преобразование логического значения источника, к которому привязано свойство логического типа, заключается в инверсии этого значения.

using System;
using System.Windows.Controls;
using System.Globalization;

namespace sdkMVVMCS.View
{
    public partial class LevelView : UserControl
    {
        public LevelView()
        {
            InitializeComponent();
        }
    }

   // Конвертер значеий для управления возможностью редактировать поле Completed
    // Если флаг не установлен, то есть, имеет значение False, 
свойство IsCompleted устанавливается в True
    //Если установлен - то есть имеет значение True, 
IsEnabled устанавливается в значение False и поле больше нельзя редактировать
   public class BoolOpposite : System.Windows.Data.IValueConverter
    {
       
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            bool b = (bool)value;
            return !b;
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            string s = value as string;
            bool b;

            if (bool.TryParse(s, out b))
            {
                return !b;
            }
            return false;
        }
   }
}

В файле StateUtilities.cs, Листинг 12.6, есть описание статического класса, который используется для хранения данных о режиме работы приложения – о том, запущено ли оно из списка приложений, или пользователь вернулся к нему после недавнего выхода (то есть, это значение позволяет узнать, было ли приложение запущено или активировано после приостановке)

using System;

namespace sdkMVVMCS
{
    public static class StateUtilities
    {
        private static Boolean isLaunching;

        //Устанавливается в true при запуске приложения
        //В false - при активации и при уходе с MainPage.xaml
        public static Boolean IsLaunching
        {
            get { return isLaunching; }
            set { isLaunching = value; }
        }
    }
}

В Листинге 23.7 приведен фрагмент файла App.xaml.cs – обработчики запуска и активации.

// Код для исполнения при запуске приложения (например, с Рабочего стола)
        // TЭтот код не выполняется при активации приложения после деактиваии
        private void Application_Launching(object sender, LaunchingEventArgs e)
        {
            StateUtilities.IsLaunching = true;
        }

        // Код для выполнения при активации приложения 
		// (тогда, когда оно становится приложением переднего плана)
        // Этот код не исполняется при запуске приложения
        private void Application_Activated(object sender, ActivatedEventArgs e)
        {
            StateUtilities.IsLaunching = false;
        }

В Листинге 23.8 приведен код файла MainPage.xaml.

<phone:PhoneApplicationPage 
    x:Class="sdkMVVMCS.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"
    
    xmlns:views="clr-namespace:sdkMVVMCS.View"
    
    mc:Ignorable="d" d:DesignWidth="480" d:DesignHeight="696"
    FontFamily="{StaticResource PhoneFontFamilyNormal}"
    FontSize="{StaticResource PhoneFontSizeNormal}"
    Foreground="{StaticResource PhoneForegroundBrush}"
    SupportedOrientations="Portrait" Orientation="Portrait"
    shell:SystemTray.IsVisible="True">

    <!--LayoutRoot is the root grid where all page content is placed-->
    <Grid x:Name="LayoutRoot" Background="Transparent">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>

        <!--TitlePanel contains the name of the application and page title-->
        <StackPanel x:Name="TitlePanel" Grid.Row="0" Margin="12,17,0,28">
            <TextBlock x:Name="ApplicationTitle" 
Style="{StaticResource PhoneTextNormalStyle}" Text="MVVM Test App"/>
            <TextBlock x:Name="PageTitle" Text="game tracker" 
Margin="9,-7,0,0" Style="{StaticResource PhoneTextTitle1Style}"/>
        </StackPanel>

        <!--ContentPanel - place additional content here-->
        <!--ContentPanel - place additional content here-->
        <Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
            <StackPanel>
                <TextBlock Text="Items Collected" Foreground="{StaticResource PhoneAccentBrush}" 
Style="{StaticResource PhoneTextLargeStyle}" />
                <views:ItemView x:Name="ItemViewOnPage" Height="200"/>

                <TextBlock Text="Levels Completed" Foreground="{StaticResource PhoneAccentBrush}" 
Style="{StaticResource PhoneTextLargeStyle}" />
                <views:LevelView x:Name="LevelViewOnPage" Height="200"/>
            </StackPanel>
        </Grid>
    </Grid>

    <phone:PhoneApplicationPage.ApplicationBar>
        <shell:ApplicationBar IsVisible="True" IsMenuEnabled="True" >
            <shell:ApplicationBarIconButton IconUri="AppBarSave.png" Text="Save"  Click="AppBarSave_Click" />
        </shell:ApplicationBar>
    </phone:PhoneApplicationPage.ApplicationBar>

</phone:PhoneApplicationPage>

Обратите внимание на объявленное пространство имен views, на объявленные в коде пользовательские элементы управления ItemView и LevelView (представления) и на панель приложения, которая содержит кнопку для сохранения данных приложения.

В Листинге 23.9 вы можете видеть код файла MainPage.xaml.cs

using System;
using System.Linq;
using System.Windows;
using Microsoft.Phone.Controls;
using sdkMVVMCS.ViewModelNS;

namespace sdkMVVMCS
{
    public partial class MainPage : PhoneApplicationPage
    {
        private ViewModel vm;

        // Конструктор
        public MainPage()
        {
            InitializeComponent();
            vm = new ViewModel();
        }

        // При переходе на страниу
        protected override void OnNavigatedTo(System.Windows.Navigation.NavigationEventArgs e)
        {
            base.OnNavigatedTo(e);

            if (!StateUtilities.IsLaunching  this.State.ContainsKey("Accomplishments"))
            {
                // Старый экземпляр приложения
                // Пользователь запустил приложение, перейдя к нему по нажатию кнопки Назад.
    
                vm = (ViewModel)this.State["Accomplishments"];
                MessageBox.Show("Получаем сведения из данных состония страницы");
            }
            else
            {
                // Новый экземпля приложения
                // Пользователь запустил приложение из списка,
                // либо нет сохраненного состояния страницы.
    
                vm.GetAccomplishments();
                MessageBox.Show("Получаем данные не из состояния страницы, 
а либо загружаем из хранилища, либо - используем данные по умолчанию");
            }


            // У нас имеется два разных представления (views), но лишь одна модель прдставления (view model.)
            // Поэтому используем LINQ-запросы для выборки данных, 
необходимых каждому из представлений.

            // Задаём контекст данных для представления, 
которое служит для вывода сведенийи о собранных элементах (Item).
            ItemViewOnPage.DataContext = from Accomplishment in vm.Accomplishments where 
Accomplishment.Type == "Item" select Accomplishment;

            // Задаём контекст данных для представления, которое служит для вывода сведений об уровнях (Level).
            LevelViewOnPage.DataContext = from Accomplishment in vm.Accomplishments where 
Accomplishment.Type == "Level" select Accomplishment;

            // Если используется лишь одно представление, его можно заполнить так, как показано ниже
            //AccomplishmentViewOnPage.DataContext = vm.Accomplishments;
        }

        // При уходее со страницы
        protected override void OnNavigatedFrom(System.Windows.Navigation.NavigationEventArgs e)
        {
            base.OnNavigatedFrom(e);

           //Если в словаре State страницы уже имется ключ с таким именем
            if (this.State.ContainsKey("Accomplishments"))
            {
                //Запишем в него данные
                this.State["Accomplishments"] = vm;
            }
            else
            {
                //Если такого ключа нет - создадим его и запишем данные
                this.State.Add("Accomplishments", vm);
            }

            StateUtilities.IsLaunching = false;
        }

        //Щелчок по кнопке панели приложения для сохранения данных
        private void AppBarSave_Click(object sender, EventArgs e)
        {
            //Вызов метода модели представления для сохранения данных
            vm.SaveAccomplishments();
        }
    }
}

Основное назначение этого кода – задать в качестве контекста данных представлений соответствующую модель представления. Здесь имеется переменная vm, которая представляет собой объект класса ViewModel. Так как модель представления одна, а представлений – два, здесь используются LINQ-запросы для выбора нужных данных из модели представления для каждого из представлений.

Кроме того, здесь реализован механизм временного хранения данных в словаре State, который используется для хранения данных, связанных со страницей. Если приложение уже было запущено и пользователь возвращается к нему, активируя (для того, чтобы узнать о состоянии приложения и используется вышерассмотренный класс StateUtilities), делается попытка получить данные из словаря и инициализировать ими модель представления. Если получить данные из словаря не удалось, либо приложение было запущено, например, из списка или с Рабочего стола, выполняется инициализация модели представления её собственными средствами (загрузка значений по умолчанию или загрузка данных из хранилища, если ранее данные были сохранены).

Запись в словарь State делается при уходе со страницы.

По нажатию на кнопку сохранения данных на панели приложения вызывается соответствующий метод модели представления.

Выводы

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

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

Задание

Для дальнейшего освоения использования шаблона MVVM в разработке приложений для Windows Phone, рекомендуется самостоятельно ознакомиться со следующими примерами:

  • "Windows Phone Starter Kit for RSS - WP8" ("Стартовый проект для разработки RSS-приложений для Windows Phone 8", http://code.msdn.microsoft.com/Windows-Phone-Starter-Kit-390ee0ef
  • "Sharing Code between Windows Store and Windows Phone App (PCL + MVVM + OData)" ("Совместное использование кода в приложениях для Магазина Windows и для Windows Phone (PCL + MVVM + OData)"), http://code.msdn.microsoft.com/Sharing-Code-between-411c999b
  • Здесь, для разработки демонстрационных приложений, использован набор инструментов MVVM Light Toolkit. Дополнительные материалы по этому набору инструментов можно найти по следующим адресам: http://www.galasoft.ch/mvvm/, http://mvvmlight.codeplex.com/, http://channel9.msdn.com/ (здесь можно найти множество видеоматериалов, в частности, по MVVM Light).

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

    К данной лабораторной работе подготовлено видеоприложение.

    Страницы:

    Цель работы: освоить методику создания приложений с использованием MVVM

    Приложение, построенное по шаблону MVVM

    В этой лабораторной работе мы рассмотрим пример приложения, построенного с использованием шаблона MVVM. Наш пример основан на примере "Model-View-ViewModel Sample" "Пример приложения, построенного по шаблону модель-представление-модель представления"), http://code.msdn.microsoft.com/Model-View-ViewModel-Sample-8cb92fd9.

    В приложении (рис. 23.1.) реализуется механизм наблюдения за игровыми достижениями.

    (рис 23.1) Проект приложения, созданного с использованием шаблона проектирования MVVM

    На главной странице имеются два списка. В первом выводятся сведения о собранных элементах (Items), во втором – данные о пройденных уровнях. Общая структура проекта, которую вы можете видеть на вышеприведенном рисунке, такова:

    В папке Models расположена модель данных (Accomplishment.cs)

    В папке ViewModel расположена модель представления, ViewModel.cs

    В папке View расположены два представления, ItemView.xaml и LevelView.xaml это – пользовательские элементы управления, настроенные на использование их совместно с системой привязки данных.

    В файле StateUtilities.cs объявлен вспомогательный класс, который используется для хранения сведений о том, запущено ли приложение из списка приложений (или с рабочего стола), или оно активировано после того, как пользователь недавно вышел из него.

    Страница MainPage.xaml содержит представления, механизмы привязки данных из модели представления и другие вспомогательные механизмы.

    В Листинге 23.1 приведен код файла Accomplishment.cs, то есть – модели данных.

    using System;
    using System.ComponentModel;
    
    namespace sdkMVVMCS.Model
    {
        public class Accomplishment : INotifyPropertyChanged
        {
            // Название достижения.
            public string Name { get; set; }
    
            // Тип достижения, Item (элемент) или Level (уровень).
            public string Type { get; set; }
    
            // Количество собранных элементов.
            private int _count;
            public int Count
            {
                get
                {
                    return _count;
                }
                set
                {
                    _count = value;
                    RaisePropertyChanged("Count");
                }
            }
    
            // Завершен ли уровень
            private bool _completed;
            public bool Completed
            {
                get
                {
                    return _completed;
                }
                set
                {
                    _completed = value;
                    RaisePropertyChanged("Completed");
                }
            }
    
    
            public event PropertyChangedEventHandler PropertyChanged;
    
            private void RaisePropertyChanged(string propertyName)
            {
                if (this.PropertyChanged != null)
                {
                    this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
                }
            }
    
            // Создание копии сведений о достижениях для сохранения.
            // Объект участвует в системе привязки данных, данная копия не участвует
            public Accomplishment GetCopy()
            {
                Accomplishment copy = (Accomplishment)this.MemberwiseClone();
                return copy;
            }
        }
    }
    

    Модель представлена в виде класса Accomplishment, он реализует интерфейс INotifyPropertyChanged и событие PropertyChanged, что позволяет ему сообщать об изменениях данных, что, в итоге, приводит к обновлению пользовательского интерфейса. То есть, данный класс готов к использованию его в системе привязки данных.

    Название достижения (поле Name) и его тип (Type) не меняются при работе, а сведения о количестве собранных элементов (Count) и о том, завершен ли уровень (Completed) могут меняться.

    В Листинге 23.2 приведен код файла ViewModel.cs. Это – модель представления, в которой используется коллекция Accomplishment, используемая для привязки данных.

    using System;
    using System.Windows;
    using System.Collections.ObjectModel;
    using System.IO.IsolatedStorage;
    using sdkMVVMCS.Model;
    
    namespace sdkMVVMCS.ViewModelNS
    {
        public class ViewModel
        {
            public ObservableCollection<Accomplishment> Accomplishments { get; set; }
    
    
            public void GetAccomplishments()
            {
               //Если в изолированном хранилище имеются параметры
                if (IsolatedStorageSettings.ApplicationSettings.Count > 0)
                {
                    // Получаем достижения из хрранилища
                    GetSavedAccomplishments();
                }
                else
                {
                    //Загружаем список достижений по умолчанию
                    GetDefaultAccomplishments();
                }
            }
    
            //Загрузка списка по умолчанию
            public void GetDefaultAccomplishments()
            {
                ObservableCollection<Accomplishment> a = new ObservableCollection<Accomplishment>();
    
                // Элементы, которые нужно собрать
                a.Add(new Accomplishment() { Name = "Potions", Type = "Item" });
                a.Add(new Accomplishment() { Name = "Coins", Type = "Item" });
                a.Add(new Accomplishment() { Name = "Hearts", Type = "Item" });
                a.Add(new Accomplishment() { Name = "Swords", Type = "Item" });
                a.Add(new Accomplishment() { Name = "Shields", Type = "Item" });
    
                // Уровни, которые нужно пройти
                a.Add(new Accomplishment() { Name = "Level 1", Type = "Level" });
                a.Add(new Accomplishment() { Name = "Level 2", Type = "Level" });
                a.Add(new Accomplishment() { Name = "Level 3", Type = "Level" });
    
                Accomplishments = a;
                MessageBox.Show("Загружен список достижений, заданный по умолчанию");
            }
    
            //Загрузка списка из хранилища
            public void GetSavedAccomplishments()
            {
                ObservableCollection<Accomplishment> a = new ObservableCollection<Accomplishment>();
    
                foreach (Object o in IsolatedStorageSettings.ApplicationSettings.Values)
                {
                    a.Add((Accomplishment)o);
                }
    
                Accomplishments = a;
                MessageBox.Show("Загружен список достижений из хранилища");
            }
    
           //Сохранение списка
            public void SaveAccomplishments()
            {
                IsolatedStorageSettings settings = IsolatedStorageSettings.ApplicationSettings;
    
                foreach (Accomplishment a in Accomplishments)
                {
                    if (settings.Contains(a.Name))
                    {
                        settings[a.Name] = a;
                    }
                    else
                    {
                        settings.Add(a.Name, a.GetCopy());
                    }
                }
    
                settings.Save();
                MessageBox.Show("Сохранение достижений завершено");
            }
        }
    }
    

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

    В Листинге 23.3 приведен XAML-код файла ItemView.xaml. Его .CS-файл содержит лишь стандартный конструктор, инициализирующий элементы. Это – представление для вывода данных о собранных элементах.

    <UserControl x:Class="sdkMVVMCS.View.ItemView"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        mc:Ignorable="d"
        FontFamily="{StaticResource PhoneFontFamilyNormal}"
        FontSize="{StaticResource PhoneFontSizeNormal}"
        Foreground="{StaticResource PhoneForegroundBrush}"
        d:DesignHeight="480" d:DesignWidth="480">
        
        <Grid x:Name="LayoutRoot">
            <ListBox ItemsSource="{Binding}">
                <ListBox.ItemTemplate>
                    <DataTemplate>
                        <Grid>
                            <Grid.ColumnDefinitions>
                                <ColumnDefinition Width="200"/>
                                <ColumnDefinition Width="80"/>
                                <ColumnDefinition Width="100"/>
                            </Grid.ColumnDefinitions>
                            <TextBlock x:Name="Item" Text="{Binding Path=Name, Mode=OneWay}" 
    Grid.Column="0" HorizontalAlignment="Left" 
    VerticalAlignment="Center" />
                            <TextBox x:Name="Count" Text="{Binding Path=Count, Mode=TwoWay}" 
    Grid.Column="1" TextAlignment="Center" 
    InputScope="Number"/>
                            <TextBlock x:Name="Check" Text="{Binding Path=Count, Mode=OneWay}" 
    Grid.Column="2" HorizontalAlignment="Center" 
    VerticalAlignment="Center" />
                        </Grid>
                    </DataTemplate>
                </ListBox.ItemTemplate>
            </ListBox>
        </Grid>
    </UserControl>
    

    Здесь всё готово к привязке данных. В списке, который определен в коде, присутствует шаблон данных, содержащий три элемента:

    Item – текстовый блок, содержит название элемента (Name), использует одностороннюю привязку данных (OneWay), в процессе работы с приложением не меняется.

    Count – текстовое поле, в которое пользователю нужно вводить сведения о собранных элементах. Для его привязки к источнику данных (Count) используется двусторонняя (TwoWay) привязка, для ввода данных в это поле используется цифровая экранная клавиатура (InputScope="Number").

    Check – текстовый блок, привязан к источнику данных (Count), то есть, к тому же, что и текстовое поле, используется для проверки того, что данные, введенные в текстовое поле были переданы в модель и обновили её состояние. После ввода данных в текстовое поле здесь отображается введенное в него число. Здесь используется односторонняя привязка.

    В Листинге 23.4 приведен код файла LevelView.xaml, представления, которое используется для ввода и отображения сведений о том, завершен ли уровень.

    <UserControl x:Class="sdkMVVMCS.View.LevelView"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    
        xmlns:src="clr-namespace:sdkMVVMCS.View"             
    
        mc:Ignorable="d"
        FontFamily="{StaticResource PhoneFontFamilyNormal}"
        FontSize="{StaticResource PhoneFontSizeNormal}"
        Foreground="{StaticResource PhoneForegroundBrush}"
        d:DesignHeight="480" d:DesignWidth="480">
    
        <UserControl.Resources>
            <src:BoolOpposite x:Key="BoolOpposite"/>
        </UserControl.Resources>
    
        <Grid x:Name="LayoutRoot">
            <ListBox ItemsSource="{Binding}">
                <ListBox.ItemTemplate>
                    <DataTemplate>
                        <Grid>
                            <Grid.ColumnDefinitions>
                                <ColumnDefinition Width="200"/>
                                <ColumnDefinition Width="80"/>
                                <ColumnDefinition Width="100"/>
                            </Grid.ColumnDefinitions>
                            <TextBlock x:Name="Level" Text="{Binding Path=Name, Mode=OneWay}" Grid.Column="0" 
    HorizontalAlignment="Left" VerticalAlignment="Center"/>
                            <CheckBox x:Name="Completed" IsChecked="{Binding Path=Completed, Mode=TwoWay}" Grid.Column="1" H
    orizontalAlignment="Center" IsEnabled="{Binding Path=Completed, Converter={StaticResource BoolOpposite}}"/>
                            <TextBlock x:Name="Check" Text="{Binding Path=Completed, Mode=OneWay}" Grid.Column="2" 
    HorizontalAlignment="Center" VerticalAlignment="Center"/>
                        </Grid>
                    </DataTemplate>
                </ListBox.ItemTemplate>
            </ListBox>
        </Grid>
    </UserControl>
    

    Здесь так же есть несколько полей, в объявлении привязок данных присутствуют некоторые особенности:

    Level – текстовый блок, содержит название уровня (Name), использует одностороннюю привязку данных (OneWay), в процессе работы с приложением не меняется.

    Check – текстовый блок, использует одностороннюю привязку к свойству Completed, применяется для контроля правильности работы системы двусторонней привязки.

    Completed – элемент управления Checkbox (флаг), его свойство IsChecked, определяющее то, установлен флаг или снят (то есть, интерпретируется ли его состояние как "истина", или "ложь"), привязано к свойству Completed (оно используется для хранения данных о том, завершен уровень или нет) с помощью двусторонней привязки данных. По умолчанию это свойство устанавливается в значение false, получая такое значение в свойство IsChecked, элемент управления выводится с неустановленным флагом. При установке флага мы не только хотим передать новое значение в модель, но и модифицировать свойство IsEnabled того же самого элемента. Если флаг не установлен (уровень не пройден), это свойство должно быть установлено в значение true – то есть, с флагом можно взаимодействовать и устанавливать его. А если флаг установлен – уровень пройден, его нужно заблокировать, запретив изменения и установив в false его свойство IsEnabled. Можно заметить, что если флаг не установлен (хранит значение false), он должен быть доступен для редактирования (IsEnabled равняется true), если флаг установлен (хранит true), он должен быть заблокирован (IsEnabled равняется false). Для того, чтобы получить подобную логику, можно воспользоваться конвертером значений. Конвертеры – это классы, реализующие интерфейс IValueConverter.

    Конвертер BoolOpposite добавлен в качестве ресурса в XAML-файле LevelView.xaml. Его класс определен в файле LevelView.xaml.cs, Листинг 12.5, у него есть два метода – Convert, он преобразует данные при передаче из источника (модели) в приёмник (свойство элемента управления CheckBox) и ConvertBack – данные преобразуются при передаче из приёмника в источник, то есть, выполняет обратное преобразование. В данном случае преобразование логического значения источника, к которому привязано свойство логического типа, заключается в инверсии этого значения.

    using System;
    using System.Windows.Controls;
    using System.Globalization;
    
    namespace sdkMVVMCS.View
    {
        public partial class LevelView : UserControl
        {
            public LevelView()
            {
                InitializeComponent();
            }
        }
    
       // Конвертер значеий для управления возможностью редактировать поле Completed
        // Если флаг не установлен, то есть, имеет значение False, 
    свойство IsCompleted устанавливается в True
        //Если установлен - то есть имеет значение True, 
    IsEnabled устанавливается в значение False и поле больше нельзя редактировать
       public class BoolOpposite : System.Windows.Data.IValueConverter
        {
           
            public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
            {
                bool b = (bool)value;
                return !b;
            }
    
            public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
            {
                string s = value as string;
                bool b;
    
                if (bool.TryParse(s, out b))
                {
                    return !b;
                }
                return false;
            }
       }
    }
    

    В файле StateUtilities.cs, Листинг 12.6, есть описание статического класса, который используется для хранения данных о режиме работы приложения – о том, запущено ли оно из списка приложений, или пользователь вернулся к нему после недавнего выхода (то есть, это значение позволяет узнать, было ли приложение запущено или активировано после приостановке)

    using System;
    
    namespace sdkMVVMCS
    {
        public static class StateUtilities
        {
            private static Boolean isLaunching;
    
            //Устанавливается в true при запуске приложения
            //В false - при активации и при уходе с MainPage.xaml
            public static Boolean IsLaunching
            {
                get { return isLaunching; }
                set { isLaunching = value; }
            }
        }
    }
    

    В Листинге 23.7 приведен фрагмент файла App.xaml.cs – обработчики запуска и активации.

    // Код для исполнения при запуске приложения (например, с Рабочего стола)
            // TЭтот код не выполняется при активации приложения после деактиваии
            private void Application_Launching(object sender, LaunchingEventArgs e)
            {
                StateUtilities.IsLaunching = true;
            }
    
            // Код для выполнения при активации приложения 
    		// (тогда, когда оно становится приложением переднего плана)
            // Этот код не исполняется при запуске приложения
            private void Application_Activated(object sender, ActivatedEventArgs e)
            {
                StateUtilities.IsLaunching = false;
            }
    

    В Листинге 23.8 приведен код файла MainPage.xaml.

    <phone:PhoneApplicationPage 
        x:Class="sdkMVVMCS.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"
        
        xmlns:views="clr-namespace:sdkMVVMCS.View"
        
        mc:Ignorable="d" d:DesignWidth="480" d:DesignHeight="696"
        FontFamily="{StaticResource PhoneFontFamilyNormal}"
        FontSize="{StaticResource PhoneFontSizeNormal}"
        Foreground="{StaticResource PhoneForegroundBrush}"
        SupportedOrientations="Portrait" Orientation="Portrait"
        shell:SystemTray.IsVisible="True">
    
        <!--LayoutRoot is the root grid where all page content is placed-->
        <Grid x:Name="LayoutRoot" Background="Transparent">
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="*"/>
            </Grid.RowDefinitions>
    
            <!--TitlePanel contains the name of the application and page title-->
            <StackPanel x:Name="TitlePanel" Grid.Row="0" Margin="12,17,0,28">
                <TextBlock x:Name="ApplicationTitle" 
    Style="{StaticResource PhoneTextNormalStyle}" Text="MVVM Test App"/>
                <TextBlock x:Name="PageTitle" Text="game tracker" 
    Margin="9,-7,0,0" Style="{StaticResource PhoneTextTitle1Style}"/>
            </StackPanel>
    
            <!--ContentPanel - place additional content here-->
            <!--ContentPanel - place additional content here-->
            <Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
                <StackPanel>
                    <TextBlock Text="Items Collected" Foreground="{StaticResource PhoneAccentBrush}" 
    Style="{StaticResource PhoneTextLargeStyle}" />
                    <views:ItemView x:Name="ItemViewOnPage" Height="200"/>
    
                    <TextBlock Text="Levels Completed" Foreground="{StaticResource PhoneAccentBrush}" 
    Style="{StaticResource PhoneTextLargeStyle}" />
                    <views:LevelView x:Name="LevelViewOnPage" Height="200"/>
                </StackPanel>
            </Grid>
        </Grid>
    
        <phone:PhoneApplicationPage.ApplicationBar>
            <shell:ApplicationBar IsVisible="True" IsMenuEnabled="True" >
                <shell:ApplicationBarIconButton IconUri="AppBarSave.png" Text="Save"  Click="AppBarSave_Click" />
            </shell:ApplicationBar>
        </phone:PhoneApplicationPage.ApplicationBar>
    
    </phone:PhoneApplicationPage>
    

    Обратите внимание на объявленное пространство имен views, на объявленные в коде пользовательские элементы управления ItemView и LevelView (представления) и на панель приложения, которая содержит кнопку для сохранения данных приложения.

    В Листинге 23.9 вы можете видеть код файла MainPage.xaml.cs

    using System;
    using System.Linq;
    using System.Windows;
    using Microsoft.Phone.Controls;
    using sdkMVVMCS.ViewModelNS;
    
    namespace sdkMVVMCS
    {
        public partial class MainPage : PhoneApplicationPage
        {
            private ViewModel vm;
    
            // Конструктор
            public MainPage()
            {
                InitializeComponent();
                vm = new ViewModel();
            }
    
            // При переходе на страниу
            protected override void OnNavigatedTo(System.Windows.Navigation.NavigationEventArgs e)
            {
                base.OnNavigatedTo(e);
    
                if (!StateUtilities.IsLaunching  this.State.ContainsKey("Accomplishments"))
                {
                    // Старый экземпляр приложения
                    // Пользователь запустил приложение, перейдя к нему по нажатию кнопки Назад.
        
                    vm = (ViewModel)this.State["Accomplishments"];
                    MessageBox.Show("Получаем сведения из данных состония страницы");
                }
                else
                {
                    // Новый экземпля приложения
                    // Пользователь запустил приложение из списка,
                    // либо нет сохраненного состояния страницы.
        
                    vm.GetAccomplishments();
                    MessageBox.Show("Получаем данные не из состояния страницы, 
    а либо загружаем из хранилища, либо - используем данные по умолчанию");
                }
    
    
                // У нас имеется два разных представления (views), но лишь одна модель прдставления (view model.)
                // Поэтому используем LINQ-запросы для выборки данных, 
    необходимых каждому из представлений.
    
                // Задаём контекст данных для представления, 
    которое служит для вывода сведенийи о собранных элементах (Item).
                ItemViewOnPage.DataContext = from Accomplishment in vm.Accomplishments where 
    Accomplishment.Type == "Item" select Accomplishment;
    
                // Задаём контекст данных для представления, которое служит для вывода сведений об уровнях (Level).
                LevelViewOnPage.DataContext = from Accomplishment in vm.Accomplishments where 
    Accomplishment.Type == "Level" select Accomplishment;
    
                // Если используется лишь одно представление, его можно заполнить так, как показано ниже
                //AccomplishmentViewOnPage.DataContext = vm.Accomplishments;
            }
    
            // При уходее со страницы
            protected override void OnNavigatedFrom(System.Windows.Navigation.NavigationEventArgs e)
            {
                base.OnNavigatedFrom(e);
    
               //Если в словаре State страницы уже имется ключ с таким именем
                if (this.State.ContainsKey("Accomplishments"))
                {
                    //Запишем в него данные
                    this.State["Accomplishments"] = vm;
                }
                else
                {
                    //Если такого ключа нет - создадим его и запишем данные
                    this.State.Add("Accomplishments", vm);
                }
    
                StateUtilities.IsLaunching = false;
            }
    
            //Щелчок по кнопке панели приложения для сохранения данных
            private void AppBarSave_Click(object sender, EventArgs e)
            {
                //Вызов метода модели представления для сохранения данных
                vm.SaveAccomplishments();
            }
        }
    }
    

    Основное назначение этого кода – задать в качестве контекста данных представлений соответствующую модель представления. Здесь имеется переменная vm, которая представляет собой объект класса ViewModel. Так как модель представления одна, а представлений – два, здесь используются LINQ-запросы для выбора нужных данных из модели представления для каждого из представлений.

    Кроме того, здесь реализован механизм временного хранения данных в словаре State, который используется для хранения данных, связанных со страницей. Если приложение уже было запущено и пользователь возвращается к нему, активируя (для того, чтобы узнать о состоянии приложения и используется вышерассмотренный класс StateUtilities), делается попытка получить данные из словаря и инициализировать ими модель представления. Если получить данные из словаря не удалось, либо приложение было запущено, например, из списка или с Рабочего стола, выполняется инициализация модели представления её собственными средствами (загрузка значений по умолчанию или загрузка данных из хранилища, если ранее данные были сохранены).

    Запись в словарь State делается при уходе со страницы.

    По нажатию на кнопку сохранения данных на панели приложения вызывается соответствующий метод модели представления.

    Выводы

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

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

    Задание

    Для дальнейшего освоения использования шаблона MVVM в разработке приложений для Windows Phone, рекомендуется самостоятельно ознакомиться со следующими примерами:

  • "Windows Phone Starter Kit for RSS - WP8" ("Стартовый проект для разработки RSS-приложений для Windows Phone 8", http://code.msdn.microsoft.com/Windows-Phone-Starter-Kit-390ee0ef
  • "Sharing Code between Windows Store and Windows Phone App (PCL + MVVM + OData)" ("Совместное использование кода в приложениях для Магазина Windows и для Windows Phone (PCL + MVVM + OData)"), http://code.msdn.microsoft.com/Sharing-Code-between-411c999b
  • Здесь, для разработки демонстрационных приложений, использован набор инструментов MVVM Light Toolkit. Дополнительные материалы по этому набору инструментов можно найти по следующим адресам: http://www.galasoft.ch/mvvm/, http://mvvmlight.codeplex.com/, http://channel9.msdn.com/ (здесь можно найти множество видеоматериалов, в частности, по MVVM Light).

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

    К данной лабораторной работе подготовлено видеоприложение.

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