Главная » Разработка для Windows Phone 7 » Базы данных и бизнес-объекты

0

Использовать ListBox для отображения объектов Color или FontFamily можно в каких-то специализированных приложениях, но что вы собираетесь поместить в свой элемент управления списками?

Как правило, ItemsControl или ListBox заполняются этими непонятными, но при этом повсеместно применяющимися сущностями, которые называют бизнес-объектами.

Например, в приложении для выбора гостиницы, скорее всего, будет использоваться класс Hotel (Гостиница), и ListBox будет заполняться объектами Hotel. Являясь бизнес-объектом, Hotel не будет наследоваться от FrameworkElement. Но весьма вероятно, что Hotel будет реализовывать интерфейс INotifyPropertyChanged, чтобы иметь возможность динамически обновлять стоимость номера. Другой бизнес-объект будет сохранять коллекцию объектов Hotel, возможно, используя ObservableCollection и реализуя INotifyCollectionChanged для динамического отображения изменений при вводе в эксплуатацию новой гостиницы.

Чтобы создать пример, немного более приближенный к реальному приложению, я собираюсь посвятить остаток этой главы приложениям, использующим базу данных учеников школы. В этих примерах база данных загружается из папки на моем Веб-сайте. Но поскольку в этой главе я хочу сосредоточиться исключительно на представлении этих данных, изменения свойств класса Student (Учащийся) будут моделироваться локально.

В папке http://www.charlespetzold.com/Students моего Веб-сайта располагается файл students.xml, включающий данные 69 учащихся. В этой папке также хранятся черно-белые фотографии всех этих учеников. Фотографии я взял из школьных альбомов города Эль-Пасо, штат Техас, за 1912 – 1914 годы. Эти школьные альбомы хранятся в оцифрованном виде в Публичной библиотеке города Эль-Пасо и доступны на их сайте по адресу http://www.elpasotexas.gov/library/ourlibraries/main library/yearbooks/yearbooks.asp.

Среди исходного кода для главы 17 можно найти проект библиотеки ElPasoHighSchool, которая включает несколько классов для чтения XML-файла с моего Веб-сайта и его десериализации в.NET-объекты.

Рассмотрим класс Student. Он реализует INotifyPropertyChanged и имеет несколько свойств, относящихся к учащемуся, включая имя, пол, имя файла с фотографией и средний балл:

Проект Silverlight: ElPasoHighSchool Файл: Student.cs

using System;

using System.ComponentModel;

