Привязка данных и страницы

98

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

Создание проекта приложения

Создайте в Visual Studio новый проект приложения Blank Application и дайте ему название AutoShop. Добавьте в приложение XML-файл XmlDataProviderFile.xml в котором содержится информация о автомобилях. Я использовал этот файл в примерах по WPF для демонстрации встроенного поставщика данных XmlDataProvider (в Metro поставщики данных отсутствуют).

Также нам потребуется набор картинок, используемых как мини-изображения автомобилей. Добавьте в проект папку images (правой кнопкой по названию проекта в Solution Explorer --> Add --> New Folder) и добавьте изображения из следующего архива - DataBindingImages.rar.

Как вы поняли, для работы с данными (добавление, обновление, удаление) мы будем использовать локальный XML-файл. Если вы хотите использовать более естественный подход, например использование базы данных, рассмотрите примеры с привязкой данных, которые я создал в разделе по WPF (там используется ADO.NET Entity). Несмотря на то, что Metro обладает сильно урезанной функциональностью по сравнению с WPF, код доступа к данным будет аналогичным и не зависящим от используемой платформы.

Код доступа к данным и ViewModel

Разделение модели данных и представления является хорошей практикой. Модель-представление (ViewModel) является важнейшей частью в данной архитектуре и представляет роль посредника между моделью и представлением. Как я сказал, в нашем тестовом приложении мы будем частично использовать эту архитектуру, в частности я не буду создавать модель (для такого простого приложения она попросту не нужна). Модель-представление будет напрямую оперировать данными.

Итак, добавьте в проект папку Data, в которую добавьте файл класса (меню Project --> Add Class) со следующим содержимым:

using System.ComponentModel;

namespace AutoShop.Data
{
    public class AutoItem : INotifyPropertyChanged
    {
        private string name, model, description, pictureSource;
        private int cost, id;

        public string Name
        {
            get { return name; }
            set { name = value; NotifyPropertyChanged("Name"); }
        }

        public string Model
        {
            get { return model; }
            set { model = value; NotifyPropertyChanged("Model"); }
        }

        public string Description
        {
            get { return description; }
            set { description = value; NotifyPropertyChanged("Description"); }
        }

        public int ID
        {
            get { return id; }
            set { id = value; NotifyPropertyChanged("ID"); }
        }

        public int Cost
        {
            get { return cost; }
            set { cost = value; NotifyPropertyChanged("Cost"); }
        }

        public string PictureSource
        {
            get { return pictureSource; }
            set { pictureSource = value; NotifyPropertyChanged("PictureSource"); }
        }

        // Реализация INotifyPropertyChanged
        public event PropertyChangedEventHandler PropertyChanged;
        private void NotifyPropertyChanged(string propName)
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(propName));
            }
        }
    }
}

Класс AutoItem определяет структуру данных с информацией об автомобилях представленных в автомагазине (марка и модель машины, стоимость, описание, ссылка на изображение). Единственным примечательным аспектом данного класса является то, что он поддерживает уведомления о изменениях (реализуя интерфейс System.ComponentModel.INotifyPropertyChanged). При изменении значения свойств этого класса будет срабатывать событие PropertyChangedEventHandler, которое, позже, мы будем обрабатывать.

Обратите внимание, что мы используем пространство имен AutoShop.Data, поэтому при проектировании страниц мы будем добавлять на него ссылку.

Второй класс, который мы добавим также в папку Data, будет реализовывать модель-представление и называться ViewModel:

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Xml.Linq;

namespace AutoShop.Data
{
    public class ViewModel : INotifyPropertyChanged
    {
        private ObservableCollection<AutoItem> autoList;
        private List<string> modelList;
        private int selectedItemIndex;
        private string selectModelList;

        public ViewModel()
        {
            autoList = this.GetAutos("XmlDataProviderFile.xml");
            modelList = this.GetAutos("XmlDataProviderFile.xml")
                .Select(p => p.Name).Distinct().ToList();
            selectedItemIndex = -1;
        }

        public ObservableCollection<AutoItem> AutoList
        {
            get { return autoList; }
        }

        public List<string> ModelList
        {
            get { return modelList; }
        }

