30 приложений для Windows Phone: Расход топлива автомобиля

Данный пример был опубликован на сайте MSDN (Building a Windows Phone 7 Application from Start to Finish), а затем статья была перевадена на русский язык и опубликована на Хабрахабре. В статье пошагово объясняются все нюансы создания приложения. Так как во многом они повторяют уже пройденные нами уроки, то я оставил только те моменты, которые непосредственно относятся к приложению Расход топлива автомобиля.

Скачать исходный код

Пользовательские задачи

Размышляя о задачах, которые пользователь может выполнить, используя ваше приложение, вы определите возможности вашего приложения. Ниже приведены некоторые способы, как начать проектирование приложения.

  • Составьте список задач, которые пользователи смогут выполнить.
  • Расставьте приоритеты в списке так, чтобы вы знали, что вы должны реализовать сразу, а что можете оставить для более поздних версий.
  • Если вы работаете в команде, пусть каждый составит свой индивидуальный список пользовательских задач. Обсудите их в команде и попытайтеся сгруппировать их.
  • Создайте эскизы страниц, которые ваше приложение может содержать, и способы, как пользователь сможет перемещаться между ними.
  • Поэкспериментируйте с различными существующими приложениями для Windows Phone, чтобы увидеть, что вам нравится и не нравится в них.<

Наше приложение-пример Fuel Tracker позволяет пользователю отслеживать расход топлива своих автомобилей. На следующем изображении показан процесс мозгового штурма и доска, на которой нарисован план работы приложения.

Ниже в качестве примера приведены некоторые из пользовательских задач, которые были изначально созданы при проектировании приложения Fuel Tracker и их приоритеты.

  • Ввод информации о заправке (процессе заправки автомобиля): 1
  • Ввод информации об автомобиле: 1
  • Просмотр истории заправок: 1
  • Просмотр отчёта «километры/литр»: 1
  • Ввод данных для нескольких автомобилей: 2
  • Сохранение местоположения заправок: 2
  • Карта размещения заправок: 2
  • Переключение единиц измерения (США или метрические): 3
  • Просмотр отчёта «цена/литр»: 3

Приложение Fuel Tracker состоит из трех страниц и окна выбора фотографий. На следующем изображении показаны страницы и пути навигации.

Старайтесь, чтобы навигационная модель была как можно более простой. Когда приложение Fuel Tracker было изначально спроектировано, на каждой странице были представлены кнопки для перехода к любой другой странице. Это привело к следующим проблемам:

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

Для решения этих вопросов, навигационная модель Fuel Tracker была изменена на другую, в которой только первая страница содержит кнопки для навигации на другие страницы, а другие страницы позволяют переходить только к первой странице.

В этой модели первая страница используется повторно, в то время как другие страницы создаются заново при каждом посещении. Другие страницы являются страницами с вводом данных, но поскольку они не используются повторно, они рассматриваются как модальные диалоговые окна. Пользователи могут либо нажать кнопку «Сохранить», чтобы сохранить изменения и перейти к первой странице, либо могут нажать кнопку «Назад», чтобы отменить изменения и вернуться назад.

Обдумывание данных

Вы должны начать думать о данных, которые приложение будет использовать, и как их организовать. Вы можете начать с составления списка необходимых данных и попытаться логически сгруппировать их. В следующей таблице приведены некоторые данные, необходимые для приложения Fuel Tracker. Это поможет вам определить элементы управления, которые необходимы в вашем приложении, и как вы будете передавать данные из пользовательского интерфейса к бизнес-логике вашего приложения. В частях «Отображение данных» и «Создание классов данных», вы узнаете больше о том, как отображать данные в пользовательском интерфейсе и как создавать классы данных для вашего приложения.

ГруппаДанные
АвтомобильНазвание
Фото
Первоначальные показания одометра
История заправокЗаправка
Дата
Показания одометра
Количество топлива
Цена топлива

Создание нового проекта Silverlight for Windows Phone

В Visual Studio создайте новый проект. В диалоговом окне New Project выберите Silverlight for Windows Phone, а затем выберите шаблон проекта (смотри День первый).

После того, как вы создадите проект, Solution Explorer будет похож на следующее изображение.

Добавление новых страниц

Приложения для Windows Phone основываются на модели страниц Silverlight, где пользователи могут перемещаться между экранами, отображающими различное содержимое. Для создания каждого экрана вашего приложения, как правило, создаётся отдельная страница. Вы можете добавлять страницы в ваш проект с помощью диалогового окна Add New Item.

При создании нового проекта Windows Phone Application в Visual Studio стандартная стартовая страница с именем MainPage создаётся автоматически. Вы можете переместить или переименовать эту страницу, но если вы сделаете это, вы должны также изменить свойство NavigationPage элемента DefaultTask в файле WMAppManifest.xml.

Добавление панели приложения

Процесс работы с панелью приложений описан в День шестой. Application Bar. Следующий XAML-код реализует панель приложения на странице заправки приложения Fuel Tracker, которая служит основной навигационной точкой приложения. В этом примере кнопки связанны с обработчиком события Click в C#-коде. Обработчики события Click в свою очередь выполняют переход на другую страницу.


<phone:PhoneApplicationPage.ApplicationBar>
    <shell:ApplicationBar IsVisible="True" IsMenuEnabled="True">
        <shell:ApplicationBarIconButton IconUri="/Images/fillup.png"
            Text="add fill-up" Click="FillupButton_Click" IsEnabled="False" />
        <shell:ApplicationBarIconButton IconUri="/Images/car.png"
            Text="add car" Click="CarButton_Click" />
    </shell:ApplicationBar>
</phone:PhoneApplicationPage.ApplicationBar>

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

  • Размер 48 пикселей на 48 пикселей.
  • Должен быть белого цвета на прозрачном фоне.
  • Не включает в себя изображения круга, поскольку он отображается панелью приложения.

Ниже показаны значки панели приложения, используемые в приложении Fuel Tracker.

Ниже показано, как значки панели приложения выглядят при тёмной и светлой теме.

Чтобы добавить значки панели приложения, добавьте их в проект и установите Build Action в Content. В XAML-коде, установите свойству IconUri для элемента ApplicationBarIconButton относительный URI изображения, как показано в следующем фрагменте.


<shell:ApplicationBarIconButton IconUri="/Images/appbar.save.rest.png"
        Text="Save Car" Click="SaveButton_Click" />

Навигация между страницами

Если в вашем приложении больше одной страницы, вам придётся предоставить пользователям возможность перемещаться между этими страницами.

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