namespace ElPasoHighSchool {

public class Student : INotifyPropertyChanged {

public event PropertyChangedEventHandler PropertyChanged;

string fullName;

string firstName;

string middleName;

string lastName;

string sex;

string photoFilename;

decimal gradePointAverage;

public string FullName {

set {

if (fullName != value) {

fullName = value; OnPropertyChanged("FullName");

}

}

get {

return fullName;

}

}

public string FirstName {

set {

if (firstName != value) {

firstName = value; OnPropertyChanged("FirstName");

}

}

get {

return firstName;

}

}

public string MiddleName {

set {

if (middleName != value) {

middleName = value; OnPropertyChanged("MiddleName");

}

get {

return middleName;

}

}

public string LastName {

set {

if (lastName != value) {

lastName = value; 0nPropertyChanged("LastName");

}

}

get {

return lastName;

}

}

public string Sex {

set {

if (sex != value) {

sex = value;

0nPropertyChanged("Sex");

}

}

get {

return sex;

}

}

public string PhotoFilename {

set {

if (photoFilename != value) {

photoFilename = value; 0nPropertyChanged("PhotoFilename");

}

}

get {

return photoFilename;

}

}

public decimal GradePointAverage {

set {

if (gradePointAverage != value) {

gradePointAverage = value; 0nPropertyChanged("GradePointAverage");

}

}

get {

return gradePointAverage;

protected virtual void OnPropertyChanged(string propChanged) {

if (PropertyChanged != null)

PropertyChanged(this, new PropertyChangedEventArgs(propChanged));

}

}

}

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

Класс StudentBody (Основные сведения об учащемся) также реализует INotifyPropertyChanged: Проект Silverlight: ElPasoHighSchool Файл: StudentBody.cs

using System;

using System.Collections.ObjectModel; using System.ComponentModel; using System.Xml.Serialization;

namespace ElPasoHighSchool {

public class StudentBody : INotifyPropertyChanged {

public event PropertyChangedEventHandler PropertyChanged; string school;

ObservableCollection<Student> students =

new ObservableCollection<Student>();

public string School {

set {

if (school != value) {

school = value; OnPropertyChanged("School");

}

}

get {

return school;

}

}

public ObservableCollection<Student> Students {

set {

if (students != value) {

students = value; OnPropertyChanged("Students");

}

}

get {

return students;

protected virtual void 0nPropertyChanged(string propChanged) {

if (PropertyChanged != null)

PropertyChanged(this, new PropertyChangedEventArgs(propChanged));

}

}

}

Этот класс включает свойство для обозначения названия школы и ObservableCollection типа Student для хранения всех объектов Student. ObservableCollection очень популярен в Silverlight, потому что реализует интерфейс INotifyCollectionChanged, т.е. формирует события CollectionChanged при каждом добавлении или удалении элемента из коллекции.

Прежде чем продолжить, рассмотрим фрагмент файла student.xml, который хранится на моем Веб-сайте:

Файл: http://www.charlespetzold.com/Students/students.xml (фрагмент)

<?xml version="1.0" encoding="utf-8"?>

<StudentBody xmlns:xsi="http://www.w3.org/2 0 01/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2 0 01/XMLSchema"> <School>El Paso High School</School> <Students> <Student>

<FullName>Adkins Bowden</FullName>

<FirstName>Adkins</FirstName>

<MiddleName />

<LastName>Bowden</LastName>

<Sex>Male</Sex>

<PhotoFilename>

http://www.charlespetzold.com/Students/AdkinsBowden.png </PhotoFilename>

<GradePointAverage>2.71</GradePointAverage> </Student> <Student>

<FullName>Alfred Black</FullName>

<FirstName>Alfred</FirstName>

<MiddleName />

<LastName>Black</LastName>

<Sex>Male</Sex>

<PhotoFilename>

http://www.charlespetzold.com/Students/AlfredBlack.png </PhotoFilename>

<GradePointAverage>2.87</GradePointAverage> </Student>

<Student>

<FullName>William Sheley Warnock</FullName>

<FirstName>William</FirstName>

<MiddleName>Sheley</MiddleName>

<LastName>Warnock</LastName>

<Sex>Male</Sex>

<PhotoFilename>

http://www.charlespetzold.com/Students/WilliamSheleyWarnock.png </PhotoFilename>

<GradePointAverage>1.82</GradePointAverage> </Student> </Students> </StudentBody>

Как видите, теги элементов соответствуют свойствам классов Student и StudentBody. Я создал этот файл, применяя сериализацию XML посредством класса XmlSerializer. С помощью

десериализации XML этот файл может быть преобразован назад в объекты Student и StudentBody. Это задача класса StudentBodyPresenter, который снова реализует INotifyPropertyChanged:

Проект Silverlight: ElPasoHighSchool Файл: StudentBodyPresenter.cs

using System;

using System.ComponentModel; using System.IO; using System.Net; using System.Windows.Threading; using System.Xml.Serialization;

namespace ElPasoHighSchool {

public class StudentBodyPresenter : INotifyPropertyChanged {

public event PropertyChangedEventHandler PropertyChanged;

StudentBody studentBody; Random rand = new Random();

public StudentBodyPresenter() {

Uri uri =

new Uri("http://www.charlespetzold.com/Students/students.xml"); WebClient webClient = new WebClient();

webClient.DownloadStringCompleted += OnDownloadStringCompleted; webClient.DownloadStringAsync(uri);

}

void OnDownloadStringCompleted(object sender,

DownloadStringCompletedEventArgs args)

{

StringReader reader = new StringReader(args.Result); XmlSerializer xml = new XmlSerializer(typeof(StudentBody)); StudentBody = xml.Deserialize(reader) as StudentBody;

DispatcherTimer tmr = new DispatcherTimer(); tmr.Tick += TimerOnTick;

tmr.Interval = TimeSpan.FromMilliseconds(10 0); tmr.Start();

}

public StudentBody StudentBody {

protected set {

if (studentBody != value) {

studentBody = value; OnPropertyChanged("StudentBody");

}

}

get {

return studentBody;

}

}

protected virtual void OnPropertyChanged(string propChanged) {

if (PropertyChanged != null)

PropertyChanged(this, new PropertyChangedEventArgs(propChanged));

void Timer0nTick(object sender, EventArgs args) {

int index = rand.Next(studentBody.Students.Count); Student student = studentBody.Students[index];

double factor = 1 + (rand.NextDouble() – 0.5) / 5;

student.GradePointAverage =

Math.Max(0, Math.Min(5, Decimal.Round((decimal)factor *

student.GradePointAverage, 2)));

}

}

}

Класс StudentBodyPresenter (Презентатор основных сведений об учащемся) реализует доступ к файлу students.xml с помощью класса WebClient. Как мы помним, WebClient осуществляет асинхронный Веб-доступ, поэтому нуждается в методе обратного вызова для сообщения приложению о завершении своего выполнения. После этого метод Deserialize класса XmlSerializer преобразует текстовый XML-файл в объект StudentBody, который доступен как открытое свойство этого класса. Когда метод обратного вызова OnDownloadStringCompleted (По завершении загрузки строки) задает это свойство, класс формирует свое первое и единственное событие PropertyChanged.

Обратный вызов OnDownloadStringCompleted также запускает DispatcherTimer, который моделирует изменение данных. Свойство GradePointAverage одного из студентов меняется десять раз в секунду, что приводит к формированию события PropertyChanged соответствующим классом Student. Я очень надеюсь увидеть эти динамические изменения на экране.

Эксперименты с базой данных можно начать с открытия нового проекта на Silverlight, включения в него ссылки на библиотеку ElPasoHighSchool.dll и объявления пространства имен XML в файле MainPage.xaml:

xmlns:elpaso="clr-namespace:ElPasoHighSchool;assembly=ElPasoHighSchool"

После этого создаем экземпляр класса StudentBodyPresenter в коллекции Resources:

<phone:PhoneApplicationPage.Resources>

<elpaso:StudentBodyPresenter x:Key="studentBodyPresenter" /> </phone:PhoneApplicationPage.Resources>

Теперь можно поместить в область содержимого TextBlock с привязкой к этому ресурсу:

<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0"> <TextBlock HorizontalAlignment="Center" VerticalAlignment="Center"

Text="{Binding Source={StaticResource studentBodyPresenter}, Path=StudentBody.School}" />

</Grid>

Такой экран свидетельствует об успешной загрузке и десериализации файла students.xml нашим приложением:

Если изменить путь привязки и задать StudentBody.Students вместо StudentBody.School, на экран будет выведен ObservableCollection:

Можно выполнить доступ к свойству Count класса ObservableCollection:

<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0"> <TextBlock HorizontalAlignment="Center" VerticalAlignment="Center"

Text="{Binding Source={StaticResource studentBodyPresenter} Path=StudentBody.Students.Count}" />

</Grid>

И коллекция Students может быть проиндексирована:

<TextBlock HorizontalAlignment="Center" VerticalAlignment="Center"

Text="{Binding Source={StaticResource studentBodyPresenter}, Path=StudentBody.Students[23]}" />

Чтобы не удлинять эту привязку, разделим ее, задав DataContext в Grid для содержимого. DataContext наследуется по дереву визуальных элементов и упрощает описание привязки в TextBlock:

<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0"

DataContext="{Binding Source={StaticResource studentBodyPresenter}, Path=StudentBody}">

<TextBlock HorizontalAlignment="Center" VerticalAlignment="Center"

Text="{Binding Path=Students[23].FullName}" />

</Grid>

Здесь мы видим, что коллекция Students включает объекты типа Student:

Данная привязка указывает на имя конкретного учащегося:

Привязку можно еще более упростить, убрав из нее часть «Path=»:

<TextBlock HorizontalAlignment="Center" VerticalAlignment="Center" Text="{Binding Students[23].FullName}" />

Теперь заменим TextBlock элементом Image, который будет ссылаться на свойство PhotoFilename класса Student:

<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0"

DataContext="{Binding Source={StaticResource studentBodyPresenter}, Path=StudentBody}">

<Image HorizontalAlignment="Center" VerticalAlignment="Center" Stretch="None"

Source="{Binding Students[23].PhotoFilename}" />

</Grid>

И фотография учащегося успешно загружается и выводится на экран:

Ну, а теперь пора заканчивать заниматься глупостями и вставить реальный ListBox:

<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0"

DataContext="{Binding Source={StaticResource studentBodyPresenter}, Path=StudentBody}">

<ListBox ItemsSource="{Binding Students}" />

</Grid>

Свойство Students типа ObservableCollection, который, безусловно, реализует IEnumerable, а это на самом деле все что необходимо ListBox для его ItemsSource. Но также ListBox проверяет, способен ли объект, связанный с ItemsSource посредством привязки, на что-то большее. Например, реализует ли он INotifyCollectionChanged, что делает ObservableCollection. Благодаря реализации INotifyCollectionChanged при добавлении нового Student в коллекцию или удалении из коллекции объектов учащихся, окончивших школу, ListBox будет знать об этом и менять отображаемые элементы соответствующим образом.

Но пока что, кажется, ListBox не очень доволен своими данными:

При виде ListBox или ItemsControl, отображающего огромный список идентичных имен классов, не впадайте в отчаяние. Наоборот, надо радоваться! Такой вывод свидетельствует о том, что ListBox успешно заполнен элементами одного типа. Теперь для отображения чего-то более значащего ему необходим лишь DataTemplate или (для ленивых) параметр DisplayMemberPath:

<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0"

DataContext="{Binding Source={StaticResource studentBodyPresenter}, Path=StudentBody}">

<ListBox ItemsSource="{Binding Students}" DisplayMemberPath="FullName" />

</Grid>

Вот что получается:

Давайте пока оставим ListBox как есть и сосредоточимся на отображении выделенного элемента ListBox.

Добавив еще одну строку в Grid, разместим внизу экрана еще один TextBlock:

<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0"

DataContext="{Binding Source={StaticResource studentBodyPresenter}, Path=StudentBody}">

<Grid.RowDefinitions>

<RowDefinition Height="*" /> <RowDefinition Height="Auto" /> </Grid.RowDefinitions>

<ListBox Grid.Row="0"

Name="listBox"

ItemsSource="{Binding Students}" DisplayMemberPath="FullName" />

<TextBlock Grid.Row="1"

FontSize="{StaticResource PhoneFontSizeLarge}"

HorizontalAlignment="Center"

Text="{Binding ElementName=listBox,

Path=SelectedItem.FullName}" />

</Grid>

Обратите внимание на привязку, заданную для TextBlock. Свойство SelectedItem класса ListBox типа Student, поэтому путь привязки может ссылаться на одно из свойств Student, например FullName (Полное имя). Теперь при выборе элемента ListBox в TextBlock отображается значение свойства FullName этого элемента:

Или заменим TextBlock элементом Image:

<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0"

DataContext="{Binding Source={StaticResource studentBodyPresenter}, Path=StudentBody}">

<Grid.RowDefinitions>

<RowDefinition Height="*" /> <RowDefinition Height="Auto" /> </Grid.RowDefinitions>

<ListBox Grid.Row="0"

Name="listBox"

ItemsSource="{Binding Students}" DisplayMemberPath="FullName" />

<Image Grid.Row="1"

HorizontalAlignment="Center" Stretch="None"

Source="{Binding ElementName=listBox,

Path=SelectedItem.PhotoFilename}" />

</Grid>

Можете теперь выбрать в ListBox любой элемент и увидеть фотографию соответствующего учащегося:

Чтобы обеспечить возможность просмотра множества свойств выбранного элемента, потребуется поместить в Border еще одно описание DataContext:

<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0"

DataContext="{Binding Source={StaticResource studentBodyPresenter}, Path=StudentBody}">

<Grid.RowDefinitions>

<RowDefinition Height="*" /> <RowDefinition Height="Auto" /> </Grid.RowDefinitions>

<ListBox Grid.Row="0"

Name="listBox"

ItemsSource="{Binding Students}" DisplayMemberPath="FullName" />

<Border Grid.Row="1"

BorderBrush="{StaticResource PhoneForegroundBrush}" BorderThickness="{StaticResource PhoneBorderThickness}" HorizontalAlignment="Center" DataContext="{Binding ElementName=listBox, Path=SelectedItem}">

</Border> </Grid>

В этом Border могут располагаться панель и элементы с привязками к свойствам класса Student. Именно это и было сделано мною в приложении StudentBodyListBox. XAML-файл включает объявление пространства имен XML для библиотеки ElPasoHighSchool:

xmlns:elpaso="clr-namespace:ElPasoHighSchool;assembly=ElPasoHighSchool"

Экземпляр класса StudentBodyPresenter создается в коллекции Resources: Проект Silverlight: StudentBodyListBox Файл: MainPage.xaml (фрагмент)

<phone:PhoneApplicationPage.Resources>

<elpaso:StudentBodyPresenter x:Key="studentBodyPresenter" /> </phone:PhoneApplicationPage.Resources>

Рассмотрим область содержимого:

Проект Silverlight: StudentBodyListBox Файл: MainPage.xaml (фрагмент)

<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0"

DataContext="{Binding Source={StaticResource studentBodyPresenter}, Path=StudentBody}">

<Grid.RowDefinitions>

<RowDefinition Height="Auto" /> <RowDefinition Height="*" /> <RowDefinition Height="Auto" /> </Grid.RowDefinitions>

<TextBlock Grid.Row="0"

Text="{Binding School}"

FontSize="{StaticResource PhoneFontSizeLarge}" HorizontalAlignment="Center" TextDecorations="Underline" />

<ListBox Grid.Row="1"

Name="listBox"

ItemsSource="{Binding Students}" DisplayMemberPath="FullName" />

<Border Grid.Row="2"

BorderBrush="{StaticResource PhoneForegroundBrush}" BorderThickness="{StaticResource PhoneBorderThickness}" HorizontalAlignment="Center" DataContext="{Binding ElementName=listBox,

Path=SelectedItem}">

<Grid>

<Grid.RowDefinitions>

<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" /> <RowDefinition Height="Auto" /> </Grid.RowDefinitions>

<TextBlock Grid.Row="0"

Text="{Binding FullName}" TextAlignment="Center" />

<Image Grid.Row="1" Width="225" Height="3 0 0" Margin="24 6"

Source="{Binding PhotoFilename}" />

<StackPanel Grid.Row="2"

0rientation="Horizontal" HorizontalAlignment="Center">

<TextBlock                       Text="GPA=" />

<TextBlock                       Text="{Binding GradePointAverage}" /> </StackPanel> </Grid>

</Border> </Grid>

В Border располагается Grid с тремя строками, в котором размещен TextBlock с привязкой к свойству FullName, элементу Image и StackPanel для отображения среднего балла. Обратите внимание, что я задал элементу Image конкретные размеры, исходя из известных размеров предоставляемых изображений. Это обеспечит неизменность размера элемента Image.

Теперь мы можем прокручивать список ListBox и просматривать сведения каждого учащегося:

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

Источник: Чарльз Петзольд, Программируем Windows Phone 7, Microsoft Press, © 2011.

По теме:

  • Комментарии