        public int SelectedItemIndex
        {
            get { return selectedItemIndex; }
            set
            {
                selectedItemIndex = value; NotifyPropertyChanged("SelectedItemIndex");
            }
        }

        public string SelectedModelList
        {
            get { return selectModelList; }
            set
            {
                selectModelList = value; NotifyPropertyChanged("SelectedModelList");
            }
        }

        // Вспомогательный метод, для создания коллекции объектов
        // AutoItem из XML файла (используется LINQ to XML)
        public ObservableCollection<AutoItem> GetAutos(string pathToXmlFile)
        {
            // Загружаем XML-документ из файла
            XDocument xdoc = XDocument.Load(pathToXmlFile);

            // Используем возможности LINQ to Objects, LINQ to XML и
            // лямбда выражений для создания коллекции
            // элементов AutoItem из XML-файла
            List<AutoItem> list = xdoc.Root.Elements("Car")
                .Select(
                   p =>
                   {
                       return new AutoItem
                       {
                           Name = p.Element("ModelName").Value,
                           ID = Int32.Parse(p.Element("ID").Value),
                           Model = p.Element("ModelNumber").Value,
                           Description = p.Element("Description").Value,
                           Cost = Int32.Parse(p.Element("Cost").Value),
                           PictureSource = p.Element("ImageCar").Value
                       };
                   }
                ).ToList();

            return new ObservableCollection<AutoItem>(list);
        }

        // Реализация INotifyPropertyChanged
        public event PropertyChangedEventHandler PropertyChanged;
        private void NotifyPropertyChanged(string propName)
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(propName));
            }
        }
    }
}

Самой важной частью этого модель-представления является получение списка автомобилей и информации о них из локального XML-файла. Список машин представлен переменной autoList, которая инициализируется с помощью вспомогательного метода GetAutos. В этом методе используются возможности LINQ to XML по загрузке XML-содержимого из файла, его чтения и динамического создания объектов AutoItem. Переменная autoList имеет тип ObservableCollection, т.к. нам нужна коллекция, поддерживающая уведомления о изменениях.

modelList содержит названия всех марок автомобилей. Она будет использоваться для подсветки в пользовательском интерфейсе машин, относящихся к одной фирме но имеющих разные модели. Эта коллекция получается из коллекции autoList путем использования стандартных LINQ-запросов Select (для извлечения строковой коллекции с марками машин) и Distinct (для извлечения уникальной коллекции, без повторяющихся значениях).

Свойства SelectedItemIndex и SelectedModelList являются вспомогательными и будут использоваться при обновлении представления.

Обратите внимание что в этом классе, как и в классе AutoItem, все закрытые переменные реализованы в виде общедоступных свойств. Это необходимо для использования механизма привязки в представлении.

Создание главной страницы

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

По умолчанию Visual Studio создает главную страницу приложения MainPage.xaml, которая будет запускаться при открытии приложения. Давайте удалим эту страницу и добавим новую с более подходящим именем ListItem.xaml. Для добавления новой страницы выберите в меню Project --> Add New Item --> Blank Page:

Добавление новой страницы в Metro-приложение

Для того, чтобы сделать эту страницу стартовой, понадобиться изменить файл App.xaml.cs, в частности указать тип новой страницы. Прежде чем это сделать, видоизмените пространство имен страницы ListItem.xaml (для данных мы использовали пространство имен AutoShop.Data, логично, что для страниц мы будем использовать AutoShop.Pages). Теперь можно изменить файл приложения App.xaml.cs:

...

if (rootFrame.Content == null)
{
      if (!rootFrame.Navigate(typeof(Pages.ListItem), args.Arguments))
      {
            throw new Exception("Failed to create initial page");
      }
}
Window.Current.Activate();

...

Ниже представлена XAML-разметка страницы ListItem.xaml:

<Page
    x:Class="AutoShop.Pages.ListItem"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:AutoShop"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d">

    <Grid Background="{StaticResource AppBackground}">
        <Grid.ColumnDefinitions>
            <ColumnDefinition/>
            <ColumnDefinition/>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition/>
            <RowDefinition/>
        </Grid.RowDefinitions>

        <TextBlock Style="{StaticResource HeaderTextStyle}" Margin="10,20"
                   Text="Список машин"/>
        <ListBox x:Name="autoList" ItemsSource="{Binding AutoList}" 
                 ItemTemplate="{StaticResource AutoListItem}" Grid.Row="1" Grid.RowSpan="2"
                 ScrollViewer.VerticalScrollBarVisibility="Hidden"
                 SelectionChanged="autoList_SelectionChanged"/>
        <TextBlock Style="{StaticResource HeaderTextStyle}" Margin="10,20"
                   Text="Информация" Grid.Column="1"/>
        <Frame x:Name="AutoDetailFrame" Grid.Column="1" Grid.Row="1" Grid.RowSpan="2" />
    </Grid>
</Page>

Как видите, компоновка приложения довольно проста - используется элемент Grid, разделенный на три строки и два столбца. В первой строке указываются заголовки разделов (элементы TextBlock), вторую и третью строки первого столбца занимает ListBox, представляющий список машин. Во втором столбце используется элемент Frame для динамической загрузки страницы с информацией о машине.

Если вы вставите этот код в окно Visual Studio, то эта IDE-среда выделит несколько ошибок, связанных с отсутствием ресурсов на которые мы ссылаемся (например цвет фона приложения AppBackground или шаблон данных AutoListItem). При этом стиль HeaderTextStyle не подсвечивается, так как это стандартный стиль, определенный в словаре ресурсов StandardStyles.xaml.

Стили и шаблоны мы добавим чуть позже, а пока не обращайте внимание на эти ошибки. Давайте теперь добавим код C# в ListItem.xaml.cs:

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Navigation;
using AutoShop.Data;

namespace AutoShop.Pages
{
    public sealed partial class ListItem : Page
    {
        ViewModel viewModel;

        public ListItem()
        {
            viewModel = new ViewModel();

            this.InitializeComponent();
            this.DataContext = viewModel;
        }

        protected override void OnNavigatedTo(NavigationEventArgs e)
        {
        }

        private void autoList_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            viewModel.SelectedItemIndex = autoList.SelectedIndex;
        }
    }
}

В конструкторе класса ListItem мы создаем экземпляр ViewModel, который задаем в качестве контекста данных для окна. Если вы не знаете, контекст данных нужен для использования привязок в элементах на странице. Например, в ListBox мы привязали свойство ItemsSource к AutoList (показано в XAML-разметке выше), это говорит приложению искать общедоступное свойство AutoList в контексте данных, в нашем случае в классе ViewModel. Т.к. AutoList представлено коллекцией (необходимо для списковых элементов управления), то ListBox отобразит эту коллекцию.

Так же мы добавили в этом коде обработчик события autoList_SelectionChanged, который срабатывает, когда пользователь выбирает элемент в ListBox. Этот обработчик приравнивает свойство модель-представления SelectedItemIndex к номеру выбранного элемента в ListBox. Это понадобиться нам тогда, когда мы будем отображать информацию о машине.

Добавление словаря ресурсов

Ранее мы определили пользовательский интерфейс приложения и задали несколько стилей. При этом эти стили нигде не определены. Стили можно поместить в разные части приложения: в файл приложения App.xaml, в ресурсы страницы, в существующий словарь ресурсов StandardStyles.xaml или в новый словарь ресурсов.

Наиболее оптимальным вариантом является создание нового словаря ресурсов. Давайте его создадим, для этого выделим папку Common в Solution Explorer и выберем в меню Project --> Add New Item --> Resource Dictionary. Назовем его AutoAppStyles.xaml. Я не случайно поместил его в папку Common - в ней уже содержится словарь стандартных Metro-ресурсов StandardStyles.xaml, логично было бы поместить туда и вновь созданный словарь ресурсов.

Добавим в него определение кистей, стилей и шаблонов данных, а также ссылку на StandardStyles.xaml:

<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">

    <ResourceDictionary.MergedDictionaries>
        <ResourceDictionary Source="/Common/StandardStyles.xaml" />
    </ResourceDictionary.MergedDictionaries>

    <!-- Фон приложения -->
    <SolidColorBrush x:Key="AppBackground" Color="#0cb3dd"/>

    <!-- Стиль текстовых элементов в шаблоне данных AutoListItem -->
    <Style x:Key="TextBlockListItem" TargetType="TextBlock" BasedOn="{StaticResource BasicTextStyle}" >
        <Setter Property="FontSize" Value="36"/>
        <Setter Property="FontWeight" Value="Light"/>
        <Setter Property="Margin" Value="8,0"/>
        <Setter Property="VerticalAlignment" Value="Center"/>
    </Style>
    
    <!-- Шаблон данных, определяющий представление ListBox -->
    <DataTemplate x:Key="AutoListItem">
        <StackPanel Orientation="Horizontal">
            <TextBlock Style="{StaticResource TextBlockListItem}" Width="50"
                       Text="{Binding ID}"/>
            <TextBlock Style="{StaticResource TextBlockListItem}"
                       Text="{Binding Name}"/>
            <TextBlock Style="{StaticResource TextBlockListItem}"
                       Text="{Binding Model}" Margin="0"/>
        </StackPanel>
    </DataTemplate>

</ResourceDictionary>

Когда вы добавите все ресурсы в данный словарь, обратите внимание, что Visual Studio все еще генерирует ошибки об отсутствии ресурсов. Это потому, что словарь ресурсов не включен в приложение. Чтобы его включить, откройте файл App.xaml и добавьте ссылку на этот словарь:

<Application
    x:Class="AutoShop.App"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:AutoShop">

    <Application.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="Common/StandardStyles.xaml"/>
                <ResourceDictionary Source="Common/AutoAppStyles.xaml"/>
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </Application.Resources>
</Application>

Переопределение стандартных стилей

На данный момент мы получили работоспособное приложение, которое можно запустить. Однако, при запуске, вы увидите неприятную особенность - цвет фона ListBox является белым, что отрицательно сказывается на юзабилити интерфейса:

Стандартный стиль ListBox

Для изменения фона можно задать специальный стиль для элементов ListBoxItem, однако, можно переопределить стандартную кисть для этого элемента (список всех стандартных цветов Metro я приведу позже):

<!-- Стандартные стили для ListBox -->
<SolidColorBrush x:Key="ListBoxBackgroundThemeBrush" Color="#0cb3dd" />
<SolidColorBrush x:Key="ListBoxFocusBackgroundThemeBrush" Color="#0cb3dd" />

Если вы теперь запустите наше приложение, то убедитесь, что цвет фона сменился и соответствует цвету приложения:

Измененный цвет фона ListBox

Вставка в макет других страниц

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

Для начала добавим новую страницу NoItemSelected.xaml в подпапку Pages приложения:

<Page
    x:Class="AutoShop.Pages.NoItemSelected"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:AutoShop"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d">

    <Grid Background="{StaticResource AppBackground}" Margin="10,0">
        <TextBlock Style="{StaticResource HeaderTextStyle}" 
                   FontSize="26" Text="Ничего не выбрано"/>
    </Grid>
</Page>

Это очень простая страница - настолько простая, что пользы от ее включения в главную страницу практически нет, так как она отображает некоторый статический текст. Но это позволяет продемонстрировать важную особенность приложений Metro, позволяя мне разделить мое приложение на управляемые части.

Ключом к добавлению страниц в основной макет приложения является элемент Frame, который мы добавили в ListItem.xaml. Чтобы сослаться на эту страницу, в конструкторе класса ListItem добавим вызов метода Navigate для элемента Frame:

...
   
public ListItem()
{
            viewModel = new ViewModel();

            this.InitializeComponent();
            this.DataContext = viewModel;
            AutoDetailFrame.Navigate(typeof(Pages.NoItemSelected));
}
Добавление простой страницы в макет

Аргументом метода Navigate является тип страницы, которую нужно включить внутри фрейма.

В данный момент пользы от этого включения нет. Однако, вы можете использовать Frame для динамической вставки различных страниц в макет в зависимости от состояния вашего приложения. Чтобы продемонстрировать это, я создал новую страницу AutoDetail.xaml в подпапке Pages:

<Page
    x:Class="AutoShop.Pages.AutoDetail"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:AutoShop.Pages"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d">

    <Grid Background="{StaticResource AppBackground}">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="1.2*"/>
            <RowDefinition/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>

        <TextBlock Text="Название:" Style="{StaticResource AutoDetailText}" />
        <TextBlock Text="Стоимость:" Style="{StaticResource AutoDetailText}"
                   Grid.Row="1"/>
        <TextBlock Text="Марка:" Style="{StaticResource AutoDetailText}"
                   Grid.Row="2"/>
        <TextBlock Text="Описание:" Style="{StaticResource AutoDetailText}"
                   Grid.Row="3" VerticalAlignment="Top"/>

        <TextBox x:Name="AutoDetailName" Style="{StaticResource AutoDetailTextBox}"
                 Grid.Column="1" IsReadOnly="True"/>
        <TextBox x:Name="AutoDetailCost" Style="{StaticResource AutoDetailTextBox}" 
                 Grid.Row="1" Grid.Column="1" IsReadOnly="True"/>
        <ComboBox x:Name="AutoDetailCar" Style="{StaticResource AutoDetailComboBox}"
                  Grid.Column="1" Grid.Row="2" 
                  SelectionChanged="AutoDetailCar_SelectionChanged" DisplayMemberPath="" />
        <TextBox x:Name="AutoDetailsDescription" Grid.Row="3" Grid.Column="1" 
                 TextWrapping="Wrap" FontSize="18" Margin="12" IsReadOnly="True"
                 ScrollViewer.VerticalScrollBarVisibility="Auto"
                 Background="{StaticResource AppBackground}" Foreground="#ddd" 
                 BorderThickness="0" Padding="0,0,30,0"/>
        <Image x:Name="AutoDetailsImage" Grid.Row="4" Grid.Column="1"
               HorizontalAlignment="Center" Margin="0,20"/>
    </Grid>
</Page>

Макет этой страницы базируется на элементе Grid, который включает текстовые поля и элемент ComboBox для отображения деталей о машине.

Добавьте в словарь ресурсов AutoAppStyles.xaml следующие стили, необходимые для этой страницы:

<!-- Стили для страницы AutoDetail.xaml -->
<Style x:Key="AutoDetailText" TargetType="TextBlock" BasedOn="{StaticResource TextBlockListItem}" >
        <Setter Property="FontSize" Value="35"/>
        <Setter Property="HorizontalAlignment" Value="Right"/>
</Style>

<Style x:Key="AutoDetailTextBox" TargetType="TextBox" >
        <Setter Property="FontSize" Value="30"/>
        <Setter Property="Margin" Value="10"/>
</Style>

<Style x:Key="AutoDetailComboBox" TargetType="ComboBox">
        <Setter Property="Foreground" Value="Black"/>
        <Setter Property="Height" Value="55"/>
        <Setter Property="FontSize" Value="30"/>
        <Setter Property="VerticalAlignment" Value="Top"/>
        <Setter Property="Margin" Value="10"/>
</Style>

Теперь, когда у меня есть 2 страницы, я могу переключаться между ними, когда состояние моего представления измениться, в данном случае, когда пользователь выберет элемент в списке autoList. Для этого дополните конструктор класса ListItem следующим кодом:

public ListItem()
{
            viewModel = new ViewModel();

            this.InitializeComponent();
            this.DataContext = viewModel;
            AutoDetailFrame.Navigate(typeof(Pages.NoItemSelected));

            viewModel.PropertyChanged += (sender, args) =>
            {
                if (args.PropertyName == "SelectedItemIndex")
                {
                    if (viewModel.SelectedItemIndex == -1)
                    {
                        AutoDetailFrame.Navigate(typeof(NoItemSelected));
                    }
                    else
                    {
                        AutoDetailFrame.Navigate(typeof(AutoDetail), viewModel);
                    }
                }
            };
}