Самым простым способом реализовать навигацию между страницами является использование элемента управления HyperlinkButton. Вы можете использовать его свойство NavigationUri для того, чтобы перейти к нужной странице. Если вы не хотите использовать HyperlinkButton, вы можете реализовать навигацию с помощью класса NavigationService. Этот класс содержит несколько свойств, методов и событий, которые помогут вам с навигацией. Вы можете использовать метод NavigationService.Navigate для перехода к определенной странице. Смотри День второй. Создаем страницы с навигацией.

Например, в приложении Fuel Tracker на странице информации об автомобиле и странице информации о заправке есть кнопка «Сохранить», которая сохраняет внесенные пользователем изменения, а затем осуществляет переход назад к первой странице со сводной информацией. Если пользователь нажмёт вместо этого кнопку «Назад», переход назад произойдёт без сохранения данных. Приложение должно всегда запрашивать подтверждение перед тем, как сбросить пользовательские данные, поэтому полезно реагировать на событие нажатия кнопки «Назад». Дополнительную информацию вы сможете найти в части «Валидация вводимых данных».

Вы должны реализовать только такое поведение кнопки «Назад», которое осуществляет переход назад или скрывает контекстное меню и диалоговые окна. Все другие реализации запрещены.

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

В приложении Fuel Tracker большинство элементов управления размещены динамически с использованием элементов управления Grid и StackPanel. На следующем изображении показана страница сведений об автомобиле, которая использует Grid. Строки и столбцы были определены с динамическими размерами (Auto и звёздочка). На изображении справа вы можете видеть, что размер текста был увеличен и разметка автоматически подстроилась.

Поддержка портретной и альбомной ориентации с помощью сетки

  • Измените свойство страницы SupportedOrientations на PortraitOrLandscape
  • Используйте Grid в качестве панели для содержимого
  • Если необходимо, создайте обработчик события OrientationChanged и добавьте код, чтобы переместить элементы в Grid

Приложение Fuel Tracker использует сетку, но также использует элементы управления ScrollViewer для того, чтобы гарантировать, что все элементы в списке могут быть просмотрены пользователем. Разметка достаточно проста, так что нет необходимости вносить изменения, связанные с обработкой события OrientationChanged. На следующем изображении показано поведение приложения при смене ориентации на странице информации об автомобиле.

Добавление изображения на страницу

Для отображения изображений на странице, вы можете указать изображение в вашем проекте или привязаться (bind) к изображению во время выполнения (run time).

Для того, чтобы добавить изображение на страницу:

  • Добавьте изображение в формате JPG или PNG к проекту и установите Build Action в значение Content.
  • Добавьте элемент управления Image на страницу.
  • Установите свойству Source элемента управления Image путь к месту расположения файла изображения.

Следующий фрагмент XAML-кода показывает, как отобразить изображение с именем myPicture.jpg в папке Images (изображения) в Solution Explorer.


<Image Source="/Images/myPicture.jpg">

Вы также можете отображать картинки, получая к ним доступ во время выполнения. Следующий фрагмент XAML-кода приложения Fuel Tracker показывает, как установить свойству Source значение свойства Picture объекта Car , используя возможности разметки.


<Image Height="75" Width="75" Margin="15" Stretch="UniformToFill"
    Source="{Binding Picture}" VerticalAlignment="Top"/>

Добавление фонового изображения на страницу

Возможно, вы захотите отобразить изображение позади элементов управления и других элементов пользовательского интерфейса на странице. Первым шагом в добавлении фонового изображения на страницу является выбор изображения для отображения. Некоторые рекомендации при выборе фонового изображения: Выбирайте изображение, которое отображается одинаково хорошо как при светлой, так и темной теме. Используйте форматы файлов JPG или PNG. Если вы собираетесь использовать прозрачность изображения, вы должны использовать PNG-файл.

Избегайте использования слишком большого количества белого цвета в приложениях, напримера, белого фона, поскольку это может оказать серьёзное влияние на время автономной работы устройств с OLED дисплем. Возможно, вам потребуется несколько проб и ошибок, чтобы выбрать соответствующий фон, который отображается одинаково хорошо как при светлой, так и темной теме. Убедитесь, что проверили фон на обеих темах. Ниже показано изображение фона страницы, используемое в приложении Fuel Tracker, оно имеет прозрачный фон.

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

Для того, чтобы добавить фоновое изображение на страницу:

Добавьте изображения в проект и установите Build Action в значение Content.

Создайте ресурс ImageBrush и установите свойству ImageSource значение относительного URI (унифицированный идентификатор ресурса) изображения. Вы также должны установить Key (ключ) для изображения. Если вы используете PNG-файл, вы также можете дополнительно задать свойство Opacity (непрозрачность) в пределах между 0 и 1 и свойство Stretch (растяжение) для того, чтобы указать, как вы хотите изменить размер изображения, чтобы оно соответствовало отведенному ему пространству.

Для приложения Fuel Tracker, ImageBrush объявлен в App.xaml, так как эта кисть (brush) используется для всех страниц.


<ImageBrush x:Key="gasBrush" ImageSource="/Images/gasPump.png" Opacity=".1" Stretch="UniformToFill" />

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

 
<!--LayoutRoot is the root grid where all page content is placed-->
<Grid x:Name="LayoutRoot" Background="{StaticResource gasBrush}">

Можно также применить ImageBrush с помощью окна Properties, как показано на следующем изображении.

Добавление значка приложения

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

  • PNG-формат
  • 62 х 62 пикселя

Ниже приведен значок приложения для Fuel Tracker.

Ниже показано, как выглядит значок приложения в списке приложений.

Для того, чтобы установить значок приложения:

Добавьте значок в проект и установите Build Action в значение Content.

В дизайнере проекта на вкладке Application укажите в раскрывающемся списке Icon ваш значок приложения.

Кроме того, вы можете изменить элемент IconPath в файле WMAppManifest.xml, чтобы выбрать иконку приложения, как показано в следующем фрагменте разметки.


<IconPath IsRelative="true" IsResource="false">gasIcon.png</IconPath>

Добавление плитки приложения

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

  • PNG-формат
  • 173 х 173 пикселя

Ниже приведена плитка приложения Fuel Tracker.

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

Для того, чтобы установить плитку приложения:

Добавьте картинку в проект и установите Build Action в значение Content.

В дизайнере проекта на вкладке Application укажите в раскрывающемся списке Background image вашу плитку.

Кроме того, вы можете изменить элемент BackgroundImageURI в файле WMAppManifest.xml, чтобы выбрать иконку приложения, как показано в следующем фрагменте разметки.


