Основы разработки приложений для мобильных устройств на платформе Windows Phone

Хранение данных приложений

Показывать лекцию целиком

Презентацию к данной лекции Вы можете скачать здесь.

14.1. Хранилище данных Windows Phone

Практически в любом приложении требуется хранить данные, которые приложение использует при работе. Приложение Silverlight должно сохранять настройки и информацию, с которой работает пользователь, игре XNA требуется хранить информацию о достижениях игрока, список рекордов и игровые настройки. Программы Windows Phone могут использовать изолированное хранилище для хранения подобной информации. Хранилище называют "изолированным", потому что приложение не может получить доступ к данным, которые сохраняет другое приложение, в отличие от компьютеров под управлением Windows, в которых любая программа может получить доступ к любому файлу в системе.

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

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

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

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

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

(рис 14.1) Экран приложения для хранения текстовых заметок

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

private void saveButton_Click(object sender, RoutedEventArgs e)
{
    saveText("notes.txt", jotTextBox.Text);
}

Это код для кнопки сохранить. Здесь текст из текстового поля передается методу saveText вместе с именем файла, в котором этот текст нужно сохранить.

private void saveText(string filename, string text)
{
    using (IsolatedStorageFile isf = IsolatedStorageFile.GetUserStoreForApplication())
    {
        using (IsolatedStorageFileStream rawStream = isf.CreateFile(filename))
        {
            StreamWriter writer = new StreamWriter(rawStream);
            writer.Write(text);
            writer.Close();
        }
    }
}

Метод saveText создает поток, связанный с указанным файлом в изолированном хранилище, и записывает в него текст. Потоки работают точно так же, как и в других программах на C#. В нашем случае метод создает объект StreamWriter, которому передается текст.

Кнопка загрузить использует метод loadText для выполнения обратных действий.

private void loadButton_Click(object sender, RoutedEventArgs e)
{
    string text;

    if (loadText("notes.txt", out text))
    {
        notesTextBox.Text = text;
    }
    else
    {
        notesTextBox.Text = "Напишите здесь что-нибудь....";
    }  
}

Метод loadText пытается открыть файл, имя которого передается в качестве параметра, после чего пытается считать строку из того файла. Если одно из этих действий заканчивается неудачей, метод возвращает значение false, и в элемент notesTextBox записывается начальное сообщение.

Если чтение содержимого файла выполнено успешно, метод loadText возвращает значение true и записывает содержимое файла в выходной параметр, после чего этот текст заносится в элемент notesTextBox.

private bool loadText(string filename, out string result)
{
    result = "";
    using (IsolatedStorageFile isf = IsolatedStorageFile.GetUserStoreForApplication())
    {
        if (isf.FileExists(filename))
        {
            try
            {
                using (IsolatedStorageFileStream rawStream =
                    isf.OpenFile(filename, System.IO.FileMode.Open))
                {
                    StreamReader reader = new StreamReader(rawStream);
                    result = reader.ReadToEnd();
                    reader.Close();
                }
            }
            catch
            {
                return false;
            }
        }
        else
        {
            return false;
        }
    }
    return true;
}

Метод loadText возвращает значение false, если входной файл не найден, или при чтении файла возникает исключение. Метод ReadToEnd позволяет делать в программе многострочные текстовые записи.

Использование изолированного хранилища для сохранения настроек

Часто программам нужно сохранить некоторые настройки, которые являются простыми значениями. В этом случае использовать файлы и потоки очень неудобно. Чтобы облегчить сохранение настроек, программа Windows Phone может использовать хранилище для настроек. Оно работает как словарь, который может сохранить любое количество пар "имя—значение" в изолированной области.

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

Словари удобно использовать для хранения пар "имя—значение". Они избавляют от необходимости написания кода для поиска необходимых значений в коллекции. Также в программе можно использовать несколько словарей.

В Windows Phone можно использовать класс IsolatedStorageSettings, который является словарем для хранения настроек в системе. Словарь настроек хранит коллекцию объектов и использует строку в качестве ключа.

Сохранение текста в изолированное хранилище настроек. Можно изменить нашу программу, добавив использование изолированного хранилища настроек:

private void saveText(string filename, string text)
{
    IsolatedStorageSettings isolatedStore =
        IsolatedStorageSettings.ApplicationSettings;
    isolatedStore.Remove(filename);
    isolatedStore.Add(filename, text);
    isolatedStore.Save();
}

Эта версия метода saveText использует имя файла в качестве ключа. Он удаляет запись с существующим ключом, значение которого является именем файла, и добавляет переданный текст в качестве нового элемента. Метод Remove вызывается для удаления элемента из словаря. Метод принимает в качестве параметра значение ключа элемента, который нужно удалить. Если элемента с таким ключом в словаре нет, метод Remove возвращает значение false, но в нашем примере это не имеет значения. После выполнения изменений в хранилище, необходимо вызвать метод Save для сохранения этих изменений.

Загрузка текста из изолированного хранилища настроек. Метод loadText будет считывать значения из словаря настроек:

private bool loadText(string filename, out string result)
{
    IsolatedStorageSettings isolatedStore =
        IsolatedStorageSettings.ApplicationSettings;
    result = "";
    try
    {
        result = (string)isolatedStore[filename];
    }
    catch
    {
        return false;
    }
    return true;
}

