Хранение данных приложений
Презентацию к данной лекции Вы можете скачать здесь.
14.1. Хранилище данных Windows Phone
Практически в любом приложении требуется хранить данные, которые приложение использует при работе. Приложение Silverlight должно сохранять настройки и информацию, с которой работает пользователь, игре XNA требуется хранить информацию о достижениях игрока, список рекордов и игровые настройки. Программы Windows Phone могут использовать изолированное хранилище для хранения подобной информации. Хранилище называют "изолированным", потому что приложение не может получить доступ к данным, которые сохраняет другое приложение, в отличие от компьютеров под управлением Windows, в которых любая программа может получить доступ к любому файлу в системе.
В изолированном хранилище можно сохранять большие объемы данных вплоть до максимально доступного объема памяти для хранения данных в телефоне. В устройствах Windows Phone имеется минимум 8 Гб встроенного хранилища, которое совместно используется программами для работы с мультимедиа файлами (музыка, изображения и видео) и всеми приложениями в устройстве.
При удалении приложения из телефона все содержимое изолированного хранилища, связанное с этим приложением, также удаляется. Когда приложение обновляется до новой версии через Marketplace, содержимое изолированного хранилища сохраняется, а если данные в хранилище должны быть обновлены до новой версии, приложение их обновляет.
Использование файловой системы изолированного хранилища
Изолированное хранилище можно использовать точно так же, как и файловую систему. Единственная разница заключается в способе подключения приложения к хранилищу. Однако, после того как программа подключится к файловой системе, она может создавать потоки для передачи данных в файлы и даже использовать папки для создания структурированного файлового хранилища. Так программа может сохранять большие объемы данных.
В качестве примера рассмотрим создание простого приложения Silverlight, которое может сохранять текстовые заметки. Эта версия программы использует один файл, но ее можно легко расширить. Внешний вид приложения представлен на рис. 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) содержит информацию о клиентах и включает четыре столбца для хранения, соответственно, идентификатора, имени, адреса и названия банка клиента. Каждая строка таблицы называется записью и хранит информацию об одном клиенте.
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) содержит столбцы для хранения информации товарах: идентификатор товара, наименование, поставщик и цена.
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) содержит информацию о заказах товаров и ссылается на записи двух других таблиц. Столбцы таблицы содержат идентификатор заказа, идентификаторы клиента и купленного товара, количество товара, дату заказа и его статус.
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". Таким образом, можно выбрать из базы данных нужные элементы. Так, в приложение можно добавить поле для поиска, если нужно найти клиентов с определенным именем.