<PrimaryToken TokenID="FuelTrackerToken" TaskName="_default">
    <TemplateType5>
        <BackgroundImageURI IsRelative="true"
            IsResource="false">gasTileIcon.png</BackgroundImageURI>
        <Count>0</Count>
        <Title>Fuel Tracker</Title>
    </TemplateType5>
</PrimaryToken>

Использование встроенных стилей

В следующем примере показан TextBlock со страницы со сводной информацией приложения Fuel Tracker, где свойство Style имеет значение стиля, являющегося встроенным в Windows Phone ресурсом, с помощью расширения разметки.


<TextBlock Grid.Row="0" Grid.Column="0"
    Style="{StaticResource PhoneTextLargeStyle}"
    Text="CURRENT MPG:"
    HorizontalAlignment="Right" VerticalAlignment="Center"/>

Подробнее о стилях смотри в День пятый. Темы и расцветка.

Приложение Fuel Tracker использует стили во многих местах. Например, страница со сводной информацией содержит стили LabelStyle и ValueStyle в первом элементе Pivot, который определен в разделе ресурсов Grid. В файле страницы со сводной информацией эти стили применяются к объектам TextBlock. Следующий фрагмент XAML-кода показывает пример того, как определяются стили и как они применяются.


<Grid.Resources>
    <Style x:Key="LabelStyle" TargetType="TextBlock">
        <Setter Property="HorizontalAlignment" Value="Right"/>
        <Setter Property="Margin" Value="15,5" />
    </Style>
    <Style x:Key="ValueStyle" TargetType="TextBlock">
        <Setter Property="Margin" Value="15,5" />
    </Style> 
</Grid.Resources>
 
. . .
 
<TextBlock Grid.Row="2" Grid.Column="0"
    Style="{StaticResource LabelStyle}"
    Text="date:"></TextBlock>
 
<TextBlock Grid.Row="2" Grid.Column="1"
    Style="{StaticResource ValueStyle}"
    Text="{Binding FillupHistory[0].Date,
        Converter={StaticResource StringFormatter},     
		ConverterParameter=\{0:d\} }" >
</TextBlock>

Привязка данных к к пользовательскому интерфейсу

Приложение Fuel Tracker имеет три страницы с данными. Данные хранятся в основном в трёх классах. На следующем изображении показаны страницы и связанные с ними классы.

Для отображения данных обычно используется привязка данных (data binding). Привязка данных предоставляет возможность подключения пользовательского интерфейса к источнику данных. Когда привязки созданы и источник данных изменяется, элементы пользовательского интерфейса, которые связаны с источником данных отображают изменения автоматически. Аналогично, изменения, внесённые пользователем в элемент пользовательского интерфейса отражаются в источнике данных. Например, если пользователь изменит значение в TextBox, соответствующий источник данных автоматически обновится, чтобы отразить эти изменения.

Следующий фрагмент XAML-кода иллюстрирует синтаксис, который используется для привязки свойства Text элемента управления TextBox к свойству Name объекта-источника.


<TextBox x:Name="NameTextBox" Text="{Binding Name, Mode=TwoWay}" />

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

Каждая привязка имеет свойство Mode (режим), которое определяет, как и когда обновляются данные. OneWay (односторонняя) привязка означает, что целевой (target) элемент пользовательского интерфейса обновляется, если источник изменяется. TwoWay (двухсторонняя) привязка означает, что как цель, так и источник обновляются, если кто-либо из них изменяется. Если вы используете OneWay или TwoWay привязки, то для того, чтобы привязка была уведомлена об изменениях объекта-источника, необходимо реализовать интерфейс INotifyPropertyChanged.

Вы указываете объект-источник, устанавливая свойство DataContext (контекст данных). Если вы используете элемент управления ListBox, необходимо указать объект-источник, установив свойство ItemsSource. В следующем примере показано, как задать свойству DataContext панели CarHeader объект-источник, извлекаемый из статического свойства.


CarHeader.DataContext = CarDataStore.Car;

Свойство DataContext позволяет установить стандартную привязку для всего элемента пользовательского интерфейса, включая все его дочерние элементы. В некоторых случаях вам будет удобней установить свойство DataContext для всей страницы, а в некоторых будет удобней установить его отдельно для каждого элемента на странице. Установка DataContext на каждом уровне XAML перекрывает любые установки на более высоком уровне. Кроме того, вы можете фактически переопределить любую установку DataContext для отдельных привязок, установив для них свойство Source (источник).

Например, в приложение Fuel Tracker каждая страница устанавливает DataContext в различное значение. Однако, на странице FillupPage DataContext уровня страницы переопределяется для панели, которая отображает название автомобиля и его фотографию. На следующем изображении показаны настройки контекста данных для приложения Fuel Tracker.

Использование data binding builder

Visual Studio включает в себя data binding builder (создатель привязок данных), чтобы помочь вам создать привязки данных в XAML. Хотя data binding builder может обеспечить повышение производительности, он не поддерживает все возможные сценарии. Например, он не поддерживает привязку к индексированным элементам и он не распознаёт привязки, созданные в коде. Таким образом, в некоторых случаях вам потребуется указать привязки данных вручную.

Для того, чтобы использовать data binding builder:

  • Выберите элемент управления, для которого вы хотите создать привязку.
  • В окне Properties найдите свойство, к которому вы хотите привязаться.
  • На краю левой колонки щелкните значок маркера свойства, а затем нажмите кнопку Apply Data Binding. Откроется data binding builder.
  • Кликните по различным панелям в data binding builder для того, чтобы раскрыть различные опции.

На следующем изображении показан data binding builder со следующими параметрами:

  • Data context: объект Fillup
  • Binding path: свойство OdometerReading
  • Converter: ZeroFormatter
  • Mode: TwoWay

Следующий фрагмент XAML показывает код, сгенерированный data binding builder.


<TextBox
    Grid.Row="0" Grid.Column="1"
    x:Name="OdometerTextBox"
    Text="{Binding OdometerReading,
	       Mode=TwoWay,
		   Converter={StaticResource ZeroFormatter}
		   }"
    InputScope="TelephoneNumber"
    MaxLength="8" />

Примечание: Когда вы устанавливаете свойство DataContext в коде, data binding builder не имеет никакого представления о свойствах, доступных в источнике данных и не может помочь вам выбрать binding path (путь). Однако, вы можете решить эту проблему, установив data context во время разработки (design-time). Дополнительные сведения по установке data context во время разработки вы можете найти здесь: Walkthrough: Using a DesignInstance to Bind to Data in the Silverlight Designer.

