Содержание
Базы данных SQLite в .NET MAUI используются довольно В .NET и C# мы можем получать доступ к данным различными способами. Мы можем использовать прямой доступ к БД, манипулируя данными вручную — создавать и удалять таблицы, редактировать записи, используя SQL-запросы и так далее. А можем использовать какую-либо ORM-технологию, например, Entity Framework Core и работать с данными в БД, как с обычными объектами .NET.
Доступ к данным в БД SQLite в .NET MAUI
Для прямого доступа к БД SQLite в .NET MAUI нам потребуется добавить в проект необходимые NuGet-пакеты, наиболее популярные из которых — это:
- sqlite-net-pcl (SQLite.NET) — сторонний проект с открытым исходным кодом, разрабатываемая изначально для Xamarin.iOS. На данный момент библиотека SQLite.NET работает как с MAUI.NET, так и с другими технологиями и платформами, например, с UWP, WPF и так далее. Поддерживает как синхронный, так и асинхронный метод доступа к данным.
- Microsoft.Data.Sqlite — библиотека от Microsoft, представляющая собой упрощенный поставщик ADO.NET. Эта же библиотека используется для доступа к SQLite в EF Core.
Здесь мы будем использовать первый пакет — SQLite.NET, а со вторым пакетом познакомимся непосредственно при рассмотрении вопросов, связанных с использованием EF Core в .NET MAUI. Рассмотрим работу с SQLite в .NET MAUI на примере небольшого проекта.
Создание проекта и установка необходимых пакетов
Создадим новый проект .NET MAUI и через диспетчер пакетов NuGet добавим в проект пакет sqlite-net-pcl:
Убедимся, что пакет установился корректно и мы сможем работать с SQLite на любой платформе, поддерживаемой в .NET MAUI:
Теперь создадим модель данных с которыми будем работать. Для этого создадим в проекте папку с именем Models и разместим в ней следующий класс:
public class Project
{
public int Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public string Author { get; set; }
}
Объекты этого класса представляют собой некие проекты у которых определяются такие свойства как имя (Name), описание (Description) и автор проекта (Author). Структура нашего проекта теперь должна выглядеть следующим образом:
Теперь настроим наш проект на работу с БД SQLite.
Создание класса доступа к базе данных SQLite
Создадим в проекте новую папку Services и добавим в неё класс ProjectDatabase:
Этот класс мы будем использовать для подключения к БД SQLite и выполнения операций CRUD (создание — Create, чтение — Read, обновление — Update и удаление — Delete записей). Вначале, определим в этом классе следующие константы:
public class ProjectDatabase
{
public const string DatabaseFilename = "Projects.sqlite";
public const SQLite.SQLiteOpenFlags Flags =
// открываем БД в режиме чтения/записи
SQLite.SQLiteOpenFlags.ReadWrite |
// создаем файл базы данных, если он не обнаружен
SQLite.SQLiteOpenFlags.Create |
// разрешаем доступ к БД из нескольких потоков
SQLite.SQLiteOpenFlags.SharedCache;
public static string DatabasePath =>
Path.Combine(FileSystem.AppDataDirectory, DatabaseFilename);
}
Здесь DatabaseFilename — имя файла базы данных, которое мы будем использовать в приложении.
DatabasePath — путь к файлу БД. Для определения пути мы используем метод статического класса Path — Combine() и, при этом, в качестве первого параметра для этого метода указываем папку приложения:
public static string DatabasePath =>
Path.Combine(FileSystem.AppDataDirectory, DatabaseFilename);
Flags — флаги, используемый при создании подключения к БД. В нашем случае мы разрешаем доступ к БД из нескольких потоков, открываем БД на чтение и запись, а также будем создавать файл БД, если он не будет обнаружен. Обратите внимание на то, что перечисление SQLiteOpenFlags содержит именно битовые флаги, поэтому значение Flags мы определяем через логическое сложение:
public const SQLite.SQLiteOpenFlags Flags = SQLite.SQLiteOpenFlags.ReadWrite | SQLite.SQLiteOpenFlags.Create | SQLite.SQLiteOpenFlags.SharedCache;
В целом, перечисление SQLiteOpenFlags содержит следующие флаги, которые мы можем использовать:
| Значение | Описание |
Create |
подключение автоматически создаст файл базы данных, если он не существует. |
FullMutex |
подключение открывается в режиме сериализованного потока. В этом режиме потоки могут как угодно использовать вызовы SQLite, но все вызовы блокируют друг друга и обрабатываются строго последовательно. |
NoMutex |
подключение открывается в многопоточном режиме. В этом режиме нельзя использовать одно и то же соединение одновременно из нескольких потоков (но допускается одновременное использование разных соединений разными потоками). Обычно используется именно этот режим. |
PrivateCache |
подключение не будет использовать общей кэш, даже если он включен. Это режим, при котором для каждого подключения к базе данных используется отдельный кэш |
ReadWrite |
подключение может считывать и записывать данные. |
SharedCache |
подключение будет использовать общий кэш, если он включен |
ProtectionComplete |
файл шифруется и недоступен, пока устройство заблокировано |
ProtectionCompleteUnlessOpen |
файл шифруется до его открытия, но затем доступен, даже если пользователь блокирует устройство. |
ProtectionCompleteUntilFirstUserAuthentication |
файл шифруется до тех пор, пока пользователь не загрузил приложение и не разблокировал устройство |
ProtectionNone |
файл базы данных не зашифрован |
Теперь создадим подключение к БД. Изменим класс следующим образом:
public class ProjectDatabase
{
public const string DatabaseFilename = "Projects.sqlite";
public const SQLite.SQLiteOpenFlags Flags = SQLite.SQLiteOpenFlags.ReadWrite | SQLite.SQLiteOpenFlags.Create | SQLite.SQLiteOpenFlags.SharedCache;
public static string DatabasePath =>
Path.Combine(FileSystem.AppDataDirectory, DatabaseFilename);
SQLiteAsyncConnection database;
async Task Init()
{
if (database is not null)
return;
database = new SQLiteAsyncConnection(DatabasePath, Flags);
var result = await database.CreateTableAsync<Project>();
}
}
Метод Init() выполняет отложенную асинхронную инициализацию базы данных. Объект database будет создан при первом обращении к нему, а сам метод мы будем использовать при каждом доступе к БД. Создадим методы доступа к данным. Библиотека SQLite.NET предоставляет нам слой ORM (object-relational mapping — отображения данных на реальные объекты), который позволяет хранить и извлекать объекты без написания инструкций SQL. Наши методы доступа к данным будут выглядеть следующим образом:
public class ProjectDatabase
{
...
public async Task<List<Project>> GetAllItemsAsync()
{
await Init();
return await database.Table<Project>().ToListAsync();
}
public async Task<Project> GetItemAsync(int id)
{
await Init();
return await database.Table<Project>().Where(i => i.Id == id).FirstOrDefaultAsync();
}
public async Task<int> SaveItemAsync(Project item)
{
await Init();
if (item.Id != 0)
return await database.UpdateAsync(item);
else
return await database.InsertAsync(item);
}
public async Task<int> DeleteItemAsync(Project item)
{
await Init();
return await database.DeleteAsync(item);
}
}
При выполнении каждого метода мы вызываем Init(), чтобы быть уверенными, что файл БД создан и содержит необходимые для работы таблицы. При этом от нас не требуется составления никаких SQL-запросов — мы работаем с данными в БД точно также, как с обычными объектами .NET. Это и есть наглядный пример использования ORM-технологии. Итак, мы разработали класс для доступа к данным в БД SQLite в .NET MAUI и теперь нам осталось немного изменить класс Project, чтобы он мог использоваться при работе с БД, а именно — необходимо определить первичный ключ в таблице проектов. Сделать это просто, используя атрибуты из SQLite.NET:
public class Project
{
[PrimaryKey, AutoIncrement]
public int Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public string Author { get; set; }
}
Здесь свойство Id нашего класса будет выступать в качестве первичного ключа таблицы. Класс доступа к базе данных SQLite готов, теперь необходимо организовать доступ к БД в нашем приложении.
Организация доступа к БД SQLite в .NET MAUI
Чтобы организовать доступ к БД SQLite в .NET MAUI мы можем использовать два варианта:
- Создавать каждый раз, при необходимости, объект класса
ProjectDatabaseи работать с ним, например, в модели представления - Зарегистрировать класс
ProjectDatabaseкак сервис и запрашивать его по мере необходимости, например, через конструктор
Воспользуемся вторым способом. Для этого откроем файл MauiProgram.cs и зарегистрируем наш сервис:
using Microsoft.Extensions.Logging;
using Projects.Services;
namespace Projects;
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.ConfigureFonts(fonts =>
{
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
});
builder.Services.AddSingleton<ProjectDatabase>(); //регистрируем класс как сервис
#if DEBUG
builder.Logging.AddDebug();
#endif
return builder.Build();
}
}
Теперь создадим модель представления для страницы MainPage. Добавим в проект папку ViewModels и разместим в ней новый класс MainPageViewModel
using Projects.Models;
using Projects.Services;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows.Input;
namespace Projects.ViewModels
{
public class MainPageViewModel: INotifyPropertyChanged
{
private readonly ProjectDatabase _database;
public ObservableCollection<Project> Projects { get; set; } = [];
private string _name = string.Empty;
public string Name
{
get => _name;
set {
if (_name == value) return;
_name = value;
OnPropertyChanged();
}
}
private string _description = string.Empty;
public string Description
{
get => _description;
set {
if (_description == value) return;
_description = value;
OnPropertyChanged();
}
}
private string _author = string.Empty;
public string Author
{
get => _author;
set
{
if (_author == value) return;
_author = value;
OnPropertyChanged();
}
}
private DateTime _deadline = DateTime.Now;
public DateTime Deadline
{
get => _deadline;
set
{
if (_deadline == value) return;
_deadline = value;
OnPropertyChanged();
}
}
public ICommand AddCommand { get; set; }
public ICommand UpdateCommand { get; set; }
public ICommand DeleteCommand { get; set; }
public ICommand ReadCommand { get; set; }
public ICommand SelectedItemCommand { get; set; }
public MainPageViewModel(ProjectDatabase database)
{
_database = database;
AddCommand = new Command(async () =>
{
Project project = new()
{
Name = this.Name,
Description = this.Description,
Author = this.Author
};
int result = await _database.SaveItemAsync(project);
if (result == 0)
throw new Exception("Ошибка добавления новой записи в базу данных");
Projects.Add(project);
Name = string.Empty;
Description = string.Empty;
Author = string.Empty;
Deadline = DateTime.Now;
});
UpdateCommand = new Command<Project>(async (Project) =>
{
Project.Name = this.Name;
Project.Description = this.Description;
Project.Author = this.Author;
int result = await _database.SaveItemAsync(Project);
if (result == 0)
throw new Exception("Ошибка обновления записи в базе данных");
ReadCommand?.Execute(null);
},
(Param) => { return Param != null; });
DeleteCommand = new Command<Project>(async (Project) =>
{
int result = await _database.DeleteItemAsync(Project);
Projects.Remove(Project);
if (result == 0)
throw new Exception("Ошибка удаления записи из базы данных");
},
(Param) => { return Param != null; });
ReadCommand = new Command(async () =>
{
var list = await _database.GetAllItemsAsync();
Projects.Clear();
foreach (var item in list)
Projects.Add(item);
});
SelectedItemCommand = new Command<Project>((Project) =>
{
Name = Project.Name;
Description = Project.Description;
Author = Project.Author;
},
(Project) => { return Project != null; });
}
public event PropertyChangedEventHandler? PropertyChanged;
public void OnPropertyChanged([CallerMemberName] string prop = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(prop));
}
}
}
Рассмотрим этот класс подробно. Итак, через конструктор класса мы запрашиваем сервис для работы с базой данных:
public class MainPageViewModel: INotifyPropertyChanged
{
private readonly ProjectDatabase _database;
...
public MainPageViewModel(ProjectDatabase database)
{
_database = database;
...
}
}
Здесь же, в конструкторе, мы определяем необходимые нам команды — на обновление списка проектов, добавление нового проекта, удаление и обновление проекта, а также команду для обновления свойств класса при выборе проекта в списке.
Команда ReadCommand используется для чтения списка проектов (обновления списка)
ReadCommand = new Command(async () =>
{
var list = await _database.GetAllItemsAsync();
Projects.Clear();
foreach (var item in list)
Projects.Add(item);
});
Здесь мы просто считываем весь список проектов и добавляем каждый проект с Projects — свойство нашей модели представления, которое выглядит следующим образом:
public ObservableCollection<Project> Projects { get; set; } = [];
Команда AddCommand добавляет новый проект в список:
AddCommand = new Command(async () =>
{
Project project = new()
{
Name = this.Name,
Description = this.Description,
Author = this.Author
};
int result = await _database.SaveItemAsync(project);
if (result == 0)
throw new Exception("Ошибка добавления новой записи в базу данных");
Projects.Add(project);
Name = string.Empty;
Description = string.Empty;
Author = string.Empty;
Deadline = DateTime.Now;
});
Наш метод сервиса ProjectDatabase SaveItemAsync() возвращает количество строк, добавленных в таблицу. Поэтому, если результат выполнения метода равен нулю, то мы генерируем исключение:
if (result == 0)
throw new Exception("Ошибка добавления новой записи в базу данных");
После добавления нового проекта в базу данных, мы также добавляем его в список Projects и сбрасываем значения свойств модели представления к значениям по умолчанию:
Projects.Add(project); Name = string.Empty; Description = string.Empty; Author = string.Empty; Deadline = DateTime.Now;
Команда UpdateCommand обновляет запись в базе данных
UpdateCommand = new Command<Project>(async (Project) =>
{
Project.Name = this.Name;
Project.Description = this.Description;
Project.Author = this.Author;
int result = await _database.SaveItemAsync(Project);
if (result == 0)
throw new Exception("Ошибка обновления записи в базе данных");
ReadCommand?.Execute(null);
},
(Param) => { return Param != null; });
Эта команда получает в качестве параметра объект типа Project, который нам необходимо обновить. При этом команда будет отключена, если значение параметра равно null:
(Param) => { return Param != null; }
После обновления объекта в базе данных мы просто вызываем команду обновления списка для того, чтобы увидеть изменения
ReadCommand?.Execute(null);
Способ обновления сведений в списке далеко не самый рациональный, но, в качестве примера подойдет.
Команда DeleteCommand удаляет проект из списка. В целом, команда похожа на предыдущую, но только здесь мы не обновляем полностью список проектов после удаления, а только удаляем проект из Projects
DeleteCommand = new Command<Project>(async (Project) =>
{
int result = await _database.DeleteItemAsync(Project);
Projects.Remove(Project);
if (result == 0)
throw new Exception("Ошибка удаления записи из базы данных");
},
(Param) => { return Param != null; });
Последняя команда — SelectedItemCommand используется для заполнения свойств модели представления значениями свойств проекта, выбранного пользователем в списке:
SelectedItemCommand = new Command<Project>((Project) =>
{
Name = Project.Name;
Description = Project.Description;
Author = Project.Author;
},
(Project) => { return Project != null; });
Теперь нам необходимо привязать свойства и команды представления к визуальной части нашего приложения.
Привязка свойств и команд модели представления
Про привязку команд и передачу параметров мы уже знаем, поэтому просто приведем здесь пример страницы MainPage на которой и будет происходить вся работа с базой данных:
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Projects.MainPage"
xmlns:local="clr-namespace:Projects.ViewModels"
xmlns:models="clr-namespace:Projects.Models"
x:DataType="local:MainPageViewModel">
<ScrollView>
<VerticalStackLayout
Padding="30,0"
Spacing="25">
<VerticalStackLayout Padding="15" Margin="15">
<Label Text="Название проекта" />
<Entry Text="{Binding Name}" />
<Label Text="Описание" />
<Entry Text="{Binding Description}" />
<Label Text="Автор" />
<Entry Text="{Binding Author}" />
<CollectionView x:Name="ProjectsView"
SelectionMode="Single"
ItemsSource="{Binding Projects, Mode=TwoWay}"
Margin="15"
SelectionChangedCommand="{Binding SelectedItemCommand}"
SelectionChangedCommandParameter="{Binding Source={RelativeSource Mode=Self},Path=SelectedItem}">
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="{x:Type models:Project}">
<Border StrokeShape="Rectangle" Background="LightGreen">
<VerticalStackLayout>
<Label Text="{Binding Name}" FontSize="16" FontAttributes="Bold" TextColor="Blue"/>
<Label Text="{Binding Id}"/>
<Label Text="{Binding Description}"/>
<Label Text="{Binding Author}"/>
</VerticalStackLayout>
</Border>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
</VerticalStackLayout>
<HorizontalStackLayout HorizontalOptions="Center">
<Button
Margin="10"
Text="Обновить список"
Command="{Binding ReadCommand}" />
<Button
Margin="10"
Text="Добавить"
Command="{Binding AddCommand}"
CommandParameter="{Binding Source={x:Reference ProjectsView}, Path=SelectedItem}"/>
<Button
Margin="10"
Text="Удалить"
Command="{Binding DeleteCommand}"
CommandParameter="{Binding Source={x:Reference ProjectsView}, Path=SelectedItem}"/>
<Button
Margin="10"
Text="Изменить"
Command="{Binding UpdateCommand}"
CommandParameter="{Binding Source={x:Reference ProjectsView}, Path=SelectedItem}"/>
</HorizontalStackLayout>
</VerticalStackLayout>
</ScrollView>
</ContentPage>
Проверим работу приложения:
Итого
Для работы с SQLite мы воспользовались библиотекой с открытым исходным кодом SQLite.NET. В качестве примера мы разработали небольшое приложение, использующее базу данных SQLite для хранения данных о проектах. Проект использует шаблон MVVM и содержит модель, модель представления и представление. Рассмотренный способ работы с SQLite в .NET MAUI не единственный. При желании, мы можем также воспользоваться и «родной» технологией .NET — Microsoft Entity Framework Core.