Метод loadText выбирает запрошенный элемент из хранилища настроек. Его использование осложняется тем, что в отличие от класса Dictionary класс IsolatedStorageSettings не содержит метод ContainsKey, который используется для определения, есть ли в словаре данный элемент. Наш метод перехватывает исключение, которое генерируется, если элемент не найден, и возвращает значение false, чтобы указать, что такого элемента нет.

Таким образом, приложениям доступны два способа сохранения данных в устройстве Windows Phone. Большие объемы данных можно сохранять в файловом хранилище, в котором можно создать целую файловую структуру. В качестве альтернативы можно сохранять отдельные элементы данных по имени в словаре настроек. Механизм изолированного хранилища можно использовать как в программах Silverlight, так и в играх XNA.

Программа Isolated Storage Explorer

Пакет инструментов для разработки приложений Windows Phone включает программу Isolated Storage Explorer, которая позволяет просматривать файлы в изолированном хранилище. Эта программа имеет интерфейс командной строки и по умолчанию устанавливается в папку C:\Program Files\Microsoft SDKs\Windows Phone\v7.1\Tools\IsolatedStorageExplorerTool.

Программе Isolated Storage Explorer необходимо указать GUID приложения, который определяет необходимую область изолированного хранилища. Значение GUID можно найти в файле приложения WMAppManifest.xml:

<App xmlns="" ProductID="{3363ac33-4f45-4b21-b932-fa2084b6deb0}" Title="JotPad"
    RuntimeType="Silverlight" Version="1.0.0.0" Genre="apps.normal"
    Author="JotPad author" Description="Sample description" Publisher="JotPad">

Для считывания содержимого изолированного хранилища используется команда ISETool с соответствующими параметрами:

ISETool ts xd 3363ac33-4f45-4b21-b932-fa2084b6deb0 c:\isoStore

Эта команда делает снимок изолированного хранилища эмулятора для приложения со значением GUID равным {3363ac33-4f45-4b21-b932-fa2084b6deb0} и помещает его в папку c:\isoStore на компьютере.

Описание всех параметров программы Isolated Storage Explorer можно получить, набрав в командной строке команду ISETool без параметров.

14.2. Базы данных в Windows Phone

База данных — это набор данных, которые организованы и управляются компьютерной программой, которая называется система управления базами данных.

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

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

Таблица Customer (таблица 5.1) содержит информацию о клиентах и включает четыре столбца для хранения, соответственно, идентификатора, имени, адреса и названия банка клиента. Каждая строка таблицы называется записью и хранит информацию об одном клиенте.

Таблица Customer
CustomerID Name Address BankDetails
123456 Rob 18 Pussycat Mews Nut East Bank
654322 Jim 10 Motor Drive Big Fall Bank
111111 Ethel 4 Funny Address Strange Bank

Таблица Product (таблица 5.2) содержит столбцы для хранения информации товарах: идентификатор товара, наименование, поставщик и цена.

Таблица Product
ProductID ProductName Supplier Price
1001 Windows Phone 7 Microsoft 200
1002 Cheese grater Cheese Industries 2
1003 Boat hook John's Dockyard 20

Таблица Order (таблица 5.3) содержит информацию о заказах товаров и ссылается на записи двух других таблиц. Столбцы таблицы содержат идентификатор заказа, идентификаторы клиента и купленного товара, количество товара, дату заказа и его статус.

Таблица Order
OrderID CustomerID ProductID Quantity OrderDate Status
1 123456 1001 1 21/10/2010 Shipped
2 111111 1002 5 10/10/2010 Shipped
3 654322 1003 4 1/09/2010 On order

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

Обратите внимание, что имена столбцов указаны без пробелов — это позволит избежать некоторых сложностей при создании запросов к базе данных.

Работа с базой данных заключается, в основном, в создании и отправке запросов к базе данных и получении и обработке результатов. Запросы к базам данных обычно составляются на языке SQL (Structured Query Language) — структурированном языке запросов. Язык SQL является мощным инструментом для работы с информацией в базе данных. С помощью SQL-запросов можно получать из базы данных необходимую информацию в удобном виде.

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

Использование LINQ для связи базы данных с объектами

Для работы с информацией в базе данных чаще всего используется язык SQL. Чтобы использовать базы данных в объектно-ориентированных программах, обычно необходимо писать много кода, который передает данные из таблиц в объекты, и обратно при сохранении данных. Это довольно большая работа, особенно для больших программ, использующих базы данных с большим количеством таблиц и классов, представляющих информацию из таблиц.

Существенно упростить эту задачу позволяет интегрированный язык запросов LINQ. Язык LINQ фактически встроен в язык C# и добавляет особые синтаксические конструкции для составления запросов, которые используют программные объекты. Windows Phone позволяет использовать в программах базы данных, взаимодействие с которыми выполняется с помощью LINQ.

Чтобы использовать в программе код LINQ, нужно добавить в проект ссылку на библиотеки System.Data.Linq. Поскольку не во всех программах необходимо использовать базы данных, эта ссылка обычно не добавляется при создании проекта. После этого для облегчения работы с классами LINQ рекомендуется добавить директивы using:

using System.Linq;
using System.Data.Linq;
using System.Data.Linq.Mapping;
using System.ComponentModel;
using System.Collections.ObjectModel;

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

  • Customer — для хранения информации о клиентах;
  • Order — для хранения информации о заказах;
  • Product — для хранения информации о товарах.
  • Создание класса для хранения информации из таблицы. Начнем с таблицы Customer. Необходимо создать класс, описывающий данные, которые должны храниться в таблице Customer. Этот класс можно использовать для создания в базе данных таблицы, содержащей информацию обо всех клиентах. Класс может быть описан так:

    public class Customer
    {
        public int CustomerID { get; set; }
        public string Name { get; set; }
        public string Address { get; set; }
        public string BankDetails { get; set; }
    }
    

    Класс Customer содержит три строковых свойства и одно целочисленное. Все свойства доступны для чтения и записи. Версия класса Customer класса для LINQ выглядит похоже:

    [Table]
    public class Customer : INotifyPropertyChanged, INotifyPropertyChanging
    {
        // здесь описывается структура класса
    }
    

    Строка [Table] является атрибутом. Атрибуты используются для добавления к классам специальной информации, которую могут использовать программы, считывающие метаданные в скомпилированном коде. Слово "метаданные" означает "данные о данных". В нашем случае добавляемые данные являются атрибутом, который LINQ интерпретирует как: "Этот класс может использоваться в качестве основы для таблицы данных". Сам атрибут является обычным тегом, и никак не влияет на поведение самого класса.

    Также для работы с LINQ класс должен реализовать интерфейсы INotifyPropertyChanged и INotifyPropertyChanging, т.е. должен содержать все методы, которые определены в интерфейсе. Эти интерфейсы содержат методы, которые будут использоваться, чтобы сообщить LINQ, когда данные в классе будут изменены. Этот механизм похож на привязку данных к визуальным элементам, только теперь изменение данных будет вызывать изменения в базе данных.

    Каждый из этих интерфейсов содержит по одному делегату событий:

    public event PropertyChangedEventHandler PropertyChanged;
    public event PropertyChangingEventHandler PropertyChanging;
    

    Инфраструктура LINQ связана с событием PropertyChanged, которое происходит после изменения значения свойства, а также с событием PropertyChanging, происходящим перед изменением значения.

    Необходимо сделать так, чтобы класс Customer генерировал эти события при изменении значений свойств. Свойство Name может содержать следующий код:

    private string nameValue;
    
    public string Name
    {
        get
        {
            return nameValue;
        }
        set
        {
            if (PropertyChanging != null)
            {
                PropertyChanging(this, new PropertyChangingEventArgs("Name"));
            }
    
            nameValue = value;
    
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs("Name"));
            }
        }
    }
    

    В этом коде секция get свойства просто возвращает значение свойства, которое содержит имя. Секция set проверяет, назначены ли событиям обработчики, и вызывает один из них перед изменением значения свойства, а другой — после. Делегаты принимают два параметра. Первый параметр является ссылкой на текущего клиента (имя которого изменяется). Второй параметр содержит название свойства, которое изменяется. LINQ использует эти параметры, чтобы выяснить, какие значения изменились, и выполняет соответствующие изменения в базе данных.

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

    Если объекты содержат много свойств, имеет смысл упростить управление событиями с помощью добавления методов, которые будут осуществлять управление:

    private void NotifyPropertyChanged(string propertyName)
    {
        if (PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }
    
    private void NotifyPropertyChanging(string propertyName)
    {
        if (PropertyChanging != null)
        {
            PropertyChanging(this, new PropertyChangingEventArgs(propertyName));
        }
    }
    

    Тогда код свойств существенно упростится:

    private string nameValue;
    
    public string Name
    {  
        get
        {
            return nameValue;
        }
        set
        {
            NotifyPropertyChanging("Name");
            nameValue = value;
            NotifyPropertyChanged("Name");
        }
    }
    

    Чтобы LINQ мог использовать это свойство, к нему необходимо добавить атрибут [Column]:

    [Column]
    public string Name
    {
        // здесь описывается поведение свойства Name
    }
    

    Аналогичным образом создается код свойств Address и BankDetails для хранения информации об адресе и банке клиента. Эти свойства будут отображаться на соответствующие столбцы таблицы в базе данных.

    Создание первичного ключа. Поле CustomerID немного отличается от остальных. В нашем примере это целочисленное значение, которое уникально для каждого клиента. Если клиент сменит имя, информацию о нем по-прежнему можно будет найти по значению идентификатора. В описанной выше таблицы базы данных поле CustomerID будет являться первичным ключом таблицы Customer. Первичный ключ позволяет уникально идентифицировать каждого клиента в этой таблице. Даже если среди клиентов у двоих совпадает имя, у них будут разные значения идентификатора, по которым можно отличить одного клиента от другого.

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

    [Column(IsPrimaryKey = true, IsDbGenerated = true)]
    public int CustomerID { get; set; }
    

    Теперь при добавлении в базу данных записи о новом клиенте значение его идентификатора будет генерироваться автоматически. При этом, мы не сможем изменить значение идентификатора клиента, поскольку оно является уникальным ключом, который будет автоматически создаваться базой данных для каждого клиента. Соответствующее поле в таблице в базе данных является первичным ключом для таблицы Customer, который будет использоваться в базе данных для связи таблицы Customer с таблицей Order. Каждая строка таблицы Order будет содержать значение идентификатора клиента, однозначино идентифицирующее клиента, который создал этот заказ.

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

    Создание контекста данных LINQ. Теперь можно создать таблицу в базе данных. Для этого необходимо создать класс, наследуемый от класса DataContext, который будет управлять подключением к базе данных. Он будет содержать все таблицы для приложения.

    public class SalesDB : DataContext
    {
        public Table<Customer> Customers;
    
        public SalesDB(string connection)
            : base(connection) 
        { 
        }
    }
    

    Класс DataContext в LINQ является базовым классом для проектирования баз данных. В его состав входят методы, которые можно использовать для управления содержимым базы данных. Конструктор наследуемого класса SalesDB просто вызывает конструктор родительского класса. Строка подключения определяет параметры для подключения к базе данных, которую нужно использовать. База данных может располагаться как на рабочем устройстве, так и на удаленном сервере в сети. В нашем случае база данных находится в файле, который хранится в изолированном хранилище в телефоне, и строка подключения должна содержать путь к этому файлу.

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

    public static void MakeTestDB(string connection)
    {
        string[] firstNames = new string[] { "Rob", "Jim", "Joe", "Nigel", "Sally",
            "Tim" };
        string[] lastsNames = new string[] { "Smith", "Jones", "Bloggs", "Miles",
            "Wilkinson", "Brown" };
    
        SalesDB newDB = new SalesDB(connection);
    
        if (newDB.DatabaseExists())
        {
            newDB.DeleteDatabase();
        }
    
        newDB.CreateDatabase();
    
        foreach (string lastName in lastsNames)
        {
            foreach (string firstname in firstNames)
            {
                string name = firstname + " " + lastName;
                string address = name + "'s address";
                string bank = name + "'s bank";
                Customer newCustomer = new Customer();
                newCustomer.Name = name;
                newCustomer.Address = address;
                newCustomer.BankDetails = bank;
                newDB.CustomerTable.InsertOnSubmit(newCustomer);
            }
        }
    
        newDB.SubmitChanges();
    }
    

    Сначала этот метод пытается подключиться к базе данных. Переменная newDB представляет подключение к базе данных в соответствии с заданной строкой подключения. Метод может использовать это подключение, чтобы отправлять запросы для управления базой данных. Если указанная база данных существует, она удаляется.

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

    Метод InsertOnSubmit добавляет новую запись в таблицу базы данных. Этот метод является безопасным с точки зрения типов передаваемых параметров, т.е. если бы мы попытались добавить в таблицу CustomerTable данные другого типа, компилятор выдал бы сообщение об ошибке.

    Завершается метод вызовом метода SubmitChanges, который, собственно, и записывает все сделанные изменения в файл базу данных. До этих пор все изменения хранятся в памяти программы. Такой подход позволяет ускорить выполнение операций с базой данных, особенно если выполняется много операций модификации данных. Если не будет вызван метод SubmitChanges, изменения в базе данных не сохранятся.

    Теперь созданную базу данных можно использовать в приложении. Контекст базы данных нужно разместить в файле программы App.xaml.cs. Там же размещается ссылка на информацию об активном клиенте:

    public SalesDB ActiveDB;
    public Customer ActiveCustomer;
    

    При запуске программы в переменной ActiveDB нужно создать ссылку на используемую базу данных.

    ActiveDB = new SalesDB("Data Source=isostore:/Sample.sdf");
    

    Путь к файлу имеет особый формат и идентифицирует файл в изолированном хранилище. Можно использовать в программе несколько баз данных, каждая из которых будет находиться в разных файлах. Формат файла базы данных такой же, как и формат файла стандартной базы данных SQL. Программа для работы с базой данных для компьютера может считывать таблицы из файла, и этим файлом может управлять редактор базы данных SQL. Приложение для Windows Phone также может использовать базу данных, подготовленную на другом компьютере.

    Привязка элемента ListBox к результату запроса LINQ. Теперь можно использовать эту базу данных в нашем приложении. Нужно получить данные из базы данных и вывести их на экран.

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

    var customers = from Customer customer
                     in thisApp.ActiveDB.CustomerTable
                     select customer;
    

    Эта команда C# создает переменную customers, которая имеет тип var. Этот тип означает, что тип переменной определяется на основе значения, которое присваивается этой переменной. При этом сохраняется строгая типизация C#, и если попытаться использовать переменную customers недопустимым способом, программа не скомпилируется.

    Остальная часть команды сообщает LINQ, что нужно получить все элементы customer из свойства CustomerTable контекста базы данных ActiveDB. Эти данные возвращаются в виде списка клиентов, который может быть наблюдаемой коллекцией. Чтобы вывести на экран полученный список клиентов, можно задать этот список в качестве источника данных элементу ListBox:

    customerList.ItemsSource = customers;
    

    Теперь у нас есть полностью рабочая база данных клиентов. Остается только добавить в программу код для применения изменений в базе данных при выходе из программы. Лучший способ это сделать — добавить необходимый код в метод OnNavigatedFrom в файле MainPage.xaml.cs:

    protected override void OnNavigatedFrom(
        System.Windows.Navigation.NavigationEventArgs e)
    {
        App thisApp = Application.Current as App;
    
        thisApp.ActiveDB.SubmitChanges();
    }
    

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

    Добавление фильтров. Если немного изменить запрос LINQ, можно выбрать записи, которые соответствуют определенным критериям:

    var customers = from Customer customer
                     in thisApp.ActiveDB.CustomerTable
                     where customer.Name.StartsWith("S")
                     select customer;
    

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

    14.3. Создание связей данных в LINQ

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

    Класс для таблицы Orders будет иметь более сложную структуру, поскольку эта таблица содержит ссылки на записи двух других классов. В C# можно создать такое описание класса:

    public class Order
    {
        public DateTime OrderDate;
        public int Quantity;
        public Customer OrderCustomer;
        public Product OrderProduct;
    }
    

    Свойства OrderCustomer и OrderProduct связывают заказ с клиентом и заказанным им товаром. Соответствующие им поля таблицы базы данных являются внешними ключами.

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

    Ассоциации LINQ

    В LINQ отношения между двумя таблицами называют ассоциацией. Она задается в классах базы данных с помощью класса EntityRef.

    Связывание заказа с клиентом. В нашей программе в классе Order должна быть ссылка на класс Customer. Она создается в виде отношения между двумя таблицами. EntityRef — это специальный класс LINQ, который можно использовать для связи двух таблиц.

    [Table]
    public class Order : INotifyPropertyChanged, INotifyPropertyChanging
    {
    
        ...
    
        private EntityRef<Customer> orderCustomer;
    
        [Association(IsForeignKey = true, Storage = "orderCustomer")]
        public Customer OrderCustomer
        {
            get
            {
                return orderCustomer.Entity;
            }
            set
            {
                NotifyPropertyChanging("OrderCustomer");
                orderCustomer.Entity = value;
                NotifyPropertyChanged("OrderCustomer");
            }
        }
    }
    

    Класс EntityRef является своего рода ссылкой на таблицу. Теперь можно написать такой код:

    Customer newCustomer = new Customer();
    Order newOrder = new Order();
    newOrder.OrderCustomer = newCustomer;
    

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

    В атрибуте [Association] указывается, что ассоциация будет являться внешним ключом. Параметр Storage сообщает LINQ, какое закрытое свойство базы данных будет хранить информацию о свойстве.

    Связывание клиента со всеми его заказами. Теперь программа может для заданного заказа получить информацию о клиенте, который создал этот заказ, поскольку запись в таблице Order содержит поле CustomerID, которое является идентификатором клиента. Однако, была бы очень полезна возможность найти для заданного клиента все его заказы.

    В базе данных таблицы Customer и Order находятся в отношении "один ко многим". В классе C# Customer для хранения заказов клиента можно использовать одну из коллекций, например, массив или список. Однако, в LINQ для представления таких отношений нужно использовать класс EntitySet. Этот класс похож на EntityRef, но он позволяет управлять целым набором элементов. Можно связать клиента с его заказами, добавив в класс Customer свойство типа EntitySet:

    [Table]
    public class Customer : INotifyPropertyChanged, INotifyPropertyChanging
    {
    
        [Column(IsPrimaryKey = true, IsDbGenerated = true, AutoSync = AutoSync.OnInsert)]
        public int CustomerID { get; set; }
    
        private EntitySet<Order> orders = new EntitySet<Order>();
    
        [Association(Storage = "orders", ThisKey = "CustomerID",
            OtherKey = "OrderCustomerID")]
        public EntitySet<Order> Orders
        {
            get
            {
                return orders;
            }
            set
            {
                orders = value;
            }
        }
    }
    

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

    Параметр ThisKey задает имя свойства, которое будет использовать класс Order для поиска клиента, с которым связана ассоциация. Можно использовать свойство CustomerID, которое является первичным ключом для класса Customer.

    Параметр OtherKey задает имя свойства класса Order, которое связано ассоциацией с другой стороны, то есть для заказа можно будет найти клиента, с которым связан этот заказ.

    При изменении заказа не требуется генерировать события NotifyPropertyChanged, поскольку тип EntitySet является контейнером для заказов, а не непосредственно заказом. При добавлении заказов происходит добавление элементов в EntitySet, а не замена контейнера.

    Чтобы ассоциацию работала должным образом, необходимо сделать некоторые изменения в ссылке в классе Order:

    [Table]
    public class Order : INotifyPropertyChanged, INotifyPropertyChanging
    {
    
        ...
    
        [Column]
        public int OrderCustomerID;
    
        private EntityRef<Customer> orderCustomer = new EntityRef<Customer>();
    
        [Association(IsForeignKey = true, Storage = "orderCustomer",
            ThisKey = "OrderCustomerID")]
        public Customer OrderCustomer
        {
            get
            {
                return orderCustomer.Entity;
            }
            set
            {
                NotifyPropertyChanging("OrderCustomer");
                orderCustomer.Entity = value;
                NotifyPropertyChanged("OrderCustomer");
                if (value != null)
                    OrderCustomerID = value.CustomerID;
            }
        }
    }
    

    Ассоциация использует внешний ключ (первичный ключ базы данных клиентов) и значение OrderCustomerID для хранения его значения.

    В этой версии кода значение CustomerID копируется в столбец OrderCustomerID таблицы, когда клиенту назначается заказ. Теперь можно написать такой код:

    Customer c = new Customer();
    Order o = new Order();
    o.OrderCustomer = c;
    

    Код секции set свойства OrderCustomer копирует значение CustomerID в свойство OrderCustomerID для записи клиента текущего заказа.

    Таким образом, можно легко добавить заказы клиента:

    Customer c = new Customer();
    Order o = new Order();
    o.OrderCustomer = c;
    c.Orders.Add(o);
    

    Класс EntitySet содержит метод Add, который позволяет добавлять заказы в базу данных. Для работы с содержимым набора объектов можно использовать одну из конструкций C# для перебора элементов коллекции.

    Проектирование структуры баз данных для возможности заказа нескольких товаров. Теперь в структуре базы данных есть связь между клиентами и заказами. У клиента может быть несколько заказов, и каждый заказ связан с одним клиентом, который его создал.

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

    Чтобы решить проблему, необходимо добавить новую таблицу OrderItem, в которой будет храниться информация обо всех товарах каждого заказа. Каждый элемент OrderItem будет связан с определенным заказом. Соответственно, каждый заказ будет содержать набор этих элементов. Таблица будет иметь структуру, представленную в таблице 5.4.

    Таблица OrderItem
    OrderItemID OrderID ProductID Quantity
    1 56 1001 1
    2 56 1002 5
    3 12343 1003 4

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

    (рис 14.2) Диаграмма классов, соответствующих таблицам базы данных приложения

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

    Выполнение запросов LINQ

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

    DateTime searchDate = new DateTime(2011, 8, 19);
    
    var orders = from Order order in activeDB.OrderTable
                  where order.OrderDate == searchDate
                 select order;
    

    Этот запрос возвращает из таблицы заказов активной базы данных все заказы, созданные 19 августа 2011 г., и заносит их в переменную orders, тип которой трактуется как "запрос LINQ". При этом, эта строка программы только создает запрос, но еще не выполняет никаких операций для получения данных. Информация будет считана из базы данных при попытке обращения к результатам этого запроса:

    int totalSales = 0;
    
    foreach (Order order in orders)
    {
        foreach (OrderItem item in order.OrderItems)
        {
            totalSales += item.OrderItemProduct.Price * item.Quantity;
        }
    }
    

    Этот код получает информацию о заказах, которые будут получены запросом orders, и вычисляет полную стоимость всех заказанных товаров. Первый цикл foreach перебирает по одному заказу за каждый проход цикла, и в этот момент LINQ начинает получать информацию из базы данных.

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

    List<Order> DateOrders = orders.ToList<Order>();
    

    Этот код создает в переменной DateOrders список, который содержит результат запроса. Этот список можно использовать в программе без повторного считывания одних и тех же записей из базы данных. При вызове метода ToList выполняется запрос LINQ и возвращается список результатов.

    Получение данных из нескольких таблиц в одном запросе LINQ

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

    var allOrders = from Customer c in newDB.CustomerTable
                      join Order o in newDB.OrderTable
                        on c.CustomerID equals o.OrderCustomerID
                     select new { c.Name, o.OrderDescription };
    

    Этот запрос LINQ содержит ключевые слова from, join и select, которые встроены в язык C#. Первая часть запроса находит всех клиентов в таблице Customer. Затем к результату запроса присоединяются заказы из таблицы Order в соответствии с условием равенства значений идентификаторов клиента в этих таблицах. После этого создается новый тип данных, который содержит результаты, указанные в секции select. В нашем случае будет создан список объектов, содержащих информацию об имени клиента и описании заказа.

    На основе результатов этого запроса можно сформировать список заказов клиентов:

    List<String> OrderDescriptions = new List<String>();
    
    foreach (var orderDesc in allOrders)
    {
        OrderDescriptions.Add(orderDesc.Name + " заказал: " + orderDesc.OrderDescription);
    }
    

    Как и в предыдущем примере, результаты считываются из базы данных непосредственно перед их использованием в цикле. Ключевое слово var указывает на то, что тип переменной orderDesc будет определен при присвоении ей значения. Для этой переменной нельзя указать конкретный тип, поскольку он создается LINQ при выполнении запроса и будет известен только во время выполнения этого запроса.

    Удаление элементов из базы данных

    При необходимости можно удалить ненужные объекты из базы данных. Например, можно удалить ненужный заказ, используя метод DeleteOnSubmit:

    ActiveDB.OrderItemTable.DeleteOnSubmit(item);
    

    Также есть метод DeleteAllOnSubmit, который может удалить заданную коллекцию объектов.

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

    Краткие итоги

  • Windows Phone предоставляет систему изолированного хранилища, в котором каждое приложение в телефоне может хранить свои данные.
  • Программы могут создавать папки и файлы в своей изолированной области хранилища и использовать для взаимодействия с ними стандартные конструкции ввода/вывода, основанные на потоках.
  • Альтернативным метод хранения данных является использование специализированного словаря для хранения в изолированном хранилище пар "имя—значение".
  • Инструмент Isolated Storage Explorer позволяет программистам просматривать содержимое изолированного хранилища разрабатываемого приложения.
  • Приложения Windows Phone могут использовать интегрированный язык запросов (LINQ) для взаимодействия с базами данных, которые могут храниться в изолированном хранилище в виде файлов. Этими базами данных управляет сервер баз данных SQL, но нельзя использовать SQL-запросы для взаимодействия с базой данных — для этого используется LINQ.
  • База данных обычно состоит из нескольких таблиц. Таблицы состоят из столбцов (различные элементы данных, такие как имя, адрес, банковские реквизиты) и строк (вся информация об одном объекте — имя, адрес, банковские реквизиты одного клиента).
  • Один из столбцов таблицы должен быть первичным ключом, значения которого уникальны для каждой строки таблицы. Каждое из этих значения однозначно идентифицирует запись таблицы. Можно указать базе данных, что нужно автоматически создавать первичный ключ для таблицы.
  • В приложениях Windows Phone можно создавать классы, которые использует LINQ для взаимодействия с таблицами базы данных. Для этого к описанию класса добавляются атрибуты [Table] и [Column]. Также могут использоваться дополнительные параметры для определения первичных ключей.
  • Свойства классов, которыми управляет LINQ, могут содержать код для создания уведомлений, чтобы LINQ автоматически обновлял записи базы данных при изменении значений этих свойств.
  • База данных может содержать отношения, в которых столбец в одной таблице содержит значения первичного ключа другой таблицы. Такие отношения являются внешними ключами.
  • В LINQ отношения реализуются с помощью класса EntityRef, который содержит ссылку на внешний ключ в другой базе данных. Класс EntitySet используется для того чтобы свойство одного объекта, связанного с таблицей базы данных, могло ссылаться на несколько записей другой таблицы. К этим элементам добавляется атрибут [Association], который описывает отношения между таблицами.
  • Запрос LINQ можно использовать для получения структурированных данных из базы данных. Запрос считывает записи из базы данных непосредственно перед их использованием в программе. Запросы LINQ могут комбинировать результаты нескольких запросов.
  • При удалении объектов из базы данных все дочерние объекты должны быть удалены перед удалением родительского объекта — иначе в таблицах базе данных могли бы существовать записи, содержащей значения первичного ключа для несуществующих записей из другой таблицы.
  • Вопросы

  • Где приложение может сохранять данные?
  • Каким образом программа может сохранять файлы?
  • Как программа может сохранить настройки, которые можно представить в виде пар "имя—значение"?
  • Для чего предназначена программа Isolated Storage Explorer?
  • Как можно создать базу данных в приложении для Windows Phone?
  • Как связать результаты запроса LINQ с визуальными элементами Silverlight?
  • Как с помощью LINQ задать связи между таблицами?
  • Как выполняются запросы LINQ?
  • Как выполняется добавление и удаление записей таблиц в LINQ?
  • Упражнения

    Упражнение 1. Использование изолированного хранилища

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

    Загрузка сохраненных данных

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

  • Откройте в Visual Studio проект CustomerManager в папке Lab5 Customer Time Logger Storage. Этот проект содержит версию программы, в которой обнаружена ошибка.
  • Убедитесь в том, что эмулятор Windows Phone не запущен. Укажите эмулятор Windows Phone в качестве целевой платформы.
  • Запустите программу. При первом запуске программа создаст новый набор тестовых данных.
  • Выберите клиента и измените информацию о нем.
  • Нажмите кнопку сохранить. Обратите внимание на то, что содержимое экрана изменилось, поскольку в программе используется привязка данных для отображения обновленных значений.
  • Остановите программу, нажав в эмуляторе Windows Phone кнопку Назад. Не останавливайте выполнение программы в Visual Studio.
  • Запустите программу еще раз. При повторном запуске программа должна загрузить список клиентов из изолированного хранилища.
  • Найдите клиента, информацию о котором вы изменили. Обратите внимание, что сделанные изменения не сохранились.
  • Список клиентов загружается в файле App.xaml.cs. Откройте этот файл в обозревателе решений Visual Studio и найдите конструктор App. В конце этого метода находится код, который должен загружать данные из изолированного хранилища. Попробуйте определить причину возникающей проблемы.
  • Конструктор содержит только код, который создает тестовый список при каждом запуске программы:
    ActiveCustomerList = Customers.MakeTestCustomers();
    
  • Необходимо изменить этот код, чтобы информация о клиентах загружалась из файла. Откройте в файле App.xaml.cs область Customer Manager Values и найдите методы LoadCustomers и SaveCustomers.
  • Метод LoadCustomers возвращает список клиентов или значение null, если список не может быть считан. Если метод возвращает null, программа должна создать набор тестовых данных. Замените указанный в шаге 10 код на следующий:
    // загрузить список клиентов из файла
    ActiveCustomerList = LoadCustomers(ListFilename);
    
    // если загрузить список не удалось, создать тестовые данные
    if (ActiveCustomerList == null)
        ActiveCustomerList = Customers.MakeTestCustomers();
    
  • Запустите программу. Измените информацию о любом клиенте.
  • Остановите программу, нажав в эмуляторе Windows Phone кнопку Назад. Не останавливайте выполнение программы в Visual Studio.
  • Запустите программу еще раз. Убедитесь в том, что теперь сделанные изменения были сохранены.
  • Пропадание информации о встрече

    Тестировщик нашел в программе еще одну проблему: программа некорректно сохраняет информацию о встрече. Необходимо исследовать эту проблему.

    Программа создает объекты для работы с потоками StreamReader и StreamWriter для загрузки и сохранения элементов в изолированном хранилище. Каждый объект может сохранить свое состояние в поток и загрузить информацию из потока. Ниже приведен код методов для сохранения информации в поток и для загрузки информации из потока:

    public void SaveToStream(StreamWriter output)
    {
        output.WriteLine(CustomerList.Count);
    
        foreach (Customer c in CustomerList)
        {
            c.SaveToStream(output);
        }
    }
    
    public Customers(StreamReader input)
    {
        CustomerList = new List<Customer>();
    
        int noOfCustomers = int.Parse(input.ReadLine());
    
        for (int i = 0; i < noOfCustomers; i++)
        {
            CustomerList.Add(new Customer(input));
        }
    }
    

    Этот код выглядит правильно. Необходимо проверить класс Customer, правильно ли он использует эти методы.

  • Вернитесь в программу и откройте файл Customers.cs.
  • Найдите класс Customers и просмотрите метод SaveToStream:
    public void SaveToStream(StreamWriter output)
    {
        output.WriteLine(Name);
        output.WriteLine(Address);
        output.WriteLine(ID);
    }
    
  • Этот метод не содержит код для сохранения информации о встрече. Аналогичная проблема возникает и в методе Customer:
    public Customer(StreamReader input)
    {
        Name = input.ReadLine();
        Address = input.ReadLine();
        ID = int.Parse(input.ReadLine());
        int noOfSessions = int.Parse(input.ReadLine());
    }
    
  • Необходимо использовать тот же шаблон для загрузки и сохранения списка встреч, который использует объект CustomerList для хранения информации о клиентах. Измените методы для сохранения и загрузки, чтобы можно было сохранять информацию о нескольких встречах:
    public void SaveToStream(StreamWriter output)
    {
        output.WriteLine(Name);
        output.WriteLine(Address);
        output.WriteLine(ID);
        output.WriteLine(Sessions.Count);
        foreach (Session session in Sessions)
        {
            session.SaveToStream(output);
        }
    }
    
    public Customer(StreamReader input)
    {
        Name = input.ReadLine();
        Address = input.ReadLine();
        ID = int.Parse(input.ReadLine());
        int noOfSessions = int.Parse(input.ReadLine());
        for (int i = 0; i < noOfSessions; i++)
        {
            Sessions.Add(new Session(input));
        }
    }
    
  • Остановите эмулятор Windows Phone, чтобы при следующем запуске программы она создала новый набор тестовых данных. Запустите программу еще раз.
  • Остановите программу, нажав в эмуляторе Windows Phone кнопку Назад. Не останавливайте выполнение программы в Visual Studio.
  • Запустите программу еще раз.
  • Выберите любого клиента и просмотрите информацию о встречах, которая должна была быть сохранена.
  • Упражнение 2. Использование базы данных

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

  • Откройте в Visual Studio проект CustomerManager в папке Lab5 Customer Time Logger Database. Этот проект содержит версию программы, в которой обнаружена ошибка.
  • Убедитесь в том, что эмулятор Windows Phone не запущен.
  • Запустите программу. Программа сгенерирует тестовые данные и попытается их использовать. После этого будет сгенерировано исключение, в котором база данных сообщает, что проблема связана с внешним ключом. Это касается связей между классами Session и Customer. Сообщение об ошибке информирует о том, что невозможно вставить внешний ключ, так как не существует соответствующий первичный ключ.

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

  • добавить встречу к списку встреч клиента;
  • установить значение customerID для встречи, чтобы идентифицировать клиента для этой встречи.
  • Код, который выполняет эти действия, выглядит следующим образом:

    int sessionLength = sessionRand.Next(5,120);
    string sessionDesc = "Встреча " + i.ToString();
    Session newSession = new Session();
    newSession.Description = sessionDesc;
    newSession.LengthInMins = sessionLength;
    newSession.SessionCustomer = c;
    newDB.SessionTable.InsertOnSubmit(newSession);
    c.Sessions.Add(newSession);
    
  • Добавьте этот код в программу и запустите ее повторно. Теперь программа работает правильно и позволяет управлять данными.
  • Тестирование хранения данных

    В настоящее время при каждом запуске программа создает новую базу данных. Измените программу так, чтобы она использовала существующую базу данных и создавала новую, если база данных не существует. Обратите внимание, что новая база данных создается при выполнении следующей строки кода в файле App.xaml.cs:

    CustomerDB.MakeTestDB("Data Source=isostore:/Sample.sdf");
    
    Вернуться к учебному плану