Отображение данных в списке

Отображение коллекции элементов в списке — одна из основных задач на телефоне. Для того, чтобы отобразить коллекцию элементов в списке с помощью привязки данных вам необходимо сделать следующее:

  • Добавить ListBox в ваше приложение.
  • Указать источник данных для ListBox путем привязки коллекции к свойству ItemsSource.
  • Чтобы настроить внешний вид каждого элемента в ListBox, добавьте шаблон данных для ListBox.
  • В шаблоне данных привяжите элементы ListBox к свойствам коллекции элементов.

На следующем изображении показаны привязки для ListBox на странице сводной информации приложения Fuel Tracker.

Следующий XAML-код показывает, как были указаны привязки для ListBox.


<ListBox ItemContainerStyle="{StaticResource ListBoxStyle}"
    ItemsSource="{Binding FillupHistory}"
    Height="380" HorizontalAlignment="Left" Margin="5,25,0,0"
    VerticalAlignment="Top" Width="444" >
    <ListBox.ItemTemplate>
    <DataTemplate>
        <StackPanel Orientation="Horizontal">
        <TextBlock Style="{StaticResource SummaryStyle}"
            Text="{Binding Date, Converter={StaticResource
			    StringFormatter}, ConverterParameter=\{0:d\} }"
            Width="105" TextWrapping="Wrap"/>
        <TextBlock Style="{StaticResource SummaryStyle}"
            Text="{Binding FuelQuantity}" TextWrapping="Wrap" />
        <TextBlock Style="{StaticResource SummaryStyle}"
            Text="{Binding DistanceDriven}" TextWrapping="Wrap" />
        <TextBlock Style="{StaticResource SummaryStyle}" 
            Text="{Binding PricePerFuelUnit,
			    Converter={StaticResource StringFormatter},
				ConverterParameter=\{0:c\}, ConverterCulture=en-US}" />
        <TextBlock Style="{StaticResource SummaryStyle}"
            Text="{Binding FuelEfficiency,
			    Converter={StaticResource StringFormatter},
				ConverterParameter=\{0:F\}}" TextWrapping="Wrap" />
        </StackPanel>
    </DataTemplate>
    </ListBox.ItemTemplate>
</ListBox>

В предыдущем XAML-коде, свойство ListBox.ItemsSource привязано к свойству Car.FillupHistory так, что каждый объект Fillup в коллекции истории появится в качестве отдельного пункта в ListBox. Элемент DataTemplate определяет внешний вид каждого пункта и содержит несколько элементов TextBlock, каждый из которых привязан к свойству класса Fillup.

Этот XAML будет работать только тогда, когда объект Car впервые ассоциирован со страницей, как показано в следующем коде из SummaryPage.xaml.cs.


this.DataContext = CarDataStore.Car;

Сложные пути привязки

В дополнение к гибкости настройки свойства DataContext на любом уровне (что позволяет переопределить настройки на более высоком уровне), можно также указать сложные пути привязки для того, чтобы «пробуриться» в свойства ссылки, такой как Car.FillupHistory. Например, следующий XAML-код из SummaryPage.xaml демонстрирует привязку к свойству Fillup.FuelEfficiency первого пункта в коллекции истории заправок.


<TextBlock Text="{Binding FillupHistory[0].FuelEfficiency,
     Converter={StaticResource StringFormatter},
     ConverterParameter=\{0:F\}}" />

На следующем изображении показаны привязки в SummaryPage.xaml и показывается, как сложные привязки и шаблоны данных позволяют вам привязывать элементы управления к различным свойствам различных объектов, даже если они все относятся к тому же DataContext.

Зеленые прямоугольники на левом экране Pivot показывают элементы управления, которые привязаны с использованием сложных путей. Эти пути начинаются с первого элемента (индекс 0) в коллекции Car.FillupHistory и заканчиваются различными свойствами класса Fillup. Например, поле Current MPG использует путь привязки FillupHistory[0].FuelEfficiency. Если вы включите в этот путь настройку DataContext страницы, весь путь привязки будет выглядеть следующим образом: CarDataStore.Car.FillupHistory[0].FuelEfficiency.

Преобразование значений

Вы могли заметить, что в примерах в предыдущей части приведены привязки, которые включают настройку Converter. Этот параметр позволяет элементу управления отображать значения привязанного свойства в некотором другом формате. В этих конкретных примерах значения отображаются с использованием формата строки, указанного в настройке ConverterParameter. Следующий XAML-код показывает Converter и ConverterParameter для Date TextBlock.


<TextBlock Style="{StaticResource SummaryStyle}"
    Text="{Binding Date, Converter={StaticResource     StringFormatter}, 
	    ConverterParameter=\{0:d\} }"
    Width="105" TextWrapping="Wrap"/>

В целом, конвертеры значений позволяют вам привязывать элементы управления к произвольным свойствам, даже если свойство имеет несовместимый тип или его значение хранится не в желаемом формате. Вы можете реализовать дополнительные свойства ваших объектов данных, которые просто обернут (wrap) другие свойства и обеспечат необходимое преобразование или форматирование для целей привязки. Однако, лучше не загромождать вашу модель данных кодом, который требуется лишь отдельными представлениям (views). Создавая конвертеры значений, вы можете инкапсулировать код преобразования отдельно как от модели данных, так и от представлений, которые его используют.

В предыдущих примерах свойство Converter имеет значение объекта, определённого как ресурс. Следующий код XAML создает новый экземпляр StringFormatter и объявляет его доступным для Grid как StaticResource под названием «StringFormatter».


<Grid.Resources>
    <local:StringFormatter x:Key="StringFormatter" />
</Grid.Resources>

Для того, чтобы выполнять преобразования значений, класс StringFormatter должен реализовывать интерфейс IValueConverter, который определяет методы Convert и ConvertBack. Поскольку привязка является OneWay, класс StringFormatter нуждается только в методе Convert, который форматирует строку, используя метод String.Format, как показано в следующем коде:


public class StringFormatter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter,
        CultureInfo culture)
    {
        // Retrieve the format string and use it to format the value.
        var formatString = parameter as string;
        if (!string.IsNullOrEmpty(formatString))
        {
            return string.Format(culture, formatString, value);
        }
 
        // If the format string is null or empty, simply call ToString()
        // on the value.
        return value.ToString();
    }
 
    // No need to implement converting back on a one-way binding
    public object ConvertBack(object value, Type targetType,
        object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

На следующем изображении показано поведение, когда конвертер StringFormatter не используется, и, соответственно, когда он используется.

Другой пример преобразования значения в файле ZeroFormatter.cs. Приложение Fuel Tracker использует класс ZeroFormatter для отображения нулевых значений в виде пустых строк. Это удобно при привязке к объектам, для которых пользователи будут указывать значения во время ввода данных. Например, целочисленные свойства для новых объектов устанавливаются равными нулю по умолчанию, но элементы управления, которые отображают эти свойства должны появляться пустыми до того, как пользователь введёт значение.

Создание классов данных

Fuel Tracker представляет собой простое приложение и использует CLR объекты для своей модели данных. На следующем изображении показаны классы Car, Fillup, и CarDataStore для приложения Fuel Tracker. Есть и другие классы в приложении, но эти являются основными.

Класс Car хранит информацию об автомобиле пользователя. Класс Fillup содержит информацию о каждой заправке. Все свойства Car и Fillup представляют собой простые типы значений, за исключением свойства Car.FillupHistory, которое является коллекцией заправок. Класс CarDataStore является общим классом, который содержит методы для сохранения и загрузки данных Car и Fillup, к которым привязывается пользовательский интерфейс страниц.

Обратите внимание, что классы Car и Fillup реализуют интерфейс INotifyPropertyChanged. INotifyPropertyChanged требуется для большинства типов привязки данных, чтобы всегда отображать пользовательский интерфейс в актуальном состоянии.

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

Для того, чтобы предоставить возможность уведомлений об изменениях, классы должны реализовывать интерфейс INotifyPropertyChanged. Уведомление об изменении не всегда необходимо, но это требуется в очень многих распространенных сценариях, так что полезно реализовать INotifyPropertyChanged просто на всякий случай. Реализация проста, как показано в следующем примере кода из Car.cs.


public class Car : INotifyPropertyChanged
{
    private string _name;
    public string Name
    {
        get { return _name; }
        set
        {
            _name = value;
            NotifyPropertyChanged("Name");
        }
    }
 
    // ... other properties ...
 
    public event PropertyChangedEventHandler PropertyChanged = delegate { };
 
    private void NotifyPropertyChanged(string propertyName)
    {
        PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
    }
}

Когда вы привязываете объекты к пользовательскому интерфейсу, binding engine подписывается на событие объекта PropertyChanged так, чтобы он смог обновить все привязанные элементы управления всякий раз, когда произойдут изменения значений свойств. Обратите внимание, что событие PropertyChanged инициализируется пустым делегатом, чтобы гарантировать, что оно никогда не равно null. Это позволяет методу NotifyPropertyChanged вызывать событие без необходимости предварительной проверки, существуют ли у него какие-либо подписчики. Если вы не знакомы с этим интерфейсом, вы можете просто повторить этот шаблон для реализации.

Уведомление об изменении для коллекции

Для полной поддержки уведомлений об изменении для коллекции, например, о добавлении или удалении из коллекции, коллекция должна реализовывать интерфейс INotifyCollectionChanged. ObservableCollection является динамической коллекцией данных, предоставленной framework'ом, она может содержать generic типы. ObservableCollection реализует как INotifyPropertyChanged, так и INotifyCollectionChanged, так что самый простой способ поддержки уведомлений об изменении для коллекции — просто поместить свои элементы в ObservableCollection. Элементы, которые вы поместили в ObservableCollection, должны реализовывать INotifyPropertyChanged, если вы хотите также получать уведомления об изменении свойств для объектов в коллекции.

Приложение Fuel Tracker использует одну коллекцию с именем FillupHistory, содержащую информацию обо всех заправках. FillupHistory является ObservableCollection из объектов Fillup, как показано в следующем примере кода из Car.cs.


public ObservableCollection FillupHistory
{
    get { return _fillupHistory; }
    set
    {
      _fillupHistory = value;
        if (_fillupHistory != null)
        {
            _fillupHistory.CollectionChanged += delegate
            {
                NotifyPropertyChanged("AverageFuelEfficiency");
            };
        }
        NotifyPropertyChanged("FillupHistory");
        NotifyPropertyChanged("AverageFuelEfficiency");
    }
}

Этот код также демонстрирует, как использовать уведомления об изменениях для взаимодействия с другими свойствами. Изменение истории заправок (fill-up history) или любых элементов внутри повлияет на расчет средней эффективности использования топлива (average fuel efficiency). Поэтому установка свойства FillupHistory вызывает уведомление об изменении для себя и для свойства AverageFuelEfficiency. Кроме того, установив свойство связанным с обработчиком события CollectionChanged для новой коллекции, вы получите уведомление об изменении для AverageFuelEfficiency всякий раз, когда элемент в коллекции будет добавлен, удалён или изменён.

Использование классов доступа к данным

Полезно инкапсулировать код сохранения и получения данных в класс доступа к данным. Например, если вы загружаете данные в перегрузке метода OnNavigatedTo, каждая страница может использовать один и тот же класс доступа к данным. Этот класс может загружать данные при первом обращении и предоставлять кэшированную копию для каждого последующего обращения.

Приложение FuelTracker включает в себя класс доступа к данным с именем CarDataStore, который инкапсулирует весь код, необходимый для сохранения и получения данных. Он включает в себя свойство Car и несколько методов для взаимодействия с данными.

Каждая страница приложения Fuel Tracker отображает данные из одного объекта Car, который приложение должно получить из хранилища. Статический класс CarDataStore предоставляет доступ к данным через статическое свойство Car, как показано в следующем фрагменте кода.


private static Car car;
public static Car Car
{
    get
    {
        if (car == null)
        {
            // Initialize the car field using data retrieved from storage
            // or create a new Car object if there is no data in storage.
            // (The code for doing this is shown and described later.)
        }
        return car;
    }
    set { car = value; }
}

Этот код позволяет методу OnNavigatedTo, перегруженному для каждой страницы, получать значение свойства, не обращая внимания на то, были ли данные уже загружены. Например, как было описано в части «Отображение данных», класс FillupPage устанавливает свойство DataContext элемента пользовательского интерфейса в значение свойства CarDataStore.Car. Это представлено в перегруженном методе OnNavigatedTo, как показано в следующем фрагменте кода.


protected override void OnNavigatedTo(NavigationEventArgs e)
{
    base.OnNavigatedTo(e);
 
    CarHeader.DataContext = CarDataStore.Car;
 
    // ... other code ...
}

Сохранение данных в изолированном хранилище

Для того, чтобы сохранить сериализуемый объект в изолированное хранилище с использованием словаря, вы просто присвиваете объект IsolatedStorageSettings.ApplicationSettings и указываете ключ. Затем вы вызываете метод IsolatedStorageSettings.Save. Вызов Save не требуется, потому что приложение будет автоматически сохранять все хранящиеся значения при выходе из приложения.

В следующем фрагменте кода показано, как сохранить объект Car (без учета его несериализуемого свойства Picture) в словаре. Метод CarDataStore.SaveCar присваивает значение свойства Car непосредственно словарю IsolatedStorageSettings.ApplicationSettings, используя константу CAR_KEY. Значение будет записано в изолированное хранилище, когда метод IsolatedStorageSettings.Save будет вызван в следующей строке.


private const string CAR_PHOTO_FILE_NAME = "CarPhoto.jpg";
private const string CAR_KEY = "FuelTracker.Car";
private static readonly IsolatedStorageSettings appSettings =
    IsolatedStorageSettings.ApplicationSettings;
 
public static void SaveCar(Action errorCallback)
{
    try
    {
        appSettings[CAR_KEY] = Car;
        appSettings.Save();
        SaveCarPhoto(CAR_PHOTO_FILE_NAME, Car.Picture, errorCallback);
        DeleteTempCarPhoto();
    }
    catch (IsolatedStorageException)
    {
        errorCallback();
    }
}

Вполне возможно, что нет доступного пространства в изолированном хранилище. В этом случае вызов метода Save выбросит exception (исключение). Важно обработать этот exception, но не имеет смысла делать это на уровне доступа к данным (in the data access layer). Вместо этого, как правило отображается предупреждение для пользователя. По этой причине метод SaveCar принимает делегат Action, который вызывается при возникновении исключения. Это позволяет вызывающему коду обрабатывать ошибки по-своему, без необходимости в собственном Try/Catch блоке или какой-либо зависимости от определенного типа исключения. Этот способ будет более подробно описан в части «Валидация вводимых данных».

Свойство Car.Picture представляет собой изображение автомобиля и является объектом типа BitmapImage. Поскольку тип BitmapImage не сериализуем, свойство Car.Picture не может быть сохранено в словаре IsolatedStorageSettings.ApplicationSettings, поэтому изображение будет рассматриваться отдельно. Однако, вы должны явно исключить свойство Picture из сериализации или вызов метода Save всегда будет бросать исключение. Чтобы исключить свойство из сериализации, вы должны применять IgnoreDataMemberAttribute к нему, как показано в следующем примере из Car.cs.


[System.Runtime.Serialization.IgnoreDataMemberAttribute]
public BitmapImage Picture
{
    get { return _picture; }
    set
    {
        _picture = value;
        NotifyPropertyChanged("Picture");
    }
}

Чтобы сохранить файл в изолированное хранилище, используйте методы IsolatedStorageFile.CreateFile или IsolatedStorageFile.OpenFile. Также есть методы в IsolatedStorageFile для проверки, существует ли файл, и для создания каталогов.

Поскольку свойство Car.Picture не сериализуемо, оно сохраняется в виде файла в изолированном хранилище. Было бы здорово, если бы вы могли бы просто обратиться к выбранной фотографии по ссылке в медиа-библиотеке (media library) так, чтобы вам не нужно было сохранять его отдельно в изолированном хранилище. Однако, на момент написания статьи это не представлялось возможным. Дополнительная информация будет представлена в части «Доступ к фотографиям на Windows Phone».

В следующем коде показан метод SaveCarPhoto, который сохраняет изображение автомобиля как файл в каталоге в изолированном хранилище. Метод OpenFile используется для создания нового файла. Обратите внимание на использование метода Extensions.SaveJpeg. Этот вспомогательный метод упрощает преобразование BitmapImage в Stream.


private const string CAR_PHOTO_DIR_NAME = "FuelTracker";
 
private static void SaveCarPhoto(string fileName, BitmapImage carPicture,
    Action errorCallback)
{
    if (carPicture == null) return;
    try
    {
        using (var store = IsolatedStorageFile.GetUserStoreForApplication())
        {
            var bitmap = new WriteableBitmap(carPicture);
            var path = Path.Combine(CAR_PHOTO_DIR_NAME, fileName);
 
            if (!store.DirectoryExists(CAR_PHOTO_DIR_NAME))
            {
                store.CreateDirectory(CAR_PHOTO_DIR_NAME);
            }
 
            using (var stream = store.OpenFile(path, FileMode.Create))
            {
                Extensions.SaveJpeg(bitmap, stream,
                    bitmap.PixelWidth, bitmap.PixelHeight, 0, 100);
            }
        }
    }
    catch (IsolatedStorageException)
    {
        errorCallback();
    }
}

На следующем изображении показано, как объект Car сохраняется в изолированном хранилище.

Для чтения объекта из словаря в изолированном хранилище, вы просто указываете ключ объекта, который вы хотите. Следующий код демонстрирует, как извлечь значения из словаря в изолированном хранилище.


private static Car car;
 
public static Car Car
{
    get
    {
        if (car == null)
        {
            if (appSettings.Contains(CAR_KEY))
            {
                car = (Car)appSettings[CAR_KEY];
                car.Picture = GetCarPhoto(CAR_PHOTO_FILE_NAME);
            }
            else
            {
                car = new Car()
                {
                    FillupHistory = new ObservableCollection()
                };
            }
        }
        return car;
    }
    set { car = value; }
}

В этом коде, если свойство Car еще не имеет значения, проверяется, содержит ли словарь значение для ключа указанного константой CAR_KEY. (Использование константы позволяет выбрать значение с помощью IntelliSense, что позволяет избежать возможность опечатки.) Если нет существующего значения, инициализируется новый экземпляр объекта Car и его свойство FillupHistory. В противном случае Car берется из словаря. Однако, следует отметить, что свойство Car.Picture имеет тип BitmapImage, который не является сериализуемым. Поскольку словарь IsolatedStorageSettings поддерживает только сериализуемые значения, свойство Picture должно обрабатываться отдельно.

Для чтения файлов из изолированного хранилища используйте метод IsolatedStorageFile.OpenFile. В следующем коде показан метод GetCarPhoto в приложении Fuel Tracker, получающий изображение из изолированного хранилища.


private static BitmapImage GetCarPhoto(string fileName)
{
    using (IsolatedStorageFile store =
        IsolatedStorageFile.GetUserStoreForApplication())
    {
        string path = Path.Combine(CAR_PHOTO_DIR_NAME, fileName);
 
        if (!store.FileExists(path)) return null;
 
        IsolatedStorageFileStream stream =
            store.OpenFile(path, FileMode.Open);
 
        try
        {
            var image = new BitmapImage();
            image.SetSource(stream);
            return image;
        }
        finally
        {
            stream.Dispose();
        }
    }
}

Для того, чтобы удалить значения из словаря состояний, вызовите метод IsolatedStorageSettings.Remove и укажите ключ. Затем вызовите метод IsolatedStorageSettings.Save для применения изменений. Для того, чтобы удалить файл из изолированного хранилища, вызовите метод IsolatedStorageFile.DeleteFile.

Следующий метод CarDataStore.DeleteCar демонстрирует, как удалить значение из словаря состояний и удалить файл.


public static void DeleteCar()
{
    Car = null;
    appSettings.Remove(CAR_KEY);
    appSettings.Save();
    DeleteCarPhoto();
    DeleteTempCarPhoto();
}

Метод CarDataStore.DeleteCar сначала устанавливает свойство Car в null так, чтобы оно могло быть повторно инициализировано в случае обращения к нему в будущем. Затем запись CAR_KEY удаляется из словаря ApplicationSettings, и метод Save вызывается для того, чтобы применить изменения. Наконец, этот метод удаляет фотографии автомобиля из изолированного хранилища.

Восстановление приложения после дезактивации

Приложение Fuel Tracker сохраняет временные состояния уровня страницы в процессе tombstoning, но не имеет данных о состоянии уровня приложений для сохранения. Каждая страница сохраняет данные о состоянии с своё свойство PhoneApplicationPage.State. Например, класс FillupPage содержит объект Fillup, привязанный к некоторым текстовым полям. Этот объект сохраняется на длительное хранение, только когда пользователь нажимает кнопку «Save». Однако, во время tombstoning страница помещает объект в словарь State (состояние) страницы наряду со значением, указывающим, вносил ли пользователь изменения в него, как показано в следующем фрагменте кода.


private const string CURRENT_FILLUP_KEY = "CurrentFillup";
private const string HAS_UNSAVED_CHANGES_KEY = "HasUnsavedChanges";
private Fillup currentFillup;
private bool hasUnsavedChanges;
 
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
    base.OnNavigatedFrom(e);
    if (cacheChanges)
    {
        CommitTextBoxes();
        this.State[CURRENT_FILLUP_KEY] = currentFillup;
        this.State[HAS_UNSAVED_CHANGES_KEY] = hasUnsavedChanges;
    }
}
 