Этот код работает следующим образом: когда пользователь выбирает элемент списка, инициируется событие ListBox.SelectionChanged. Мы обрабатываем это событие в autoList_SelectionChanged изменяя свойство SelectedItemIndex модель-представления. При изменении свойства инициируется событие PropertyChanged (т.к. ViewModel реализует INotifyPropertyChanged). В приведенном выше коде мы обрабатываем это событие, используя лямбда-выражение. При этом проверяется значение свойства SelectedItemIndex, если оно равно -1 (соответствует значению ListBox.SelectedIndex когда ничего не выбрано), то мы отображаем страницу NoItemSelected, иначе, если пользователь выбрал элемент списка, мы отображаем другую страницу AutoDetail, с информацией о машине.

Заметьте, что я передал экземпляр объекта ViewModel в параметре метода Navigate для AutoDetail. Это позволит получить ссылку на этот объект на странице AutoDetail.xaml и использовать ее в качестве контекста данных.

Следующий код, в файле AutoDetail.xaml.cs, определяет логику для извлечения данных и отображения их на странице:

using System;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Media.Imaging;
using Windows.UI.Xaml.Navigation;
using AutoShop.Data;

namespace AutoShop.Pages
{
    public sealed partial class AutoDetail : Page
    {
        private ViewModel viewModel;

        public AutoDetail()
        {
            this.InitializeComponent();
        }

        protected override void OnNavigatedTo(NavigationEventArgs e)
        {
            viewModel = e.Parameter as ViewModel;
            this.DataContext = viewModel;

            viewModel.PropertyChanged += (sender, eventArgs) =>
            {
                if (eventArgs.PropertyName == "SelectedItemIndex")
                {
                    if (viewModel.SelectedItemIndex == -1)
                    {
                        SetItemDetail(null);
                    }
                    else
                    {
                        SetItemDetail(viewModel.AutoList[viewModel.SelectedItemIndex]);
                    }
                }

            };

            SetItemDetail(viewModel.AutoList[viewModel.SelectedItemIndex]);
        }

        private void SetItemDetail(AutoItem item)
        {
            AutoDetailName.Text = (item == null) ? "" : item.Name + " " + item.Model;
            AutoDetailCost.Text = (item == null) ? ""
                : item.Cost.ToString();
            AutoDetailsDescription.Text = (item == null) ? "" : item.Description;

            if (item != null)
            {
                AutoDetailCar.ItemsSource = viewModel.ModelList;
                AutoDetailCar.SelectedItem = item.Name;

                string uriToImage = this.BaseUri.ToString().Replace("Pages/AutoDetail.xaml","") 
                    + item.PictureSource;
                AutoDetailsImage.Source = new BitmapImage(new Uri(uriToImage));
            }
            else
            {
                AutoDetailCar.SelectedIndex = -1;
                AutoDetailsImage.Source = null;
            }
        }

        private void AutoDetailCar_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            viewModel.SelectedModelList = (string)(sender as ComboBox).SelectedItem;
        }
    }
}

Давайте подробно разберем этот код. Метод SetItemDetail является вспомогательным и подставляет данные из экземпляра класса AutoItem в элементы управления используемые на странице. Здесь выполняется проверка ссылки на объект AutoItem и соответствующим образом заполняются текстовые поля, элемент ComboBox и картинка.

В обработчике события навигации OnNavigatedTo (заметьте что не в конструкторе) извлекается объект ViewModel из параметра, передающегося при вызове окна (как раз для этого мы передали экземпляр ViewModel в вызове метода Navigate в ListItem.xaml). Далее задается контекст данных и инициируется обработчик события PropertyChanged, вызывающий метод SetItemDetail.

Также мы добавили обработчик события ComboBox.SelectionChanged. Это нам понадобится чуть позже, а сейчас можете запустить это приложение, чтобы убедиться что привязка работает:

Простое приложение Metro

Добавление обратного взаимодействия

Созданное приложение уже имеет неплохую функциональность, но здесь мы не рассмотрели возможность обратного взаимодействия между главной и вспомогательной страницами. Сейчас, когда пользователь выберет элемент в ListBox главной страницы ListItem.xaml, представление вспомогательной страницы AutoDetails.xaml автоматически обновиться. Это прямое взаимодействие. Давайте создадим обратное взаимодействие - когда пользователь выберет в ComboBox марку автомобиля, мы подсветим в ListBox автомобили с этой же маркой (например, если пользователь выберет Audi, будут подсвечены поля с Audi A3, Audi R8 и Audi TT).

