Статьи

Windows Phone 8: кратко: доступ к данным — хранение

Интернет играет важную роль в мобильных приложениях. Большинство приложений Windows Phone, доступных в Магазине, используют сетевое соединение, предлагаемое каждым устройством. Однако полагаться только на сетевое соединение может быть ошибкой; пользователи могут оказаться в ситуациях, когда соединение недоступно. Кроме того, планы передачи данных часто ограничены, поэтому чем меньше сетевых операций мы выполняем, тем лучше для пользователя.

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

Локальное хранилище — это одна из функций, которая предлагает дублированные API — старые Silverlight, основанные на классе IsolatedStorageFile и новые Windows Runtime, основанные на классе LocalFolder . Как упоминалось в начале серии, мы сосредоточимся на API-интерфейсах Windows Runtime.

Базовый класс, который идентифицирует папку в локальном хранилище, называется StorageFolder . Даже корень хранилища (доступ к которому можно получить с помощью класса ApplicationData.Current.LocalStorage который является частью пространства имен Windows.Storage ) является объектом StorageFolder .

Этот класс предоставляет различные асинхронные методы для взаимодействия с текущей папкой, такие как:

  • CreateFolderAsync() для создания новой папки в текущем пути.
  • GetFolderAsync() чтобы получить ссылку на подпапку текущего пути.
  • GetFoldersAsync() чтобы получить список папок, доступных по текущему пути.
  • DeleteAsync() чтобы удалить текущую папку.
  • RenameAsync() чтобы переименовать папку.

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

1
2
3
4
5
6
private async void OnCreateFolderClicked(object sender, RoutedEventArgs e)
{
    await
 
    ApplicationData.Current.LocalFolder.CreateFolderAsync(“myFolder”);
}

К сожалению, API не имеют метода, чтобы проверить, существует ли папка. Самое простое решение — попытаться открыть папку с помощью GetFolderAsync() и перехватить ошибку FileNotFoundException которая возникает, если папка не существует, как показано в следующем примере:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
private async void OnOpenFileClicked(object sender, RoutedEventArgs e)
{
    StorageFolder folder;
    try
    {
        folder = await ApplicationData.Current.LocalFolder.GetFolderAsync(“myFolder”);
    }
    catch (FileNotFoundException exc)
    {
        folder = null;
    }
             
    if (folder == null)
    {
        MessageBox.Show(“The folder doesn’t exist”);
    }
}

Вместо этого файлы идентифицируются классом StorageFile , который также предлагает методы взаимодействия с файлами:

  • DeleteAsync() чтобы удалить файл.
  • RenameAsync() чтобы переименовать файл.
  • CopyAsync() для копирования файла из одного места в другое.
  • MoveAsync() для перемещения файла из одного места в другое.

Отправной точкой для манипулирования файлом является класс StorageFolder мы ранее обсуждали, поскольку он предлагает методы для открытия существующего файла ( GetFileAsync() ) или создания нового в текущей папке ( CreateFileAsync() ).

Давайте рассмотрим две наиболее распространенные операции: запись содержимого в файл и чтение содержимого из файла.

Как уже упоминалось, первым шагом для создания файла является использование метода CreateFile() для объекта StorageFolder . В следующем примере показано, как создать новый файл с именем file.txt в file.txt локального хранилища:

1
2
3
4
private async void OnCreateFileClicked(object sender, RoutedEventArgs e)
{
    StorageFile file = await ApplicationData.Current.LocalFolder.CreateFileAsync(“file.txt”, CreationCollisionOption.ReplaceExisting);
}

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

Теперь, когда у вас есть ссылка на файл благодаря объекту StorageFile , вы можете работать с ним с помощью OpenAsync() . Этот метод возвращает файловый поток, который вы можете использовать для записи и чтения содержимого.

В следующем примере показано, как написать текст внутри файла:

01
02
03
04
05
06
07
08
09
10
private async void OnCreateFileClicked(object sender, RoutedEventArgs e)
{
    StorageFile file = await ApplicationData.Current.LocalFolder.CreateFileAsync(“file.txt”, CreationCollisionOption.ReplaceExisting);
    IRandomAccessStream randomAccessStream = await file.OpenAsync(FileAccessMode.ReadWrite);
 
    using (DataWriter writer = new DataWriter(randomAccessStream.GetOutputStreamAt(0)))
    {
        writer.WriteString(“Sample text”);
        await writer.StoreAsync();
}

Ключом является класс DataWriter , который представляет собой класс среды выполнения Windows, который можно использовать для простой записи данных в файл. Нам просто нужно создать новый объект DataWriter , передав в качестве параметра поток вывода файла, который мы получаем с помощью GetOuputStreamAt() в потоке, возвращаемом методом OpenAsync() .

Класс DataWriter предлагает множество методов для записи различных типов данных, таких как WriteDouble() для десятичных чисел, WriteDateTime() для дат и WriteBytes() для двоичных данных. В этом примере мы пишем текст с использованием WriteString() , а затем вызываем StoreAsync() и FlushAsync() для завершения операции записи.

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

Операция чтения файла не очень отличается от записи. В этом случае нам также нужно получить поток файлов с помощью метода OpenFile() . Разница в том, что вместо использования класса DataWriter мы будем использовать класс DataReader , который выполняет противоположную операцию. Посмотрите на следующий пример кода:

01
02
03
04
05
06
07
08
09
10
11
12
private async void OnReadFileClicked(object sender, RoutedEventArgs e)
{
    StorageFile file = await ApplicationData.Current.LocalFolder.GetFileAsync(«file.txt»);
    IRandomAccessStream randomAccessStream = await file.OpenAsync(FileAccessMode.Read);
 
    using (DataReader reader = new DataReader(randomAccessStream.GetInputStreamAt(0)))
    {
        uint bytesLoaded = await reader.LoadAsync((uint) randomAccessStream.Size);
        string readString = reader.ReadString(bytesLoaded);
        MessageBox.Show(readString);
    }
}

В этом случае вместо метода CreateFileAsync() мы используем метод GetFileAsync() , который можно использовать для получения ссылки на уже существующий файл. Затем мы запускаем процедуру чтения с использованием класса DataReader , на этот раз с использованием входного потока, который мы получаем с помощью GetInputStreamAt() .

Как и класс DataWriter , DataReader также предлагает множество методов для чтения различных типов данных, таких как ReadDouble() , ReadDateTime() и ReadBytes() . В этом случае мы читаем текст, который мы ранее написали, используя метод ReadString() , который требует размер файла в качестве его параметра.

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

Среда выполнения Windows предлагает API для предоставления доступа к папке, в которой установлено приложение и куда копируются все файлы, являющиеся частью вашего проекта Visual Studio. Он называется Package.Current.InstalledLocation и является частью пространства имен Windows.ApplicationModel .

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

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

1
2
3
4
5
private async void OnCopyFileClicked(object sender, RoutedEventArgs e)
{
    StorageFile file = await Package.Current.InstalledLocation.GetFileAsync(«file.xml»);
    await file.CopyAsync(ApplicationData.Current.LocalFolder);
}

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

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

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

Примечание. Класс IsolatedStorageSettings является частью старых API хранилища; Среда выполнения Windows предлагает новый API для управления настройками, но, к сожалению, он недоступен в Windows Phone.

Использовать свойство ApplicationSettings очень просто: его тип — Dictionary<string, object> и его можно использовать для хранения любого объекта.

В следующем примере вы видите два обработчика событий: первый сохраняет объект в настройках, а второй получает его.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
private void OnSaveSettingsClicked(object sender, RoutedEventArgs e)
{
    IsolatedStorageSettings settings = IsolatedStorageSettings.ApplicationSettings;
    settings.Add(«name», «Matteo»);
    settings.Save();
}
 
private void OnReadSettingsClicked(object sender, RoutedEventArgs e)
{
    IsolatedStorageSettings settings = IsolatedStorageSettings.ApplicationSettings;
    if (settings.Contains(«name»))
    {
        MessageBox.Show(settings[«name»].ToString());
    }
}

Единственное, что нужно выделить, — это метод Save() , который необходимо вызывать каждый раз, когда вы хотите сохранить сделанные вами изменения. За исключением этого, он работает как обычная коллекция Dictionary .

Примечание. Настройки скрыты в файле XML. API автоматически обеспечивает сериализацию и десериализацию сохраняемого объекта. Подробнее о сериализации мы поговорим позже в этой статье.

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

Лучший способ просмотреть локальное хранилище приложения — использовать сторонний инструмент, доступный в CodePlex под названием Windows Phone Power Tools , который предлагает визуальный интерфейс для исследования локального хранилища приложения.

Инструмент прост в использовании. После установки вы сможете подключиться к устройству или к одному из доступных эмуляторов. Затем в разделе « Изолированное хранилище » вы увидите список всех приложений, которые были загружены из Visual Studio. Каждый будет идентифицирован своим идентификатором приложения (который является GUID). Как обычный файловый менеджер, вы можете расширить древовидную структуру и проанализировать содержимое хранилища. Вы сможете сохранять файлы с устройства на ПК, копировать файлы с ПК в хранилище приложений и даже удалять объекты.

Локальное хранилище приложения Windows Phone

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

Сериализация — это самый простой способ хранения данных приложения в локальном хранилище. Это процесс, который преобразует сложные объекты в простой текст, чтобы их можно было сохранить в текстовом файле, используя XML или JSON в качестве вывода. Десериализация — это противоположный процесс; простой текст преобразуется обратно в объекты, чтобы они могли использоваться приложением.

В приложении Windows Phone, которое использует эти методы, сериализация обычно применяется каждый раз, когда данные приложения изменяются (когда добавляется, редактируется или удаляется новый элемент), чтобы минимизировать риск потери данных в случае чего-либо, например, неожиданного сбоя или подвеска. Вместо этого десериализация обычно применяется при первом запуске приложения.

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

В следующих примерах мы будем использовать тот же класс Person мы использовали ранее в этой серии.

1
2
3
4
5
public class Person
{
    public string Name { get;
    public string Surname { get;
}

Мы предполагаем, что у вас будет коллекция объектов Person , которая представляет ваши локальные данные:

01
02
03
04
05
06
07
08
09
10
11
12
13
List<Person> people = new List<Person>
                       {
                           new Person
                               {
                                   Name = «Matteo»,
                                   Surname = «Pagani»
                               },
                           new Person
                               {
                                   Name = «John»,
                                   Surname = «Doe»
                               }
                       };

Для сериализации данных нашего приложения мы будем использовать API локального хранилища, о которых мы узнали в предыдущем разделе. Мы снова будем использовать метод CreateFile() , как показано в следующем примере:

01
02
03
04
05
06
07
08
09
10
11
12
13
private async void OnSerializeClicked(object sender, RoutedEventArgs e)
{
    DataContractSerializer serializer = new DataContractSerializer(typeof(List<Person>));
 
    StorageFile file = await ApplicationData.Current.LocalFolder.CreateFileAsync(«people.xml»);
    IRandomAccessStream randomAccessStream = await file.OpenAsync(FileAccessMode.ReadWrite);
 
    using (Stream stream = randomAccessStream.AsStreamForWrite())
    {
        serializer.WriteObject(stream, people);
        await stream.FlushAsync();
    }
}

Класс DataContractSerializer (который является частью пространства имен System.Runtime.Serialization ) отвечает за управление процессом сериализации. Когда мы создаем новый экземпляр, нам нужно указать, какой тип данных мы собираемся сериализовать (в предыдущем примере это List<Person> ). Затем мы создаем новый файл в локальном хранилище и получаем поток, необходимый для записи данных. Операция сериализации выполняется путем вызова WriteObject() класса DataContractSerializer , который требует в качестве параметров местоположение потока, в котором записываются данные, и объект для сериализации. В этом примере это коллекция объектов Person мы ранее определили.

Если вы посмотрите на содержимое хранилища с помощью Windows Phone Power Tools, вы найдете файл people.xml , который содержит XML-представление ваших данных:

01
02
03
04
05
06
07
08
09
10
<ArrayOfPerson xmlns:i=»http://www.w3.org/2001/XMLSchema-instance» xmlns=»http://schemas.datacontract.org/2004/07/Storage.Classes»>
  <Person>
    <Name>Matteo</Name>
    <Surname>Pagani</Surname>
  </Person>
  <Person>
    <Name>John</Name>
    <Surname>Doe</Surname>
  </Person>
</ArrayOfPerson>

Совет: класс DataContractSerializer использует XML в качестве выходного формата. Если вы хотите использовать вместо этого JSON, вам придется использовать класс DataContractJsonSerializer , который работает таким же образом.

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

01
02
03
04
05
06
07
08
09
10
11
12
private async void OnDeserializeClicked(object sender, RoutedEventArgs e)
{
    StorageFile file = await ApplicationData.Current.LocalFolder.GetFileAsync(«people.xml»);
    DataContractSerializer serializer = new DataContractSerializer(typeof(List<Person>));
 
    IRandomAccessStream randomAccessStream = await file.OpenAsync(FileAccessMode.Read);
 
    using (Stream stream = randomAccessStream.AsStreamForRead())
    {
        List<Person> people = serializer.ReadObject(stream) as List<Person>;
    }
}

Единственные различия:

  • Мы получаем поток для чтения с помощью AsStreamForRead() .
  • Мы используем метод ReadObject() класса DataContractSerializer для десериализации содержимого файла, которое принимает поток файла в качестве входного параметра. Важно отметить, что метод всегда возвращает универсальный объект, поэтому вам всегда придется приводить его к реальному типу данных (в примере мы приводим его как List<Person> ).

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

SQL CE — это решение для базы данных, которое было представлено в Windows Phone 7.5. Это отдельная база данных, что означает, что данные хранятся в одном файле в хранилище без необходимости использования СУБД для управления всеми операциями.

Windows Phone использует SQL CE 3.5 (последний выпуск на данный момент — 4.0, но он не поддерживается) и не поддерживает выполнение SQL-запросов. Каждая операция выполняется с использованием LINQ to SQL, который является одним из первых решений Microsoft для ORM.

Примечание. ORM (Object-Relation Mapping) — это библиотеки, которые могут автоматически преобразовывать операции с объектами (вставка, редактирование, удаление) в операции базы данных. Таким образом, вы можете продолжать работать над своим проектом, используя объектно-ориентированный подход. ORM позаботится о написании необходимых SQL-запросов для хранения ваших данных в базе данных.

Подход, используемый SQL CE в Windows Phone, сначала называется кодом . База данных создается при первой необходимости данных в соответствии с определением сущностей, которое вы собираетесь хранить в таблицах. Другое решение — включить уже существующий файл SQL CE в проект Visual Studio. В этом случае вы сможете работать с ним только в режиме только для чтения.

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

Определение сущности выполняется с использованием атрибутов, которые являются частью System.Data.Linq.Mapping имен System.Data.Linq.Mapping . Каждое свойство украшено атрибутом, который будет использоваться для перевода его в столбец. В следующем примере мы адаптируем знакомый класс Person для хранения в таблице:

01
02
03
04
05
06
07
08
09
10
11
12
[Table]
public class Person
{
    [Column(IsPrimaryKey = true, CanBeNull = false, IsDbGenerated = true)]
    public string Id { get;
 
    [Column]
    public string Name { get;
 
    [Column]
    public string Surname { get;
}

Вся сущность помечается атрибутом Table , а каждое свойство помечается атрибутом Column . Атрибуты могут быть настроены с некоторыми свойствами, такими как:

  • IsPrimaryKey для применения к столбцам, которые являются частью первичного ключа.
  • IsDbGenerated в случае, если значение столбца необходимо генерировать автоматически каждый раз, когда вставляется новая строка (например, автоматически увеличиваемое число).
  • Name если вы хотите присвоить столбцу другое имя, чем свойство.
  • DbType для настройки типа столбца. По умолчанию тип столбца автоматически устанавливается типом свойства.

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

Класс DataContext содержит определение строки подключения (которая является путем хранения базы данных) и все таблицы, включенные в базу данных. В следующем примере вы можете увидеть определение DataContext которое включает таблицу Person мы ранее определили:

01
02
03
04
05
06
07
08
09
10
11
public class DatabaseContext: DataContext
{
    public static string ConnectionString = «Data source=isostore:/Persons.sdf»;
 
    public DatabaseContext(string connectionString):base(connectionString)
    {
             
    }
 
    public Table<Person> Persons;
}

Отдельный класс вашего проекта наследуется от класса DataContext . Это заставит вас реализовать открытый конструктор, который поддерживает строку подключения в качестве входного параметра. Существует два типа строки подключения, основанные на следующих префиксах:

  • isostore:/ означает, что файл хранится в локальном хранилище. В предыдущем примере имя файла базы данных было Persons.sdf и оно хранится в корне хранилища.
  • appdata:/ означает, что файл хранится в проекте Visual Studio. В этом случае вы должны установить File Mode атрибута « File Mode значение « Read Only .
1
public static string ConnectionString = «Data source=appdata:/Persons.sdf; File Mode=Read Only»;

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

1
public static string ConnectionString = «Data source=isostore:/Persons.sdf; Password=’password'»;

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

  • DatabaseExists() возвращает, существует ли база данных уже.
  • CreateDatabase() эффективно создает базу данных в хранилище.

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

01
02
03
04
05
06
07
08
09
10
private void OnCreateDatabaseClicked(object sender, RoutedEventArgs e)
{
    using (DatabaseContext db = new DatabaseContext(DatabaseContext.ConnectionString))
    {
        if (!db.DatabaseExists())
        {
            db.CreateDatabase();
        }
    }
}

Все операции выполняются с использованием объекта Table<T> который мы объявили в определении DataContext . Он поддерживает стандартные операции LINQ, поэтому вы можете запрашивать данные с помощью таких методов, как Where() , FirstOrDefault() , Select() и OrderBy() .

В следующем примере вы можете увидеть, как мы получаем все объекты Person в таблице, имя которой Matteo:

1
2
3
4
5
6
7
private void OnShowClicked(object sender, RoutedEventArgs e)
{
    using (DatabaseContext db = new DatabaseContext(DatabaseContext.ConnectionString))
    {
        List<Person> persons = db.Persons.Where(x => x.Name == «Matteo»).ToList();
    }
}

Возвращенный результат можно использовать не только для отображения, но и для редактирования. Чтобы обновить элемент в базе данных, вы можете изменить значения возвращаемого объекта, вызвав метод SubmitChanges() , предоставляемый классом DataContext .

Чтобы добавить новые элементы в таблицу, класс Table<T> предлагает два метода: InsertOnSubmit() и InsertAllOnSubmit() . Первый метод может использоваться для вставки одного объекта, а второй добавляет несколько элементов за одну операцию (фактически он принимает коллекцию в качестве параметра).

01
02
03
04
05
06
07
08
09
10
11
12
13
private void OnAddClicked(object sender, RoutedEventArgs e)
{
    using (DatabaseContext db = new DatabaseContext(DatabaseContext.ConnectionString))
    {
        Person person = new Person
        {
            Name = «Matteo»,
            Surname = «Pagani»
        };
        db.Persons.InsertOnSubmit(person);
        db.SubmitChanges();
    }
}

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

Аналогичным образом вы можете удалять элементы, используя методы DeleteOnSubmit() и DeleteAllOnSubmit() . В следующем примере мы удаляем всех людей с именем Matteo:

1
2
3
4
5
6
7
8
9
private void OnDeleteClicked(object sender, RoutedEventArgs e)
{
    using (DatabaseContext db = new DatabaseContext(DatabaseContext.ConnectionString))
    {
        List<Person> persons = db.Persons.Where(x => x.Name == «Matteo»).ToList();
        db.Persons.DeleteAllOnSubmit(persons);
        db.SubmitChanges();
    }
}

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

С LINQ to SQL мы сможем:

  • Добавьте свойство Person в сущность Order которой будет храниться ссылка на пользователя, который сделал заказ.
  • Добавьте коллекцию Orders в сущность Person которая будет содержать все заказы, сделанные пользователем.

Это достигается с помощью внешнего ключа , который является свойством, объявленным в сущности « Order который будет содержать значение первичного ключа пользователя, который сделал заказ.

Вот как выглядит класс Order :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
[Table]
public class Order
{
    [Column(IsPrimaryKey = true)]
    public int OrderCode
    {
        get;
        set;
    }
 
    [Column]
    public double TotalPrice
    {
        get;
        set;
    }
 
    [Column]
    public string ShippingAddress
    {
        get;
        set;
    }
 
    [Column]
    public int PersonId
    {
        get;
        set;
    }
 
    private EntityRef<Person> _Person;
 
    [Association(Name = «PersonOrders»,
        Storage = «_Person»,
        ThisKey = «PersonId»,
        OtherKey = «PersonId»,
        IsForeignKey = true)]
    public Person Person
    {
        get
        {
            return this._Person.Entity;
        }
        set
        {
            Person previousValue = this._Person.Entity;
            if (((previousValue != value) || (this._Person.HasLoadedOrAssignedValue == false)))
            {
                if ((previousValue != null))
                {
                    this._Person.Entity = null;
                    previousValue.Orders.Remove(this);
                }
                this._Person.Entity = value;
                if ((value != null))
                {
                    value.Orders.Add(this);
                    this.PersonId = value.Id;
                }
                else
                {
                    this.PersonId = default(int);
                }
            }
        }
    }
}

В определении класса есть два ключевых свойства:

  • PersonId — это внешний ключ, который просто содержит идентификатор человека.
  • Person — это реальный объект Person который благодаря атрибуту Association может содержать ссылку на пользователя, который сделал заказ. Установщик свойства содержит некоторую логику для управления добавлением нового значения или удалением уже существующего.

Конечно, мы должны также изменить определение класса Person для управления отношениями:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
[Table]
public class Person
{
    public Person()
    {
        _Orders = new EntitySet<Order>();
    }
    [Column(IsPrimaryKey = true, CanBeNull = false, IsDbGenerated = true)]
    public int Id { get;
 
    [Column]
    public string Name { get;
 
    [Column]
    public string Surname { get;
 
    private EntitySet<Order> _Orders;
 
    [Association(Name = «PersonOrders»,
    Storage = «_Orders»,
    ThisKey = «PersonId»,
    OtherKey = «PersonId»,
    DeleteRule = «NO ACTION»)]
    public EntitySet<Order> Orders
    {
        get
        {
            return this._Orders;
        }
        set
        {
            this._Orders.Assign(value);
        }
    }
}

Также в этом классе мы определили новое свойство с именем Orders , типом которого является EntitySet<T> , где T — это тип другой таблицы, участвующей в отношении. Благодаря атрибуту Association мы можем получить доступ ко всем заказам, сделанным пользователем, просто запросив коллекцию Orders .

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
private void OnAddClicked(object sender, RoutedEventArgs e)
{
    using (DatabaseContext db = new DatabaseContext(DatabaseContext.ConnectionString))
    {
        Person person = new Person
        {
            Name = «Matteo»,
            Surname = «Pagani»,
        };
 
        Order order = new Order
        {
            TotalPrice = 55,
            ShippingAddress
                = «Fake Street, Milan»,
            Person = person
        };
 
        db.Orders.InsertOnSubmit(order);
        db.SubmitChanges();
    }
}
 
private void OnQueryClicked(object sender, RoutedEventArgs e)
{
    using (DatabaseContext db = new DatabaseContext(DatabaseContext.ConnectionString))
    {
        Order result = db.Orders.FirstOrDefault(x => x.OrderCode == 1);
        MessageBox.Show(result.Person.Name);
    }
}

Поскольку Person является свойством класса Order , достаточно создать новый заказ и установить объект, представляющий пользователя, который сделал заказ, в качестве значения свойства Person .

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

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

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

Примечание. Цель DatabaseSchemaUpdater — просто обновить схему уже существующей базы данных. Вам все еще нужно обновить свои сущности и определение DataContext, чтобы отразить новые изменения.

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

После того, как вы изменили свои сущности или определение DataContext в своем проекте, вы можете использовать следующие методы:

  • AddTable<T>() если вы добавили новую таблицу (типа T ).
  • AddColumn<T>() если вы добавили новый столбец в таблицу (типа T ).
  • AddAssociation<T>() если вы добавили новую связь в таблицу (типа T ).

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private void OnUpdateDatabaseClicked(object sender, RoutedEventArgs e)
{
    using (DatabaseContext db = new DatabaseContext(DatabaseContext.ConnectionString))
    {
        if (!db.DatabaseExists())
        {
            db.CreateDatabase();
            DatabaseSchemaUpdater updater = db.CreateDatabaseSchemaUpdater();
            updater.DatabaseSchemaVersion = 2;
            updater.Execute();
        }
        else
        {
            DatabaseSchemaUpdater updater = db.CreateDatabaseSchemaUpdater();
            if (updater.DatabaseSchemaVersion < 2)
            {
                updater.AddColumn<Person>(«BirthDate»);
                updater.DatabaseSchemaVersion = 2;
                updater.Execute();
            }
        }
    }
}

Мы предполагаем, что текущая версия схемы базы данных равна 2. Если база данных не существует, мы просто создаем ее и, используя класс DatabaseSchemaUpdater , обновляем свойство DatabaseSchemaVersion . Таким образом, в следующий раз, когда понадобятся данные, операция обновления не будет выполнена, так как мы уже работаем с последней версией.

Вместо этого, если база данных уже существует, мы проверяем номер версии. Если это более старая версия, мы обновляем текущую схему. В предыдущем примере мы добавили новый столбец в таблицу Person именем BirthDate (это параметр, запрашиваемый методом AddColumn<T>() ). Также в этом случае мы должны помнить, чтобы правильно установить свойство DatabaseSchemaVersion чтобы избежать дальнейшего выполнения операции обновления.

В обоих случаях нам необходимо применить описанные изменения, вызвав метод Execute() .

Эрик Эй, Microsoft MVP, разработал мощный инструмент Visual Studio под названием SQL Server Compact Toolbox, который может быть очень полезен для работы с приложениями SQL CE и Windows Phone.

Доступны две версии инструмента:

Ниже приведены некоторые функции, поддерживаемые инструментом:

  • Автоматически создавать сущности и класс DataContext начиная с уже существующей базы данных SQL CE.
  • Сгенерированный DataContext может скопировать базу данных из вашего проекта Visual Studio в локальное хранилище вашего приложения. Таким образом, вы можете начать с предварительно заполненной базы данных и в то же время иметь доступ для записи.
  • Сгенерированный DataContext поддерживает ведение журнала в окне вывода Visual Studio, чтобы вы могли видеть запросы SQL, сгенерированные LINQ to SQL.

SQLite, с концептуальной точки зрения, является решением, аналогичным SQL CE: это автономное решение для баз данных, где данные хранятся в одном файле без требования к СУБД.

Плюсы использования SQLite:

  • Он предлагает лучшую производительность, чем SQL CE, особенно с большими объемами данных.
  • Это открытый исходный код и кроссплатформенный; вы найдете реализацию SQLite для Windows 8, Android, iOS, веб-приложений и т. д.

Поддержка SQLite была введена только в Windows Phone 8 благодаря новой функции поддержки встроенного кода (поскольку механизм SQLite написан на собственном коде) и доступна в виде расширения Visual Studio, которое можно загрузить с веб-сайта Visual Studio .

После того, как вы установили его, вы найдете среду выполнения SQLite для Windows Phone в окне « Добавить ссылку» в разделе « Расширение Windows Phone ». Быть осторожен; эта среда выполнения — просто движок SQLite, написанный на нативном коде. Если вам нужно использовать базу данных SQLite в приложении C #, вам понадобится сторонняя библиотека, которая сможет выполнить соответствующие собственные вызовы.

На самом деле есть две доступные библиотеки SQLite: sqlite-net и SQLite Wrapper для Windows Phone . К сожалению, ни один из них не является настолько мощным и гибким, как библиотека LINQ to SQL, доступная для SQL CE.

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

Sqlite-net — сторонняя библиотека. Оригинальная версия для приложений Магазина Windows разработана Фрэнком А. Крюгером , а порт Windows Phone 8 — Питером Хьюном .

Версия для Windows Phone доступна на GitHub. Процедура его настройки немного сложна и время от времени меняется, поэтому обязательно следуйте указаниям разработчика на домашней странице проекта .

Sqlite-net предлагает LINQ-подход к использованию базы данных, который аналогичен первому коду, предлагаемому LINQ to SQL с SQL CE.

Например, в sqlite-net таблицы сопоставляются с сущностями вашего проекта. Разница в том, что на этот раз атрибуты не требуются, поскольку каждое свойство будет автоматически переведено в столбец. Атрибуты необходимы только в том случае, если вам нужно настроить процесс преобразования, как в следующем примере:

01
02
03
04
05
06
07
08
09
10
public class Person
{
    [PrimaryKey, AutoIncrement]
    public int Id { get;
 
    [MaxLength(50)]
    public string Name { get;
 
    set;
}

Surnameне имеет никакого атрибута, поэтому он будет автоматически преобразован в varcharстолбец. Вместо этого мы устанавливаем Idв качестве первичного ключа значение автоинкремента, а мы указываем, что Nameмаксимальная длина может быть 50 символов.

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

Как и в случае с SQL CE и LINQ to SQL, нам необходимо создать базу данных перед ее использованием. Это делается путем вызова CreateTableAsync<T>()метода для каждой таблицы, которую нам нужно создать, где Tнаходится тип таблицы. В следующем примере мы создаем таблицу для хранения Personсущности:

1
2
3
4
5
private async Task CreateDatabase()
{
    SQLiteAsyncConnection conn = new SQLiteAsyncConnection(Path.Combine(ApplicationData.Current.LocalFolder.Path, "people.db"), true);
    await conn.CreateTableAsync<Person>();
}

У нас нет способа проверить, существует ли таблица, поскольку она не нужна; если таблица, которую мы создаем, уже существует, CreateTableAsync<T>()метод просто ничего не сделает.

По аналогии с LINQ to SQL запросы выполняются с использованием Table<T>объекта. Единственное отличие состоит в том, что все методы LINQ являются асинхронными.

1
2
3
4
5
private async void OnReadDataClicked(object sender, RoutedEventArgs e)
{
    SQLiteAsyncConnection conn = new SQLiteAsyncConnection(Path.Combine(ApplicationData.Current.LocalFolder.Path, "people.db"), true);
    List<Person> person = await conn.Table<Person>().Where(x => x.Name == "Matteo").ToListAsync();
}

В предыдущем примере мы извлекаем все Personобъекты, которые называются Matteo.

Вставка, обновление и удаление операций , а не непосредственно выполняются с помощью SQLiteAsyncConnectionобъекта, который предлагает InsertAsync(), UpdateAsync()и DeleteAsync()методы. Не обязательно указывать тип объекта; sqlite-net автоматически обнаружит его и выполнит операцию на соответствующей таблице. В следующем примере вы можете увидеть, как новая запись добавляется в таблицу:

01
02
03
04
05
06
07
08
09
10
11
12
private async void OnAddDataClicked(object sender, RoutedEventArgs e)
{
    SQLiteAsyncConnection conn = new SQLiteAsyncConnection(Path.Combine(ApplicationData.Current.LocalFolder.Path, "people.db"), true);
 
    Person person = new Person
    {
        Name = "Matteo",
        Surname = "Pagani"
    };
 
    await conn.InsertAsync(person);
}

Sqlite-net — это библиотека SQLite, которая предлагает самый простой подход, но имеет много ограничений. Например, внешние ключи не поддерживаются, поэтому невозможно легко управлять отношениями.

SQLite Wrapper для Windows Phone был разработан непосредственно членами команды Microsoft (в частности, Питером Торром и Энди Уигли) и предлагает совершенно иной подход, чем sqlite-net. Он не поддерживает LINQ, просто простые операторы SQL-запросов.

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

Чтобы узнать, как настроить упаковщик в вашем проекте, следуйте инструкциям, размещенным на странице проекта CodePlex . Вам нужно будет загрузить исходный код проекта и добавить правильную версию оболочки в свое решение — есть две отдельные библиотеки, одна для Windows Phone 8 и одна для приложений Магазина Windows.

Вызывается ключевой класс Database, который заботится об инициализации базы данных и предлагает все методы, необходимые для выполнения запросов. В качестве параметра необходимо указать локальный путь хранения для сохранения базы данных. Если путь не существует, он будет создан автоматически. Затем вам нужно открыть соединение, используя OpenAsync()метод. Теперь вы готовы к выполнению операций.

Есть два способа выполнить запрос на основе значения, которое он возвращает.

Если запрос не возвращает значение — например, создание таблицы — вы можете использовать ExecuteStatementAsync()метод, как показано в следующем примере:

01
02
03
04
05
06
07
08
09
10
11
12
13
private async void OnCreateDatabaseClicked(object sender, RoutedEventArgs e)
{
    Database database = new Database(ApplicationData.Current.LocalFolder, “people.db”);
 
    await database.OpenAsync();
 
    string query = “CREATE TABLE PEOPLE “ +
                    “(Id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,” +
                    “Name varchar(100), “ +
                    “Surname varchar(100))”;
 
    await database.ExecuteStatementAsync(query);
}

Предыдущий метод просто выполняет запрос к открытой базе данных. В примере мы создаем Peopleтаблицу с двумя полями Nameи Surname.

Вместо этого запрос может содержать некоторые динамические параметры или возвращать некоторые значения. В этом случае нам нужно ввести новый класс с именем, Statementкак показано в следующем примере:

01
02
03
04
05
06
07
08
09
10
11
12
13
private async void OnAddDataClicked(object sender, RoutedEventArgs e)
{
    Database database = new Database(ApplicationData.Current.LocalFolder, “people.db”);
 
    await database.OpenAsync();
 
    string query = “INSERT INTO PEOPLE (Name, Surname) VALUES (@name, @surname)”;
    Statement statement = await database.PrepareStatementAsync(query);
    statement.BindTextParameterWithName(“@name”, “Matteo”);
    statement.BindTextParameterWithName(“@surname”, “Pagani”);
 
    await statement.StepAsync();
}

StatementКласс идентифицирует запрос, но он позволяет дополнительная настройка должна быть выполнена с ним. В примере мы используем его , чтобы присвоить значение динамического к Nameи Surnameпараметрам. Мы устанавливаем заполнитель, используя префикс @ ( @nameи @surname), а затем присваиваем им значение, используя BindTextParameterWithName()метод, передавая имя параметра и значение.

BindTextParameterWithName()не единственный доступный метод, но он специально для строковых параметров. Существуют и другие методы, основанные на типе параметра, например, BindIntParameterWithName()для чисел.

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

В следующем примере мы можем увидеть, как этот метод может использоваться для управления результатами SELECTзапроса:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
private async void OnGetDataClicked(object sender, RoutedEventArgs e)
{
    Database database = new Database(ApplicationData.Current.LocalFolder, “people.db”);
 
    await database.OpenAsync();
 
    string query = “SELECT * FROM PEOPLE”;
    Statement statement = await database.PrepareStatementAsync(query);
 
    while (await statement.StepAsync())
    {
        MessageBox.Show(statement.GetTextAt(0) + “ “ + statement.GetTextAt(1));
    }
}

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

Когда у нас есть ссылка на строку, мы можем получить доступ к ее значениям, используя индекс столбца и Get()метод. У нас есть Get()вариант для каждого типа данных, например GetText(), GetInt()и т. Д.

Другой способ — получить доступ к столбцам, используя Columnsколлекцию с именем столбца в качестве индекса. В этом случае сначала необходимо вызвать EnableColumnsProperty()метод, как показано в следующем примере:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
private async void OnGetSomeDataWithColumnsPropertyClicked(object sender, RoutedEventArgs e)
{
    Database database = new Database(ApplicationData.Current.LocalFolder, “people.db”);
 
    await database.OpenAsync();
 
    string query = “SELECT * FROM PEOPLE”;
    Statement statement = await database.PrepareStatementAsync(query);
 
    statement.EnableColumnsProperty();
 
    while (await statement.StepAsync())
    {
        MessageBox.Show(statement.Columns[“Name”] + “ “ + statement.Columns[“Surname”]);
    }
}

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

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

  • Правильное использование файлов и папок в изолированном хранилище благодаря API среды выполнения Windows.
  • Простое управление настройками нашего приложения с помощью IsolatedStorageSettingsкласса.
  • Хранение данных нашего приложения с использованием сериализации и десериализации в простых сценариях приложения.
  • В случае более сложных приложений мы увидели, как мы можем лучше организовать наши данные, используя базы данных. Мы проанализировали два доступных решения: SQL CE и SQLite. Они оба предлагают автономную платформу базы данных. SQL CE эксклюзивен для Windows Phone, но он более мощный и простой в использовании; SQLite является открытым исходным кодом и кроссплатформенным, но вы должны полагаться на сторонние библиотеки, которые не так мощны, как LINQ to SQL для SQL CE.

Это руководство представляет собой главу из Windows Phone 8 Succinctly , бесплатной электронной книги от команды Syncfusion.