protected override void OnNavigatedTo(NavigationEventArgs e)
{
    base.OnNavigatedTo(e);
 
    CarHeader.DataContext = CarDataStore.Car;
 
    DataContext = currentFillup =
        State.ContainsKey(CURRENT_FILLUP_KEY) ?
        (Fillup)this.State[CURRENT_FILLUP_KEY] :
        new Fillup() { Date = DateTime.Now };
 
    hasUnsavedChanges = State.ContainsKey(HAS_UNSAVED_CHANGES_KEY) ?
        (bool)State[HAS_UNSAVED_CHANGES_KEY] : false;
}

В этом коде перегруженный метод OnNavigatedFrom сначала проверяет, должен ли он кэшировать данные о состоянии. Класс FillupPage инициализирует поле cacheChanges в значение true, но устанавливает его в false всегда, когда происходит обычная навигация (то есть, когда пользователь нажимает кнопку «Save» или «Back»). Таким образом, страница кэширует данные только тогда, когда навигация происходит в результате дезактивации.

Если изменения были кэшированы, метод OnNavigatedFrom применяет значения текстовых полей к привязанному (bound) объекту (хранящемуся в поле currentFillup), а затем сохраняет значения currentFillup и hasUnsavedChanges в словарь State. Применение (committing) значения текстового поля необходимо, поскольку привязки данных обычно происходят, когда элемент управления теряет фокус, что не происходит автоматически, когда приложение дезактивируется.