В этом нам помогут селекторы стиля. Добавьте в папку Pages новый файл класса HighlightStyleSelector который будет реализовывать StyleSelector:

using System;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using AutoShop.Data;

namespace AutoShop.Pages
{
    public class HighlightStyleSelector : StyleSelector
    {
        public HighlightStyleSelector(Style defaultStyle, Style highlightStyle, string property)
        {
            DefaultStyle = defaultStyle;
            HighlightStyle = highlightStyle;
            PropertyValueHighlight = property;
        }

        public Style DefaultStyle
        {
            get;set;
        }

        public Style HighlightStyle
        {
            get;set;
        }

        public string PropertyValueHighlight
        {
            get;set;
        }

        protected override Style SelectStyleCore(object item, DependencyObject container)
        {
            AutoItem autoitem = (AutoItem)item;

            if (autoitem.Name == PropertyValueHighlight)
                return HighlightStyle;
            else
                return DefaultStyle;
        }
    }
}

Это простой селектор стиля, имеющий три свойства: DefaultStyle и HighlightStyle содержат ссылки на соответствующие стили, PropertyValueHighlight - значение, с которым мы будем сравнивать марку автомобиля. SelectStyleCore - переопределенный метод StyleSelector, возвращающий стиль, в зависимости от условия.

Добавим в словарь ресурсов стиль для подсветки элемента:

<!-- Селектор стиля -->
<Style x:Key="AutoListitemHighlight" TargetType="ListBoxItem">
        <Setter Property="Background" Value="#0dd09b"/>
</Style>

Теперь видоизменим обработчик события PropertyChanged в конструкторе класса ListItem:

public ListItem()
{
            viewModel = new ViewModel();

            this.InitializeComponent();
            this.DataContext = viewModel;
            AutoDetailFrame.Navigate(typeof(Pages.NoItemSelected));

            viewModel.PropertyChanged += (sender, args) =>
            {
                if (args.PropertyName == "SelectedItemIndex")
                {
                    if (viewModel.SelectedItemIndex == -1)
                    {
                        AutoDetailFrame.Navigate(typeof(NoItemSelected));
                    }
                    else
                    {
                        AutoDetailFrame.Navigate(typeof(AutoDetail), viewModel);
                    }
                }

                if (args.PropertyName == "SelectedModelList")
                {
                    Style style = (Style)Application.Current.Resources["AutoListitemHighlight"];

                    autoList.ItemContainerStyleSelector =
                        new HighlightStyleSelector(null, style, viewModel.SelectedModelList);
                }
            };
}

Добавленный код отслеживает изменение свойства SelectedModelList в модель-представлении (фактически это и является обратным взаимодействием). В ответ на это событие, для элемента ListBox в качестве свойства ItemContainerStyleSelector передается новый объект HighlightStyleSelector. Изменение свойства SelectedModelList работает благодаря обработчику AutoDetailCar_SelectionChanged, который мы добавили ранее.

Если теперь вы запустите это приложение, то увидите подсветку машин одинаковых марок:

Обратное взаимодействие в Metro

Экран заставки и логотип

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

Стандартный логотип приложения Metro
Стандартный экран заставки приложения Metro

Изменить эти детали достаточно просто, нужно заменить стандартные рисунки логотипов и заставки, по умолчанию находящиеся в папке Assets приложения. При этом все рисунки должны быть в формате .png, заставка (файл SplashScreen.png) должна иметь разрешение 620х300, логотип имеет три различных вида - Logo.png (150x150), StoreLogo (50x50) и SmallLogo (30x30). Ниже показан пример, как могут выглядеть эти детали в готовом приложении:

Измененный логотип приложения Metro
Измененный экран заставки приложения Metro

Для создания экрана заставки можно пойти дальше и создать собственную заставку, имеющую другой цвет фона, анимацию и т.д. Более подробно об этом написано в статье - How to extend the splash screen (Windows Store apps using C#/VB/C++ and XAML).

Пройди тесты
Лучший чат для C# программистов