Прочитав эту главу, вы научитесь:
• объяснять, как шаблон Model — View — ViewModel (модель — представление — модель представления) используется для реализации логики приложения универсальной платформы Windows;
• использовать в представлении привязку данных для их отображения и изменения;
• создавать модель представления (ViewModel), с помощью которой представление сможет взаимодействовать с моделью.
В главе 25 «Реализация пользовательского интерфейса для приложений универсальной платформы Windows» были продемонстрированы способы конструирования пользовательского интерфейса, способного адаптироваться под различные форм-факторы, варианты ориентации и области просмотра устройств, которые может использовать клиент, запускающий ваше приложение. Учебное приложение, разработанное в этой главе, является одним из простейших приложений, сконструированных для отображения и редактирования данных о клиентах.
Далее вы увидите, как отобразить данные в пользовательском интерфейсе, и узнаете о характерных особенностях Windows 10, используя которые можно вести поиск данных в приложении. В ходе выполнения этих задач вы также изучите способы, которыми можно воспользоваться для структурирования UWP-приложения. В этой главе охвачено множество основных вопросов. В частности, вы увидите, как используется привязка данных для связи пользовательского интерфейса с отображаемыми им данными и как создается модель представления (ViewModel), предназначенная для отделения логики пользовательского интерфейса от модели данных и бизнес-логики приложения.
В графических приложениях, имеющих четко выраженную структуру, конструкция пользовательского интерфейса отделена от данных, используемых приложением, и от бизнес-логики, охватывающей функциональные возможности приложения. Это помогает устранить зависимости между различными компонентами, позволяет иметь различные представления данных, не испытывая при этом необходимости изменять бизнес-логику или положенную в основу модель данных. Этот подход также открывает пути конструирования и реализации различных элементов специалистами узкого профиля. Например, художник-график может сконцентрироваться на разработке привлекательного и интуитивно понятного пользовательского интерфейса, специалист по базам данных — сосредоточиться на реализации оптимизированного набора структур данных для хранения данных и доступа к ним, а разработчик, владеющий C#, — направить свои усилия на реализацию бизнес-логики приложения. Эта же общая цель ставится при разработке многих программных средств, не только UWP-приложений, и на протяжении последних нескольких лет для построения структуры приложения именно в таком плане было придумано множество технологических приемов.
Пожалуй, наиболее популярным подходом является следование шаблону разработки Model — View — ViewModel (MVVM). В этом шаблоне модель (Model) предоставляет данные, используемые приложением, а представление (View) задает способ отображения данных в пользовательском интерфейсе. Модель представления (ViewModel) содержит логику, которая связывает два первых компонента, принимая пользовательский ввод и превращая его в команды, выполняющие бизнес-операции в отношении модели, а также забирая данные из модели и форматируя их ожидаемым представлением образом. На следующей схеме (рис. 26.1) показаны упрощенные взаимоотношения между элементами шаблона MVVM. Обратите внимание на то, что приложение может предоставлять несколько представлений одних и тех же данных. Например, в UWP-приложении можно реализовать несколько состояний представлений, способных предоставлять информацию, используя различные разметки экрана. Одной из задач модели представления является обеспечение возможности отображения данных одной и той же модели и работы с ними с использованием множества различных представлений. Для связи с данными, предоставляемыми моделью представления, в представлении UWP-приложения может использоваться привязка данных. Кроме того, используя вызов команд, реализуемых моделью представления, представление может запросить у модели представления обновление данных в модели или выполнение бизнес-задач.
Рис. 26.1
Перед тем как вы приступите к реализации модели представления для приложения Customers, будет полезно более тщательно разобраться в привязке данных и в способе применения этой технологии для отображения данных в пользовательском интерфейсе. Используя привязку данных, можно связать свойство элемента управления со свойством объекта: если значение указанного свойства объекта изменяется, то изменяется и свойство связанного с объектом элемента управления. Кроме того, привязка данных может быть двунаправленной: если изменяется значение свойства в элементе управления, использующем привязку данных, изменение распространяется и на объект, к которому привязан этот элемент управления. В следующем упражнении дается краткое введение в порядок использования привязки данных для отображения данных. Это упражнение основано на приложении Customers из главы 25.
Откройте в среде Visual Studio 2015 проект Customers, который находится в папке \Microsoft Press\VCSBS\Chapter 26\Data Binding вашей папки документов. Это версия приложения Customers, которая была разработана в главе 25, но разметка пользовательского интерфейса немного изменена — чтобы элементы управления стали заметнее, они отображаются на синем фоне.
В обозревателе решений щелкните правой кнопкой мыши на проекте Customers, укажите на пункт Добавить, а затем щелкните на пункте Класс. Убедитесь в том, что в диалоговом окне Добавить новый элемент — Customers выбран шаблон Класс. Наберите в поле Имя строку Customer.cs, а затем щелкните на кнопке Добавить.
ПРИМЕЧАНИЕ Синий фон был создан путем использования элемента управления Rectangle (примоугольник), охватывающего те же строки и столбцы, что и элементы управления TextBlock и TextBox, в которых отображаются заголовки и данные. Прямоугольник закрашивается с помощью кисти LinearGradientBrush, которая постепенно меняет его цвет от синего средней насыщенности вверху до темно-синего внизу. XAML-разметка для элемента управления Rectangle, который отображается в Grid-элементе по имени customersTabularView, выглядит следующим образом (XAML-разметка для Grid-элемента customersColumnarView Grid включает такой же элемент управления Rectangle, охватывающий строки и столбцы, используемые этой разметкой):
<Rectangle Grid.Row="0" Grid.RowSpan="6" Grid.Column="1"
Grid.ColumnSpan="7" ...>
<Rectangle.Fill>
<LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
<GradientStop Color="#FF0E3895"/>
<GradientStop Color="#FF141415" Offset="0.929"/>
</LinearGradientBrush>
</Rectangle.Fill>
</Rectangle>
Этот класс будет использоваться для реализации типа данных Customer и привязки данных к отображению сведений, содержащихся в Customer-объектах, в пользовательском интерфейсе.
Выведите в окно редактора файл Customer.cs, сделайте класс Customer открытым и добавьте к нему следующие закрытые поля и свойства, выделенные жирным шрифтом:
public class Customer
{
public int _customerID;
public int CustomerID
{
get { return this._customerID; }
set { this._customerID = value; }
}
public string _title;
public string Title
{
get { return this._title; }
set { this._title = value; }
}
public string _firstName;
public string FirstName
{
get { return this._firstName; }
set { this._firstName = value; }
}
public string _lastName;
public string LastName
{
get { return this._lastName; }
set { this._lastName = value; }
}
public string _emailAddress;
public string EmailAddress
{
get { return this._emailAddress; }
set { this._emailAddress = value; }
}
public string _phone;
public string Phone
{
get { return this._phone; }
set { this._phone = value; }
}
}
Вы можете удивиться, почему эти свойства не реализованы в качестве автоматических, учитывая то, что они всего лишь извлекают и устанавливают значение в закрытом поле. Дело в том, что в выполняемом далее упражнении к этим свойствам будет добавляться дополнительный код.
В обозревателе решений в проекте Customers дважды щелкните на файле MainPage.xaml, чтобы в окне конструктора отобразился пользовательский интерфейс приложения. Найдите в XAML-панели разметку для элемента управления типа TextBox по имени id. Замените XAML-разметку, которая устанавливает свойство Text для этого элемента управления, следующей разметкой, показанной далее жирным шрифтом:
<TextBox Grid.Row="1" Grid.Column="1" x:Name="id" ...
Text="{Binding CustomerID}" .../>
Синтаксис Text="{Binding Path}" указывает на то, что значение свойства Text будет предоставлено значением выражения Path (путь) в ходе выполнения приложения. В данном случае для выражения Path установлено значение CustomerID, поэтому этим элементом управления будет отображено значение, хранящееся в выражении CustomerID. Но чтобы указать, что CustomerID фактически является свойством объекта Customer, вам следует предоставить дополнительную информацию. Для этого нужно установить значение для свойства DataContext элемента управления, что вам вскоре и предстоит сделать.
Добавьте в форме к каждому второму элементу ввода текста следующие выражения привязки. Примените привязку данных, показанную в следующем коде жирным шрифтом, к элементам управления типа TextBox в Grid-элементах customersTabularView и customersColumnarView. (Элементы управления типа ComboBox требуют несколько другого обращения, которое будет рассмотрено далее в разделе «Использование привязки данных к элементам управления типа ComboBox».)
<Grid x:Name="customersTabularView" ...>
...
<TextBox Grid.Row="1" Grid.Column="1" x:Name="id" ...
Text="{Binding CustomerID}" .../>
...
<TextBox Grid.Row="1" Grid.Column="5" x:Name="firstName" ...
Text="{Binding FirstName}" .../>
<TextBox Grid.Row="1" Grid.Column="7" x:Name="lastName" ...
Text="{Binding LastName}" .../>
...
<TextBox Grid.Row="3" Grid.Column="3" Grid.ColumnSpan="3"
x:Name="email" ... Text="{Binding EmailAddress}" .../>
...
<TextBox Grid.Row="5" Grid.Column="3" Grid.ColumnSpan="3"
x:Name="phone" ... Text="{Binding Phone}" .../>
</Grid>
<Grid x:Name="customersColumnarView" Margin="10,20,10,20"
Visibility="Collapsed">
...
<TextBox Grid.Row="0" Grid.Column="1" x:Name="cId" ...
Text="{Binding CustomerID}" .../>
...
<TextBox Grid.Row="2" Grid.Column="1" x:Name="cFirstName" ...
Text="{Binding FirstName}" .../>
<TextBox Grid.Row="3" Grid.Column="1" x:Name="cLastName" ...
Text="{Binding LastName}" .../>
...
<TextBox Grid.Row="4" Grid.Column="1" x:Name="cEmail" ...
Text="{Binding EmailAddress}" .../>
...
<TextBox Grid.Row="5" Grid.Column="1" x:Name="cPhone" ...
Text="{Binding Phone}" .../>
</Grid>
Обратите внимание на то, что одни и те же выражения привязки могут использоваться с более чем одним элементом управления. Например, на выражение {Binding CustomerID} имеются ссылки в элементах управления типа TextBox с именами id и cId, что заставляет оба элемента управления отображать одни и те же данные.
В обозревателе решений откройте файл MainPage.xaml, а затем двойным щелчком откройте в окне редактора файл MainPage.xaml.cs с кодом для формы MainPage.xaml. Добавьте в конструктор MainPage инструкцию, выделенную далее жирным шрифтом.
public MainPage()
{
this.InitializeComponent();
Customer customer = new Customer
{
CustomerID = 1,
Title = "Mr",
FirstName = "John",
LastName = "Sharp",
EmailAddress = "",
Phone = "111-1111"
};
}
Этот код создает новый экземпляр класса Customer и наполняет его данными, взятыми для примера. После кода, создающего новый объект Customer, добавьте следующую инструкцию, выделенную жирным шрифтом:
Customer customer = new Customer
{
...
};
this.DataContext = customer;
Эта инструкция указывает объект, к которому должны быть привязаны элементы управления формы MainPage. XAML-разметка Text="{Binding Path}" в каждом элементе управления будет разрешена в отношении этого объекта. Например, как для элемента управления типа TextBox с именем id, так и для элемента управления типа TextBox с именем cId указывается атрибут Text="{Binding CustomerID}", следовательно, они будут отображать значение, найденное в свойстве CustomerID объекта типа Customer, к которому привязана форма.
ПРИМЕЧАНИЕ В данном примере вами было установлено значение для имеющегося у формы свойства DataContext, поэтому одна и та же привязка данных автоматически применяется ко всем элементам управления формы. Если нужно привязать конкретные элементы управления к разным объектам, можно также установить значения для свойств DataContext, принадлежащих отдельным элементам управления.
В меню Отладка щелкните на пункте Начать отладку, чтобы выполнить сборку и запуск приложения. Убедитесь в том, что форма заняла весь экран и, как показано на рис. 26.2, отобразила данные клиента John Sharp.
Измените размер окна, чтобы приложение отобразилось в более узкой области просмотра. Убедитесь в том, что отображаются те же данные, которые показаны на рис. 26.3.
Элементы управления, отображаемые в узкой области просмотра, привязаны к тем же данным, что и элементы управления, отображаемые в полноэкранной области просмотра.
Измените в узкой области просмотра адрес электронной почты (Email) на . Расширьте окно приложения, чтобы произошло переключение
Рис. 26.2
к представлению в широкой области просмотра. Заметьте, что адрес электронной почты, отображаемый в этом представлении, не изменился.
Вернитесь в среду Visual Studio и остановите отладку.
Выведите в окно редактора среды Visual Studio код класса Customer и установите контрольную точку на методе доступа set для свойства EmailAddress.
Щелкните в меню Отладка на пункте Начать отладку для повторной сборки и запуска приложения.
Когда отладчик дойдет до контрольной точки первый раз, нажмите клавишу F5, чтобы продолжить выполнение приложения.
Когда появится пользовательский интерфейс для приложения Customers, измените размер окна приложения, чтобы отобразилось представление для узкой области просмотра, и измените адрес электронной почты на .
Снова увеличьте окно приложения, чтобы вернуться к представлению для широкой области просмотра.
Заметьте, что отладчик не дойдет до контрольной точки на строке с методом доступа set для свойства EmailAddress: когда элемент управления типа TextBox по имени email теряет фокус, обновленное значение не записывается обратно в объект типа Customer.
Рис. 26.3
Вернитесь в среду Visual Studio и остановите отладку.
Удалите контрольную точку из строки с методом доступа set свойства EmailAddress в классе Customer.
В предыдущем упражнении было показано, насколько просто за счет использования привязки данных, имеющихся в объекте, эти данные можно отобразить в форме приложения. Но привязка данных по умолчанию является однонаправленной операцией, и любые изменения, вносимые в отображаемые данные, обратно в источник данных не копируются. В упражнении это было заметно при изменении адреса электронной почты в представлении для узкой области просмотра: когда произошло переключение на представление для широкой области просмотра, данные там не изменились. Реализовать двунаправленную привязку данных можно изменением в XAML-разметке элемента управления параметра Mode, принадлежащего спецификации Binding. Этот параметр показывает, какой именно является привязка данных, одно- или двунаправленной. Его изменением вы сейчас и займетесь.
Выведите в окно редактора файл MainPage.xaml и измените, как показано в следующем примере кода жирным шрифтом, XAML-разметку для каждого элемента управления типа TextBox:
<Grid x:Name="customersTabularView" ...>
...
<TextBox Grid.Row="1" Grid.Column="1" x:Name="id" ...
Text="{Binding CustomerID, Mode=TwoWay}" .../>
...
<TextBox Grid.Row="1" Grid.Column="5" x:Name="firstName" ...
Text="{Binding FirstName, Mode=TwoWay}" .../>
<TextBox Grid.Row="1" Grid.Column="7" x:Name="lastName" ...
Text="{Binding LastName, Mode=TwoWay}" .../>
...
<TextBox Grid.Row="3" Grid.Column="3" Grid.ColumnSpan="3"
x:Name="email" ... Text="{Binding EmailAddress, Mode=TwoWay}" .../>
...
<TextBox Grid.Row="5" Grid.Column="3" Grid.ColumnSpan="3"
x:Name="phone" ... Text="{Binding Phone, Mode=TwoWay}" ..."/>
</Grid>
<Grid x:Name="customersColumnarView" Margin="10,20,10,20" ...>
...
<TextBox Grid.Row="0" Grid.Column="1" x:Name="cId" ...
Text="{Binding CustomerID, Mode=TwoWay}" .../>
...
<TextBox Grid.Row="2" Grid.Column="1" x:Name="cFirstName" ...
Text="{Binding FirstName, Mode=TwoWay}" .../>
<TextBox Grid.Row="3" Grid.Column="1" x:Name="cLastName" ...
Text="{Binding LastName, Mode=TwoWay}" .../>
...
<TextBox Grid.Row="4" Grid.Column="1" x:Name="cEmail" ...
Text="{Binding EmailAddress, Mode=TwoWay}" .../>
...
<TextBox Grid.Row="5" Grid.Column="1" x:Name="cPhone" ...
Text="{Binding Phone, Mode=TwoWay}" .../>
</Grid>
Параметр Mode в спецификации Binding показывает, какой является привязка данных, однонаправленной (эта установка используется по умолчанию) или двунаправленной. Установка для параметра Mode значения TwoWay заставляет любые изменения, произведенные пользователем, передавать назад объекту, к которому привязан элемент управления.
В меню Отладка щелкните на пункте Начать отладку, чтобы снова выполнить сборку и запуск приложения. При используемом для приложения представлении, предназначенном для широкой области просмотра, измените адрес электронной почты на , а затем измените размер окна, чтобы для отображения приложения использовалось представление, предназначенное для узкой области просмотра.
Обратите внимание на то, что, несмотря на изменения привязки данных на режим TwoWay, адрес электронной почты, отображаемый в представлении для узкой области просмотра, не обновился и остался прежним — .
Вернитесь в среду Visual Studio и остановите отладку.
Очевидно, что-то не сработало! Теперь проблема не в том, что не обновились данные, а в том, что представление не отобразило самую последнюю версию данных. (Если в классе Customer восстановить контрольную точку на методе доступа set свойства EmailAddress и запустить приложение в отладчике, вы увидите, что отладчик дойдет до контрольной точки при изменении значения адреса электронной почты и перемещении фокуса с элемента управления типа TextBox.) Несмотря на внешние признаки, процесс привязки данных не волшебный и привязка данных не получает информации об изменении этих данных. Объект должен проинформировать привязку данных о любых изменениях отправкой пользовательскому интерфейсу события PropertyChanged. Это событие является частью интерфейса INotifyPropertyChanged, и все объекты, поддерживающие двунаправленную привязку данных, должны иметь реализацию этого интерфейса. Его реализацией вы займетесь в следующем упражнении.
Выведите в окно редактора среды Visual Studio файл Customer.cs.
Добавьте к списку в начале файла следующую директиву using:
using System.ComponentModel;
В этом пространстве имен определен интерфейс INotifyPropertyChanged.
Измените определение класса Customer, чтобы указать на реализацию этим классом интерфейса INotifyPropertyChanged (изменение показано жирным шрифтом):
class Customer : INotifyPropertyChanged
Добавьте после свойства Phone в классе Customer событие PropertyChanged, выделенное в следующем примере жирным шрифтом:
class Customer : INotifyPropertyChanged
{
...
public string _phone;
public string Phone {
get { return this._phone; }
set { this._phone = value; }
}
public event PropertyChangedEventHandler PropertyChanged;
}
Это событие является единственным элементом, определяемым интерфейсом INotifyPropertyChanged. Все объекты, реализующие этот интерфейс, должны предоставлять это событие и инициировать его, как только им потребуется уведомить внешний мир об изменении значения свойства.
Добавьте к классу Customer после события PropertyChanged метод OnPropertyChanged, показанный далее жирным шрифтом:
class Customer : INotifyPropertyChanged
{
...
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
Метод OnPropertyChanged инициирует событие PropertyChanged. Параметр PropertyChangedEventArgs события PropertyChanged должен указывать имя измененного свойства. Это значение передается в качестве параметра методу OnPropertyChanged.
Измените в классе Customer методы доступа к свойству set для каждого свойства, чтобы в них вызывался метод OnPropertyChanged, как только содержащееся в них значение подвергнется изменению (все изменения выделены далее жирным шрифтом):
class Customer : INotifyPropertyChanged
{
public int _customerID;
public int CustomerID
{
get { return this._customerID; }
set
{
this._customerID = value;
this.OnPropertyChanged(nameof(CustomerID));
}
}
public string _title;
public string Title
{
get { return this._title; }
set
{
this._title = value;
this.OnPropertyChanged(nameof(Title));
}
}
public string _firstName;
public string FirstName
{
get { return this._firstName; }
set
{
this._firstName = value;
this.OnPropertyChanged(nameof(FirstName));
}
}
public string _lastName;
public string LastName
{
get { return this._lastName; }
set
{
this._lastName = value;
this.OnPropertyChanged(nameof(LastName));
}
}
public string _emailAddress;
public string EmailAddress
{
get { return this._emailAddress; }
set
{
this._emailAddress = value;
this.OnPropertyChanged(nameof(EmailAddress));
}
}
public string _phone;
public string Phone
{
get { return this._phone; }
set
{
this._phone = value;
this.OnPropertyChanged(nameof(Phone));
}
}
...
}
Оператор nameof
Оператор nameof, показанный в классе Customer, является редко используемым, но весьма полезным в таком коде, как этот, средством C#. Он возвращает имя переменной, переданной ему в качестве параметра, в виде строки. Без использования оператора nameof вам пришлось бы воспользоваться точными указаниями строковых значений, например:
public int CustomerID
{
get { return this._customerID; }
set
{
this._customerID = value;
this.OnPropertyChanged("CustomerID");
}
}
Хотя использование строковых значений позволяет набирать меньше текста, подумайте о том, что произойдет, если когда-либо в будущем вам потребуется изменить имя переменной. При использовании подхода, предполагающего использование строкового значения, вам придется вносить изменения еще и в это строковое значение. Если этого не сделать, код будет по-прежнему проходить компиляцию и запускаться, но любые изменения, внесенные в значение свойства в ходе выполнения приложения, не будут регистрироваться, что приведет к возникновению трудно отслеживаемых ошибок. Если при использовании оператора nameof вы измените имя свойства, но забудете изменить аргумент для оператора nameof, код не пройдет компиляцию и вы тут же получите предупреждение о наличии ошибки, которую можно будет легко и быстро устранить.
В меню Отладка щелкните на пункте Начать отладку, чтобы еще раз выполнить сборку и запуск приложения.
Когда появится форма Customers, измените адрес электронной почты на , а номер телефона — на 222-2222.
Измените размер окна, чтобы приложение отображалось в представлении для узкой области просмотра, и убедитесь в том, что адрес электронной почты и номер телефона изменились.
Измените имя на James, увеличьте размер окна, чтобы приложение отображалось в представлении для широкой области просмотра, и убедитесь в том, что имя изменилось.
Вернитесь в среду Visual Studio и остановите отладку.
Использование привязки данных к элементам управления типа TextBox или TextBlock особых вопросов не вызывает. А вот элементам управления типа ComboBox требуется уделить немного больше внимания. Дело в том, что элемент управления типа ComboBox фактически имеет две отображаемые категории: список значений в раскрывающемся списке, из которого пользователь может выбрать элемент, и значение текущего выбранного элемента. Если реализовать привязку данных для отображения списка элементов ComboBox, то значение, выбранное пользователем, должно входить в этот список. В приложении Customers вы можете настроить привязку данных для выбранного значения в элементе управления типа ComboBox по имени title, установив значение для свойства SelectedValue:
<ComboBox ... x:Name="title" ... SelectedValue="{Binding Title}" ... />
Но следует помнить, что список значений для раскрывающегося списка конкретно определен в XAML-разметке:
<ComboBox ... x:Name="title" ... >
<ComboBoxItem Content="Mr"/>
<ComboBoxItem Content="Mrs"/>
<ComboBoxItem Content="Ms"/>
<ComboBoxItem Content="Miss"/>
</ComboBox>
До создания элемента управления эта разметка не применяется, следовательно, значение, указанное в привязке данных, не будет найдено в списке, поскольку при построении привязки данных списка еще не существует. В результате этого значение не будет выведено на экран. Если хотите, можете проверить — настройте привязку для свойства SelectedValue, как только что было показано, и запустите приложение. Элемент управления title типа ComboBox при начальном отображении будет пуст, несмотря на то что обращение к пользователю было в форме Mr.
Эту проблему можно решить несколькими способами, но проще всего создать источник данных, содержащий список допустимых значений, а затем указать, что элемент управления типа ComboBox должен использовать этот список при установке своих значений для раскрывающегося списка. Кроме того, вам нужно сделать это до применения к элементу управления типа ComboBox привязки данных.
Откройте в окне редактора среды Visual Studio файл MainPage.xaml.cs. Добавьте к конструктору MainPage следующий код, выделенный жирным шрифтом:
public MainPage()
{
this.InitializeComponent();
List<string> titles = new List<string>
{
"Mr", "Mrs", "Ms", "Miss"
};
this.title.ItemsSource = titles;
this.cTitle.ItemsSource = titles;
Customer customer = new Customer
{
...
};
this.DataContext = customer;
}
Этот код создает список строковых значений, содержащий допустимые обращения, которые могут быть применены к клиентам. Затем код устанавливает в качестве значения свойства ItemsSource для обоих элементов управления типа ComboBox по имени title ссылку на этот список (вспомним, что элемент управления типа ComboBox имеется в каждом представлении).
ПРИМЕЧАНИЕ В коммерческом приложении список значений, отображаемых элементом управления типа ComboBox, будет извлекаться, скорее всего, из базы данных или какого-нибудь другого источника, а не из фиксированного списка, как показано в этом примере.
Важно определить для этого кода правильно место. Он должен запускаться перед инструкцией, устанавливающей свойство DataContext формы MainPage, поскольку эта инструкция привязывает данные к элементам управления формы.
Выведите в окно редактора файл MainPage.xaml. Измените XAML-разметку для элементов управления title и cTitle, относящихся к типу ComboBox (изменения выделены жирным шрифтом):
<Grid x:Name="customersTabularView" ...>
...
<ComboBox Grid.Row="1" Grid.Column="3" x:Name="title" ...
SelectedValue="{Binding Title, Mode=TwoWay}">
</ComboBox>
...
</Grid>
<Grid x:Name="customersColumnarView" ...>
...
<ComboBox Grid.Row="1" Grid.Column="1" x:Name="cTitle" ...
SelectedValue="{Binding Title, Mode=TwoWay}">
</ComboBox>
...
</Grid>
Заметьте, что список ComboBoxItem-элементов для каждого элемента управления был перемещен и свойство SelectedValue настроено на использование привязки данных к полю Title в объекте типа Customer.
В меню Отладка щелкните на пункте Начать отладку, чтобы выполнить сборку и запуск приложения.
Убедитесь в том, что значение обращения к клиенту отображено правильно (оно должно выглядеть как Mr). Щелкните на стрелке раскрытия списка в элементе типа ComboBox и убедитесь в том, что он содержит значения Mr, Mrs, Ms и Miss.
Измените размер окна, чтобы приложение выводилось в представлении для узкой области просмотра, и проконтролируйте наличие тех же самых данных. Заметьте, что вы можете изменить обращение и при переключении на представление для широкой области просмотра будет отображаться новая форма обращения.
Вернитесь в среду Visual Studio и остановите отладку.
Вы увидели, как настраивается привязка данных для связи источника данных с элементами управления в пользовательском интерфейсе, но используемый источник данных был очень простым, состоящим из данных всего лишь одного клиента. В реальном мире источники данных намного сложнее и состоят из коллекций различных типов объектов. Вспомним, что в понятиях MVVM источник данных зачастую предоставляется моделью, а пользовательский интерфейс (или представление) связывается с моделью только опосредованно, через объект модели представления (ViewModel). Рациональным зерном такого подхода является выполнение требования независимости модели и представления, отображающего данные, предоставляемые моделью: вам не нужно изменять модель в случае изменения пользовательского интерфейса и не требуется подстраивать пользовательский интерфейс при внесении изменений в базовую модель.
Связь между представлением и моделью обеспечивает модель представления, в которой реализуется также бизнес-логика приложения. Эта бизнес-логика должна быть независима от представления и модели. Модель представления предоставляет бизнес-логику представлению, реализуя коллекцию команд. Пользовательский интерфейс может инициировать эти команды, основываясь на способе перемещения пользователя по приложению. В следующем упражнении вы расширите приложение Customers за счет реализации модели, содержащей список Customer-объектов, и создания модели представления, предоставляющей команды, с помощью которых пользователь может осуществлять в представлении переходы между сведениями о клиентах.
Откройте проект Customers, который находится в папке \Microsoft Press\VCSBS\Chapter 26\ViewModel вашей папки документов. Он содержит полную версию приложения Customers из предыдущего набора упражнений, но если хотите, можете и дальше использовать собственную версию проекта.
В обозревателе решений щелкните правой кнопкой мыши на проекте Customers, укажите на пункт Добавить, а затем щелкните на пункте Класс.
Наберите в поле Имя диалогового окна Добавить новый элемент — Customers строку ViewModel.cs, а затем щелкните на кнопке Добавить.
Этот класс предоставляет основную модель представления (ViewModel), в которой содержится коллекция объектов типа Customer. Пользовательский интерфейс будет привязан к данным, предоставляемым этой моделью представления.
Пометьте в окне редактора, показывающем файл ViewModel.cs, класс открытым (public) и добавьте к классу ViewModel код, показанный в следующем примере жирным шрифтом:
public class ViewModel
{
private List<Customer> customers;
public ViewModel()
{
this.customers = new List<Customer>
{
new Customer
{
CustomerID = 1,
Title = "Mr",
FirstName="John",
LastName="Sharp",
",
Phone="111-1111"
},
new Customer
{
CustomerID = 2,
Title = "Mrs",
FirstName="Diana",
LastName="Sharp",
",
Phone="111-1112"
},
new Customer
{
CustomerID = 3,
Title = "Ms",
FirstName="Francesca",
LastName="Sharp",
",
Phone="111-1113"
}
};
}
}
Класс ViewModel использует в качестве своей модели объект типа List<Customer>, и конструктор заполняет этот список образцом данных. Строго говоря, эти данные должны храниться в отдельном классе Model, но, преследуя цели именно этого упражнения, мы будем работать с образцом данных.
Добавьте к классу ViewModel закрытую переменную currentCustomer, показанную в следующем примере кода жирным шрифтом, и инициализируйте ее в конструкторе нулевым значением:
class ViewModel
{
private List<Customer> customers;
private int currentCustomer;
public ViewModel()
{
this.currentCustomer = 0;
this.customers = new List<Customer>
{
...
}
}
}
Класс ViewModel будет использовать эту переменную для отслеживания того объекта типа Customer, который в данный момент выводится на экран.
Добавьте к классу ViewModel сразу после конструктора свойство Current, выделенное в следующем примере кода жирным шрифтом:
class ViewModel
{
...
public ViewModel()
{
...
}
public Customer Current
{
get { return this.customers.Count > 0 ?
this.customers[currentCustomer] : null; }
}
}
Свойство Current предоставляет в модели доступ к текущему Customer-объекту. Если клиенты отсутствуют, оно возвращает объект со значением null.
ПРИМЕЧАНИЕ Предоставление управляемого доступа к модели данных считается устоявшейся практикой — возможность внесения изменений в модель должна быть только у ViewModel. Но это ограничение не препятствует тому, чтобы представление могло обновлять данные, предоставляемые ViewModel, — оно просто не может переключить модель и заставить ее ссылаться на другой источник данных.
В окне редактора откройте файл MainPage.xaml.cs. Удалите из конструктора MainPage код, создающий объект типа Customer, заменив его инструкцией, создающей экземпляр класса ViewModel. Измените инструкцию, устанавливающую значение для свойства DataContext объекта типа MainPage, указав ссылку на новый объект типа ViewModel (изменения выделены жирным шрифтом):
public MainPage()
{
...
this.cTitle.ItemsSource = titles;
ViewModel viewModel = new ViewModel();
this.DataContext = viewModel;
}
В окне конструктора откройте файл MainPage.xaml. Измените в панели XAML привязки данных для элементов управления типа TextBox и ComboBox, чтобы в них были ссылки на свойства, задаваемые посредством объекта Current, предоставляемого моделью представления (изменения показаны жирным шрифтом):
<Grid x:Name="customersTabularView" ...>
...
<TextBox Grid.Row="1" Grid.Column="1" x:Name="id" ...
Text="{Binding Current.CustomerID, Mode=TwoWay}" .../>
<TextBox Grid.Row="1" Grid.Column="5" x:Name="firstName" ...
Text="{Binding Current.FirstName, Mode=TwoWay }" .../>
<TextBox Grid.Row="1" Grid.Column="7" x:Name="lastName" ...
Text="{Binding Current.LastName, Mode=TwoWay }" .../>
<ComboBox Grid.Row="1" Grid.Column="3" x:Name="title" ...
SelectedValue="{Binding Current.Title, Mode=TwoWay}">
</ComboBox>
...
<TextBox Grid.Row="3" Grid.Column="3" ... x:Name="email" ...
Text="{Binding Current.EmailAddress, Mode=TwoWay }" .../>
...
<TextBox Grid.Row="5" Grid.Column="3" ... x:Name="phone" ...
Text="{Binding Current.Phone, Mode=TwoWay }" .../>
</Grid>
<Grid x:Name="customersColumnarView" Margin="20,10,20,110" ...>
...
<TextBox Grid.Row="0" Grid.Column="1" x:Name="cId" ...
Text="{Binding Current.CustomerID, Mode=TwoWay }" .../>
<TextBox Grid.Row="2" Grid.Column="1" x:Name="cFirstName" ...
Text="{Binding Current.FirstName, Mode=TwoWay }" .../>
<TextBox Grid.Row="3" Grid.Column="1" x:Name="cLastName" ...
Text="{Binding Current.LastName, Mode=TwoWay }" .../>
<ComboBox Grid.Row="1" Grid.Column="1" x:Name="cTitle" ...
SelectedValue="{Binding Current.Title, Mode=TwoWay}">
</ComboBox>
...
<TextBox Grid.Row="4" Grid.Column="1" x:Name="cEmail" ...
Text="{Binding Current.EmailAddress, Mode=TwoWay }" .../>
...
<TextBox Grid.Row="5" Grid.Column="1" x:Name="cPhone" ...
Text="{Binding Current.Phone, Mode=TwoWay }" .../>
</Grid>
В меню Отладка щелкните на пункте Начать отладку, чтобы выполнить сборку и запуск приложения. Убедитесь в том, что приложение отображает информацию о клиенте John Sharp (о первом клиенте в списке).
Измените информацию о клиенте и переключитесь на другое представление, чтобы удостовериться, что привязка данных по-прежнему работает должным образом.
Вернитесь в среду Visual Studio и остановите отладку.
Модель представления предоставляет доступ к клиентской информации через свойство Current, но на данный момент она не обеспечивает какого-либо способа перехода от информации об одном клиенте к информации о другом клиенте. Вы можете создать методы, уменьшающие и увеличивающие значение переменной currentCustomer на единицу, чтобы в свойство Current извлекалась информация о различных клиентах, но это нужно сделать так, чтобы не привязывать представление (View) к модели представления (ViewModel). Чаще всего для этой цели применяется технология, использующая шаблон Command. В этом шаблоне модель представления предоставляет методы в форме команд, которые могут вызываться представлением. Замысел заключается в том, чтобы обойтись в коде представления без явной ссылки на эти методы по именам. Чтобы добиться этой цели, XAML позволяет декларативно связывать команды с действиями, инициируемыми элементами управления в пользовательском интерфейсе, что и будет показано в упражнениях следующего раздела.
XAML-разметка, привязывающая действие в отношении элемента управления к команде, требует, чтобы команды, предоставляемые моделью представления, реализовывали интерфейс ICommand. Этот интерфейс определяет следующие элементы.
• CanExecute. Этот метод возвращает булево значение, показывающее, может ли команда быть запущена. Используя этот метод, модель представления может в зависимости от контекста включить или выключить команду. Например, команда, которая извлекает из списка данные о следующем клиенте, должна включаться для запуска, только если есть следующий клиент, если же клиентов больше нет, команда должна быть выключена.
• Execute. Этот метод запускается при вызове команды.
• CanExecuteChanged. Это событие инициируется, когда состояние модели представления изменяется. При таких обстоятельствах те команды, которые до этого могли запускаться, теперь могут быть выключены, и наоборот. Например, если пользовательский интерфейс вызывает команду, извлекающую из списка данные о следующем клиенте, и если этот клиент является последним, то следующие вызовы метода CanExecute должны возвращать false. При таких обстоятельствах должно инициироваться событие CanExecuteChanged, показывающее, что команда была выключена.
В следующем упражнении будет создан класс-обобщение, реализующий интерфейс ICommand.
В среде Visual Studio щелкните правой кнопкой мыши на проекте Customers, укажите на пункт Добавить и щелкните на пункте Класс.
В диалоговом окне Добавить новый элемент — Customers выберите шаблон Класс. В поле Имя наберите строку Command.cs, а затем щелкните на кнопке Добавить.
В окне редактора, показывающего код файла Command.cs, добавьте к списку в начале файла следующую директиву using:
using System.Windows.Input;
В этом пространстве имен определен интерфейс ICommand.
Объявите класс Command открытым и укажите, что в нем реализуется интерфейс ICommand, добавив к его объявлению следующие элементы, показанные жирным шрифтом:
public class Command : ICommand
{
}
Добавьте к классу Command следующие закрытые поля:
public class Command : ICommand
{
private Action methodToExecute = null;
private Func<bool> methodToDetectCanExecute = null;
}
Типы Action и Func были вкратце рассмотрены в главе 20 «Отделение логики приложения и обработка событий». Тип Action является делегатом, которым можно пользоваться для ссылки на метод, не принимающий параметры и не возвращающий значение, тип Func<T> также является делегатом, который может ссылаться на метод, не принимающий параметры, но возвращающий значение того типа, который указан параметром типа T. В этом классе для ссылки на код, который Command-объект будет запускать после его вызова из представления, вы будете использовать поле methodToExecute. Поле methodToDetectCanExecute будет использоваться для ссылки на метод, который обнаруживает возможность запуска команды (она может быть выключена по каким-либо причинам, зависящим от состояния приложения или данных).
Добавьте к классу Command конструктор, который будет принимать два параметра: Action-объект и Func<T>-объект. Как показано далее жирным шрифтом, значения этих параметров нужно присвоить полям methodToExecute и methodToDetectCanExecute:
public Command : ICommand
{
...
public Command(Action methodToExecute, Func<bool> methodToDetectCanExecute)
{
this.methodToExecute = methodToExecute;
this.methodToDetectCanExecute = methodToDetectCanExecute;
}
}
Модель представления создаст экземпляр этого класса для каждой команды. При вызове конструктора модель представления предоставит метод для запуска команды и метод для обнаружения того, следует ли включить команду.
Реализуйте методы Execute и CanExecute класса Command, воспользовавшись методами, на которые ссылаются поля methodToExecute и methodToDetectCanExecute:
public Command : ICommand
{
...
public Command(Action methodToExecute,
Func<bool> methodToDetectCanExecute)
{
...
}
public void Execute(object parameter)
{
this.methodToExecute();
}
public bool CanExecute(object parameter)
{
if (this.methodToDetectCanExecute == null)
{
return true;
}
else
{
return this.methodToDetectCanExecute();
}
}
}
Заметьте, что если модель представления дает для параметра конструктора methodToDetectCanExecute ссылку null, то по умолчанию предполагается, что команда может быть запущена, и метод CanExecute возвращает true.
Добавьте к классу Command событие CanExecuteChanged:
public Command : ICommand
{
...
public bool CanExecute(object parameter)
{
...
}
public event EventHandler CanExecuteChanged;
}
Когда команда привязывается к элементу управления, он автоматически подписывается на это событие, которое будет инициировано Command-объектом, если состояние модели представления обновлено и значение, возвращенное методом CanExecute, изменяется. Самой простой стратегией будет использование таймера для инициирования события CanExecuteChanged примерно раз в секунду. Тогда элемент управления может вызвать CanExecute, чтобы определить, может ли команда по-прежнему быть выполнена, и предпринять действия по своему включению или выключению в зависимости от результата.
Добавьте к списку в начале файла следующую директиву using:
using Windows.UI.Xaml;
Добавьте к классу Command выше конструктора следующее поле, выделенное жирным шрифтом:
public class Command : ICommand
{
...
private Func<bool> methodToDetectCanExecute = null;
private DispatcherTimer canExecuteChangedEventTimer = null;
public Command(Action methodToExecute,
Func<bool> methodToDetectCanExecute)
{
...
}
}
Класс DispatcherTimer, определение которого находится в пространстве имен Windows.UI.Xaml, реализует таймер, который может инициировать событие через указанные интервалы времени. Для инициирования события CanExecuteChanged через односекундные интервалы вы будете использовать поле canExecuteChangedEventTimer.
Добавьте к классу Command метод canExecuteChangedEventTimer_Tick, показанный в следующем примере кода жирным шрифтом:
public class Command : ICommand
{
...
public event EventHandler CanExecuteChanged;
void canExecuteChangedEventTimer_Tick(object sender, object e)
{
if (this.CanExecuteChanged != null)
{
this.CanExecuteChanged(this, EventArgs.Empty);
}
}
}
Этот метод просто инициирует событие CanExecuteChanged, если как минимум один элемент управления привязан к команде. Строго говоря, этот метод перед инициированием события должен также проверять, не изменилось ли состояние объекта. Но чтобы свести к минимуму издержки от неэффективности работы кода, связанной с отказом от проверки состояния, вы установите интервал таймера на достаточно продолжительный (в понятиях обработки данных) период.
Добавьте к конструктору Command следующие инструкции, выделенные жирным шрифтом:
public class Command : ICommand
{
...
public Command(Action methodToExecute, Func<bool> methodToDetectCanExecute)
{
this.methodToExecute = methodToExecute;
this.methodToDetectCanExecute = methodToDetectCanExecute;
this.canExecuteChangedEventTimer = new DispatcherTimer();
this.canExecuteChangedEventTimer.Tick +=
canExecuteChangedEventTimer_Tick;
this.canExecuteChangedEventTimer.Interval = new TimeSpan(0, 0, 1);
this.canExecuteChangedEventTimer.Start();
}
...
}
Этот код инициирует DispatcherTimer-объект и устанавливает перед запуском таймера значение его интервала на одну секунду.
В меню Сборка щелкните на пункте Собрать решение и убедитесь в том, что сборка приложения прошла без ошибок.
Теперь вы можете воспользоваться классом Command для добавления команд к классу ViewModel. В следующем упражнении вы определите команды, позволяющие пользователю переходить в представлении между сведениями о разных клиентах.
В окне редактора среды Visual Studio откройте файл ViewModel.cs. Добавьте к началу файла следующую директиву using и измените определение класса ViewModel, указав, что в нем реализуется интерфейс INotifyPropertyChanged:
...
using System.ComponentModel;
namespace Customers
{
public class ViewModel : INotifyPropertyChanged
{
...
}
}
Добавьте к концу класса ViewModel событие PropertyChanged и метод OnPropertyChanged. Это точно такой же код, который был включен вами в класс Customer:
public class ViewModel : INotifyPropertyChanged
{
...
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
Не забудьте, что представление для различных входящих в него элементов управления ссылается на данные в выражениях привязки данных посредством свойства Current. Когда класс ViewModel переходит к данным другого клиента, он должен инициировать событие PropertyChanged, чтобы уведомить представление о том, что отображаемые данные были изменены.
Добавьте к классу ViewModel сразу же после конструктора следующие поля и свойства:
public class ViewModel : INotifyPropertyChanged
{
...
public ViewModel()
{
...
}
private bool _isAtStart;
public bool IsAtStart
{
get { return this._isAtStart; }
set
{
this._isAtStart = value;
this.OnPropertyChanged(nameof(IsAtStart));
}
}
private bool _isAtEnd;
public bool IsAtEnd
{
get { return this._isAtEnd; }
set
{
this._isAtEnd = value;
this.OnPropertyChanged(nameof(IsAtEnd));
}
}
...
}
Эти два свойства будут использоваться вами для отслеживания состояния ViewModel. Свойство IsAtStart будет установлено в true, когда поле currentCustomer в ViewModel позиционировано на начало коллекции клиентов, а поле IsAtEnd будет установлено в true, когда это поле позиционировано на конец коллекции клиентов.
Внесите в конструктор изменения, показанные жирным шрифтом, для установки значений свойств IsAtStart и IsAtEnd:
public ViewModel()
{
this.currentCustomer = 0;
this.IsAtStart = true;
this.IsAtEnd = false;
this.customers = new List<Customer>
...
}
Добавьте к классу ViewModel после свойства Current закрытые методы Next и Previous, выделенные жирным шрифтом:
public class ViewModel : INotifyPropertyChanged
{
...
public Customer Current
{
...
}
private void Next()
{
if (this.customers.Count - 1 > this.currentCustomer)
{
this.currentCustomer++;
this.OnPropertyChanged(nameof(Current));
this.IsAtStart = false;
this.IsAtEnd =
(this.customers.Count - 1 == this.currentCustomer);
}
}
private void Previous()
{
if (this.currentCustomer > 0)
{
this.currentCustomer--;
this.OnPropertyChanged(nameof(Current));
this.IsAtEnd = false;
this.IsAtStart = (this.currentCustomer == 0);
}
}
...
}
ПРИМЕЧАНИЕ Свойство Count возвращает количество элементов в коллекции, но не забудьте, что нумерация элементов коллекции идет от 0 до Count – 1.
Эти методы обновляют значение переменной currentCustomer для ссылки на следующего (или предыдущего) клиента в списке клиентов. Заметьте, что эти методы предоставляют значения для свойств IsAtStart и IsAtEnd и показывают, что текущий клиент изменился, инициируя для свойства Current событие PropertyChanged. Эти методы объявлены закрытыми, потому что к ним не должно быть доступа за пределами класса ViewModel. Внешние классы будут запускать их, используя команды, которые вы добавите на следующих этапах выполнения упражнения.
Добавьте к классу ViewModel автоматически создаваемые свойства NextCustomer и PreviousCustomer, показанные далее жирным шрифтом:
public class ViewModel : INotifyPropertyChanged
{
private List<Customer> customers;
private int currentCustomer;
public Command NextCustomer { get; private set; }
public Command PreviousCustomer { get; private set; }
...
}
Представление будет привязываться к этим Command-объектам, чтобы пользователь мог переходить от клиента к клиенту.
Установите в конструкторе ViewModel для свойств NextCustomer (следующий клиент) и PreviousCustomer (предыдущий клиент) ссылки на новые Command-объекты:
public ViewModel()
{
this.currentCustomer = 0;
this.IsAtStart = true;
this.IsAtEnd = false;
this.NextCustomer = new Command(this.Next, () =>
{ return this.customers.Count > 1 && !this.IsAtEnd; });
this.PreviousCustomer = new Command(this.Previous, () =>
{ return this.customers.Count > 0 && !this.IsAtStart; });
...
}
Ссылка на новый Command-объект в свойстве NextCustomer указывает на метод Next как на операцию, проводимую в том случае, когда вызывается метод Execute. Лямбда-выражение () => { return this.customers.Count >1 && !this.IsAtEnd; } указывается в качестве функции, вызываемой при запуске метода CanExecute. Это выражение возвращает true при условии, что в списке клиентов содержится более одного клиента, а объект типа ViewModel не позиционирован на последнего клиента в списке. Ссылка на новый Command-объект в свойстве PreviousCustomer действует по аналогичной схеме: она вызывает метод Previous для извлечения из списка предыдущего клиента, а метод CanExecute ссылается на выражение () => { return this.customers.Count > 0 && !this.IsAtStart; }, которое возвращает true при условии, что в списке клиентов содержится хотя бы один клиент, и объект типа ViewModel не позиционирован на первого клиента в этом списке.
В меню Сборка щелкните на пункте Собрать решение и убедитесь в том, что сборка приложения прошла без ошибок.
Теперь, после добавления к ViewModel команд NextCustomer и PreviousCustomer, вы можете привязать их к кнопкам представления. Когда пользователь щелкнет на кнопке, запустится соответствующая команда.
Общие рекомендации и руководства по Microsoft publishes, касающиеся добавления кнопок к представлениям в UWP-приложениях, сводятся к тому, что кнопки, вызывающие команды, должны помещаться на панели команд. В UWP-приложениях предоставляются две панели команд: та, что появляется в верхней части формы, и та, что появляется в ее нижней части. Кнопки навигации по приложению или данным чаще всего помещаются в панель команд, расположенную в верхней части. Именно этим подходом вы и воспользуетесь в следующем упражнении.
ПРИМЕЧАНИЕ Рекомендации компании Microsoft относительно реализации панелей команд можно найти по адресу .
В окне конструктора откройте файл MainPage.xaml. Перейдите в нижнюю часть кода в панели XAML и добавьте следующую разметку, выделенную жирным шрифтом, непосредственно перед закрывающим тегом </Page>:
...
<Page.TopAppBar>
<CommandBar>
<AppBarButton x:Name="previousCustomer" Icon="Previous"
Label="Previous" Command="{Binding Path=PreviousCustomer}"/>
<AppBarButton x:Name="nextCustomer" Icon="Next"
Label="Next" Command="{Binding Path=NextCustomer}"/>
</CommandBar>
</Page.TopAppBar>
</Page>
В данном фрагменте XAML-разметки следует обратить внимание на ряд особенностей.
• По умолчанию панель команд появляется в верхней части экрана — видны значки содержащихся на ней кнопок. Надпись для каждой кнопки возникает только тогда, когда пользователь щелкает на кнопке More (…), появляющейся справа от панели команд. Но при разработке приложения, которое может использоваться в разных локализациях, следует для надписей вместо жестко заданных значений сохранять текст надписей в файле ресурсов с указанием локализаций, а свойство Label привязывать к нему в динамическом режиме при запуске приложения. Для получения дополнительных сведений следует обратиться к странице «Краткое руководство: преобразование ресурсов пользовательского интерфейса (XAML)» на веб-сайте компании Microsoft по адресу .
• Элемент управления CommandBar может содержать лишь ограниченный набор элементов управления (тех, которые реализуют интерфейс ICommandBarElement). Этот набор включает элементы управления типа AppBarButton, AppBarToggleButton и AppBarSeparator. Они специально разработаны для работы внутри элемента CommandBar. При попытке добавления на панель команд такого элемента управления, как кнопка, будет получено сообщение об ошибке с объяснением, что указанное значение не может быть присвоено коллекции: «The specified value cannot be assigned to the collection».
• Шаблоны UWP-приложения включают разнообразные значки (такие, как для кнопок Previous и Next, показанных в предыдущем примере кода), которые можно отобразить в элементе управления AppBarButton. Просмотреть весь набор доступных значков можно с помощью окна Свойства. Щелкните в разделе Значок на пункте Symbol Icon — в поле списка будет выведен набор доступных значков. Можно также определить собственные значки и графические изображения.
• У каждой кнопки есть свойство Command, которое можно привязать к объекту, реализующему интерфейс ICommand. В данном приложении кнопки вами были привязаны в классе ViewModel к командам PreviousCustomer и NextCustomer. Когда пользователь в ходе выполнения приложения щелкает на любой из них, запускается соответствующая команда.
Щелкните в меню Отладка на пункте Начать отладку. Должна появиться форма Customers и показать информацию о клиенте по имени John Sharp. На рис. 26.4 показано, что в верхней части формы должна отобразиться панель команд, содержащая кнопки Next и Previous.
Заметьте, что кнопка Previous недоступна. Дело в том, что свойство IsAtStart ViewModel-объекта имеет значение true, а метод CanExecute объекта типа Command, на который ссылается кнопка Previous, показывает, что команду запустить нельзя.
На панели команд щелкните на кнопке с тремя точками. Должны появиться надписи для кнопок. Они будут видны до тех пор, пока не будет сделан щелчок на одной из кнопок на панели команд.
На панели команд щелкните на кнопке Next. Появятся сведения о клиенте № 2, Diana Sharp, и после небольшой задержки (не более секунды) кнопка Previous
Рис. 26.4
станет доступной. Теперь свойство IsAtStart уже не имеет значения true, поэтому метод CanExecute этой команды возвращает true. Но кнопка не уведомляется об этом изменении состояния до тех пор, пока не истечет время в объекте таймера команды и не будет инициировано событие CanExecuteChanged, на что может уйти не более секунды.
ПРИМЕЧАНИЕ Если вам нужна более быстрая реакция на изменение состояния команд, можно настроить таймер в классе Command на более частое истечение времени. Но не следует сокращать время срабатывания таймера слишком сильно, поскольку излишне частое инициирование события CanExecuteChanged может повлиять на производительность интерфейса пользователя.
Еще раз щелкните на кнопке Next на панели команд.
Должны появиться сведения о клиенте № 3, Francesca Sharp, и после не более чем секундной задержки кнопка Next должна стать недоступной. На этот раз значение true получит свойство IsAtEnd объекта типа ViewModel, поэтому метод CanExecute объекта типа Command для кнопки Next вернет значение false и команда станет недоступной.
Измените размер окна приложения, чтобы оно перешло к отображению, предназначенному для узкой области просмотра, и убедитесь, что приложение продолжает работать подобающим образом. Кнопки Next и Previous будут вызывать переход в списке клиентов на одну позицию.
Вернитесь в среду Visual Studio и остановите отладку.
В этой главе вы научились выводить данные в форму с помощью привязки данных. Вы увидели, как устанавливается контекст данных для формы и как реализацией интерфейса INotifyPropertyChanged создается источник данных, поддерживающий привязку данных. Вы также научились использовать для создания UWP-приложения шаблон Model — View — ViewModel и увидели, как создается модель представления (ViewModel), с помощью которой представление может взаимодействовать с источником данных путем использования команд.
Чтобы | Сделайте следующее |
Привязать свойство элемента управления к свойству объекта | Воспользуйтесь выражением привязки данных в XAML-разметке элемента управления, например: <TextBox ... Text="{Binding FirstName}" .../> |
Задействовать объект и уведомить привязку об изменении в значении данных | Реализуйте интерфейс INotifyPropertyChanged в том классе, который определяет объект и инициирует событие PropertyChanged при каждом изменении значения свойства, например: class Customer : INotifyPropertyChanged { ... public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged( string propertyName) { if (PropertyChanged != null) { PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); } } } |
Задействовать элемент управления, использующий привязку данных, для обновления значения свойства, к которому сделана эта привязка | Настройте привязку данных на работу в обоих направлениях, например: <TextBox ... Text="{Binding FirstName, Mode=TwoWay}" .../> |
Отделить бизнес-логику, запускаемую при щелчке пользователя на элементе управления типа Button, от пользовательского интерфейса, содержащего этот элемент управления | Воспользуйтесь моделью представления (ViewModel), которая предоставляет команды, реализованные с помощью интерфейса ICommand, и привяжите элемент управления типа Button к одной из этих команд, например: <Button x:Name="nextCustomer" ... Command="{Binding Path=NextCustomer}"/> |