Метод OnNavigatedTo устанавливает элемент CarHeader в свойство DataContext, а затем пытается получить хранящиеся значения из словаря State. Если значения представлены в словаре, они используются для установки свойства DataContext страницы в дополнение к полям currentFillup и hasUnsavedChanges. В противном случае, используются значения по умолчанию.

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

Валидация вводимых данных

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

Обычно полезно ограничить данные, которые пользователи могут ввести. Вот два простых способа, как можно ограничить вводимые пользователем данные:

  • Установить свойство TextBox.MaxLength так, чтобы пользователи могли вводить только ожидаемое количество текста.
  • Установить свойство TextBox.InputScope, так чтобы пользователи могли вводить только ожидаемый тип данных (такой как цифровые данные).

Даже с этими ограничениями, как правило, еще можно ввести значения, которые неправильно отформатированы или недействительны по другим причинам.

Ваши требования к валидации могут быть различными, но, как правило, вы будете проверять данные, чтобы предотвратить сохранение неправильных значений. В этом случае полезно привязывать пользовательский интерфейс ко временным объектам, чем привязывать непосредственно к хранилищу данных. Например, в приложении Fuel Tracker, новые данные о заправке не добавляются в коллекцию FillupHistory, пока пользователь не нажмёт кнопку «Save». Однако, прежде чем они будут сохранены пользователь может просмотреть данные, чтобы убедиться, что они указаны точно. Когда пользователь нажмёт кнопку «Save», код валидации выполнит одну последнюю проверку на ошибки и предупредит пользователя о любых проблемах.

Следующий код демонстрирует простой метод проверки, используемый в классе Fillup.


public IEnumerable<string> Validate()
{
    if (OdometerReading <= 0) yield return
        "The odometer value must be greater than zero.";
 
    if (DistanceDriven <= 0) yield return
        "The odometer value must be greater than the previous value.";
 
    if (FuelQuantity <= 0) yield return
        "The fuel quantity must be greater than zero.";
 
    if (PricePerFuelUnit <= 0) yield return
        "The fuel price must be greater than zero.";
}

На следующем изображении показан пример сообщения, которое отображается при нажатии кнопки «Save», когда указано значение показаний одометра меньше предыдущего значения.

Этот код просто проверяет значения свойства Fillup и возвращает сообщения об ошибках для любых недопустимых значений. Однако, не все проверяемые значения указываются пользователем, поэтому некоторая инициализация должна произойти заранее. В частности, расстояние должно быть рассчитано путем сравнения значения Fillup.OdometerReading с предыдущим значением показаний одометра. Однако, текущий объект Fillup не имеет доступа к предыдущему значению, так что это действие осуществляется методом CarDataStore.SaveFillup, как показано в следующем фрагменте кода.


public static SaveResult SaveFillup(Fillup fillup, Action errorCallback)
{
    var lastReading =
        Car.FillupHistory.Count > 0 ?
        Car.FillupHistory.First().OdometerReading :
        Car.InitialOdometerReading;
    fillup.DistanceDriven = fillup.OdometerReading - lastReading;
 
    var saveResult = new SaveResult();
    var validationResults = fillup.Validate();
    if (validationResults.Count() > 0)
    {
        saveResult.SaveSuccessful = false;
        saveResult.ErrorMessages = validationResults;
    }
    else
    {
        Car.FillupHistory.Insert(0, fillup);
        saveResult.SaveSuccessful = true;
        SaveCar(delegate {
            saveResult.SaveSuccessful = false;
            errorCallback(); });
    }
    return saveResult;
}

Метод CarDataStore.SaveFillup принимает объект Fillup для сохранения и делегат Action для обработки ошибок. Этот метод начинается с вычитания из текущих показаний одометра предыдущего значения показаний и сохраняет результаты в свойстве Fillup.DistanceDriven так, чтобы проверка молга быть осуществлена в этом классе. Далее метод SaveFillup вызывает метод Fillup.Validate и сохраняет результаты в специальный объект SaveResult, который он возвращает вызывающему методу. Если ошибок валидации не происходит, метод SaveFillup сохраняет данные о заправке и об автомобиле. Наконец, метод SaveFillup возвращает объект SaveResult.

Примечание: Silverlight for Windows Phone предоставляет базовую, встроенную в привязку валидацию через свойство Binding.ValidatesOnExceptions. Тем не менее, версии элементов управления для Windows Phone не предоставляют шаблонов валидации. Для поддержки валидации одним из вариантов является предоставление собственных шаблонов валидации для элементов управления.

Следующий фрагмент кода демонстрирует, что происходит, когда пользователь нажимает кнопку «Save» на странице информации о заправке.


private void SaveButton_Click(object sender, EventArgs e)
{
    CommitTextBoxes();
 
    SaveResult result = CarDataStore.SaveFillup(currentFillup,
        delegate {
            MessageBox.Show("There is not enough space on your phone to " +
            "save your fill-up data. Free some space and try again.",
            "Warning", MessageBoxButton.OK);
        });
 
    if (result.SaveSuccessful)
    {
        Microsoft.Phone.Shell.PhoneApplicationService.Current
            .State["FillupSaved"] = true;
        cacheChanges = false;
        NavigationService.GoBack();
    }
    else
    {
         string errorMessages = String.Join(
            Environment.NewLine + Environment.NewLine,
            result.ErrorMessages.ToArray());
         if (!String.IsNullOrEmpty(errorMessages))
         {
             MessageBox.Show(errorMessages,
             "Warning: Invalid Values", MessageBoxButton.OK);
         }
    }
}

Этот код вызывает метод SaveFillup, передав ему callback ошибки, как описано выше. Этот делегат просто отображает условие ошибки в окне сообщения. Затем метод проверяет, есть ли ошибки валидации, обработчик события отображает их в окне сообщения.

Если сохранение прошло успешно, метод SaveButton_Click использует словарь состояния уровня приложения для хранения значения, указывающего, что новая информация о заправке только что была сохранена. Затем метод осуществляет переход назад, хотя сначала он устанавливает флаг, чтобы предотвратить кэширование, как было описано ранее в части «Восстановлении приложения после деактивации». После выполнения перехода, если SummaryPage обнаруживает, что значение хранится в словаре состояний, страница отображает первый пункт сводной информации так, чтобы пользователь мог сразу увидеть результаты заправки. В противном случае, SummaryPage будет автоматически отображаться в зависимости от того, какой пункт сводной информации показывался до перехода к FillupPage.

Здесь описан только один способ валидации, но помимо него существует ещё множество других.

По умолчанию, нажатие кнопки «Назад» осуществляет переход назад. Если у вас есть страница ввода данных с несохраненными изменениями, и пользователь нажимает кнопку «Назад», вы должны сообщить об этом пользователю. Чтобы реализовать данное поведение, можно перегрузить метод PhoneApplicationPage.OnBackKeyPress и выполнить проверку на несохраненные изменения. Если есть несохраненные изменения, вы можете сообщить об этом пользователю и, возможно, отменить переход назад.

Сертификационное требование

Для поддержки согласованного пользовательского опыта, кнопка «Назад» должна использоваться только для перехода назад в приложении.

Для выполнения этого требования, вы не должны использовать перегрузку OnBackKeyPress для отмены перехода назад и полностью изменять поведение кнопки «Назад». Возможность отмены навигации назад предоставляется для того, чтобы выполнить необходимые связанные с навигацией операции, такие, как ожидание подтверждения пользователем удаления данных.

Приложение Fuel Tracker имеет две страницы для ввода данных: CarDetailsPage и FillupPage. Если есть несохраненные изменения на этих страницах и пользователь нажимает «Назад», на экране появится сообщение, требующее подтверждение, которое позволяет отменить переход назад. На следующем изображении показан пример сообщения, требующего подтверждение.

Действия приложения, которые перезаписывают или удаляют данные, или не могут быть обратимы должны иметь кнопку «Отмена». Следующий фрагмент кода показывает, как приложение Fuel Tracker перегружает метод PhoneApplicationPage.OnBackKeyPress для отображения сообщения, требующего подтверждение, если есть несохраненные данные.


protected override void OnBackKeyPress(
    System.ComponentModel.CancelEventArgs e)
{
    base.OnBackKeyPress(e);
 
    // If there are no changes, do nothing.
    if (!hasUnsavedChanges) return;
 
    var result = MessageBox.Show("You are about to discard your " +
        "changes. Continue?", "Warning", MessageBoxButton.OKCancel);
    if (result == MessageBoxResult.OK)
    {
        // Normal navigation; there's no need to handle tombstoning,
        // so set the cacheChanges flag and clear the current cache.
        cacheChanges = false;
        this.State.Remove(CURRENT_FILLUP_KEY);
    }
    else
    {
        // Cancel backward navigation.
        e.Cancel = true;
    }
}

Сначала этот метод проверяет, есть ли несохраненные изменения. Если их нет, то метод не делает ничего, и переход назад происходит автоматически. В противном случае, метод отображает сообщение, требующее подтверждение. Если пользователь нажимает «ОК», чтобы продолжить переход назад, то странице не нужно обрабатывать tombstoning, так что метод выполняет некоторые действия и выполняет переход назад. Если пользователь нажимает «Отмена», то переход назад будет отменен.

Источник: Создание приложения для Windows Phone 7 от начала до конца.