Files
Phonebook/LABORATORY_WORK.md
2026-04-10 11:20:19 +03:00

1486 lines
62 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# ЛАБОРАТОРНАЯ РАБОТА
## Разработка кроссплатформенного мобильного приложения «Телефонный справочник» с использованием .NET MAUI и C#
---
## СОДЕРЖАНИЕ
1. [Введение](#1-введение)
2. [Теоретическая часть](#2-теоретическая-часть)
3. [Практическая часть](#3-практическая-часть)
4. [Заключение](#4-заключение)
5. [Список использованных источников](#5-список-использованных-источников)
---
## 1. ВВЕДЕНИЕ
### 1.1 Актуальность темы
В современном мире мобильные приложения стали неотъемлемой частью повседневной жизни. Смартфоны используются для хранения контактной информации, что делает приложения-справочники одними из наиболее востребованных утилит. Традиционные телефонные справочники, встроенные в операционные системы, зачастую не满足ают потребностям пользователей в части гибкости, кастомизации интерфейса и дополнительной функциональности.
Создание собственного приложения «Телефонный справочник» является актуальной задачей по следующим причинам:
- **Персонализация** — возможность адаптировать приложение под конкретные нужды пользователя
- **Офлайн-доступ** — данные хранятся локально и доступны без интернет-соединения
- **Кроссплатформенность** — охват максимальной аудитории при минимальных затратах на разработку
- **Практическое применение** — разработка реального приложения с CRUD-операциями, базой данных и графическим интерфейсом
### 1.2 Почему .NET MAUI и C#?
**.NET MAUI** (Multi-platform App UI) — это современный фреймворк от Microsoft для создания кроссплатформенных приложений с единой кодовой базой. Выбор данной технологии обусловлен рядом преимуществ:
| Критерий | Описание |
|----------|----------|
| **Единая кодовая база** | Один проект для iOS, Android, Windows, macOS |
| **Нативная производительность** | Компиляция в нативный код для каждой платформы |
| **C# как язык разработки** | Современный, типобезопасный объектно-ориентированный язык |
| **Прямой доступ к API платформ** | Возможность использовать платформо-специфичный код |
| **Встроенные контролы** | Богатая библиотека кроссплатформенных UI-компонентов |
| **Интеграция с экосистемой .NET** | Доступ к NuGet-пакетам и инструментам |
**C#** является оптимальным выбором благодаря:
- Строгой типизации и безопасности памяти
- Мощной стандартной библиотеке
-LINQ для работы с коллекциями
- Асинхронному программированию (async/await)
- Активной поддержке и развитию языка
### 1.3 Цель и задачи лабораторной работы
**Цель:** разработать кроссплатформенное мобильное приложение «Телефонный справочник» с использованием .NET MAUI и языка программирования C#.
**Задачи:**
1. Изучить теоретические основы .NET MAUI
2. Спроектировать архитектуру приложения
3. Реализовать CRUD-операции для контактов
4. Настроить хранение данных с использованием SQLite
5. Реализовать систему смены тем оформления
6. Добавить функционал импорта/экспорта контактов
7. Провести тестирование приложения
---
## 2. ТЕОРЕТИЧЕСКАЯ ЧАСТЬ
### 2.1 Обзор технологического стека
#### 2.1.1 .NET 9.0
**.NET 9** — latest stable release платформы .NET, обеспечивающий высокую производительность и широкие возможности для разработки приложений. В данном проекте используется как рантайм для MAUI-приложений.
**Ключевые особенности .NET 9:**
- Улучшенная производительность JIT-компилятора (Just-In-Time)
- **Native AOT компиляция** — компиляция Ahead-of-Time для быстрого запуска
- Обновлённые библиотеки Base Class Library (BCL)
- Улучшенная работа с JSON (System.Text.Json)
- Новые функции в LINQ
- Поддержка регулярных выражений нового поколения
**.NET Runtime** обеспечивает:
- Управление памятью через сборщик мусора (GC)
- JIT/AOT компиляцию IL-кода в нативный
- Common Type System (CTS) для единообразия типов данных
- Безопасность и управление доступом кода
#### 2.1.2 .NET MAUI
**.NET MAUI** (Multi-platform App UI) — эволюция Xamarin.Forms, предоставляющая единый фреймворк для создания нативных приложений для всех поддерживаемых платформ. Релиз состоялся в 2022 году как часть .NET 7.
**История развития:**
```
Xamarin.Forms (2014) → Xamarin.Forms 5.0 → .NET MAUI (.NET 7, 2022) → .NET MAUI (.NET 9, 2024)
```
**Архитектура MAUI:**
```
┌─────────────────────────────────────────────────────────────────┐
│ Код приложения (C#) │
├─────────────────────────────────────────────────────────────────┤
│ UI-слой (XAML/C#) │
│ ┌──────────────────────────────────────────┐ │
│ │ Pages │ Layouts │ Controls │ Converters │ │
│ └──────────────────────────────────────────┘ │
├─────────────────────────────────────────────────────────────────┤
│ Platform Features │
│ Geolocation │ Contacts │ Media │ Sensors │ Sharing │
├─────────────────────────────────────────────────────────────────┤
│ Graphics (Сжатые графики) │
├─────────────────────────────────────────────────────────────────┤
│ Android │ iOS │ Windows │ Mac │ Linux │ Tizen │
│ (Skia) │(CoreG)│ (WinUI) │(CoreG) │ (GTK) │ (Tizen) │
└─────────────────────────────────────────────────────────────────┘
```
**Жизненный цикл приложения MAUI:**
| Метод | Описание |
|-------|---------|
| `OnStart()` | Вызывается при первом запуске приложения |
| `OnResume()` | Вызывается при возобновлении из фона |
| `OnSleep()` | Вызывается при переходе в фоновый режим |
**Целевые платформы проекта:**
- `net9.0-android` — Android 5.0+ (API 21+)
- `net9.0-ios` — iOS 11.0+
- `net9.0-maccatalyst` — macOS 10.15+
- `net9.0-windows10.0.19041.0` — Windows 10+
**Ключевые преимущества MAUI:**
1. **Hot Reload** — мгновенное обновление UI без перезапуска
2. **Handlers Architecture** — настраиваемая система обработчиков для платформо-специфичных элементов
3. **Graphics API** — новый единый API для рисования через `Microsoft.Maui.Graphics`
4. **Shell** — навигация на основе оболочки с поддержкой меню и вкладок
#### 2.1.3 Язык C# 12
**C# 12** — современный объектно-ориентированный язык программирования с поддержкой:
- Nullable reference types
- Pattern matching
- Record types
- Init-only properties
- Async/await
- LINQ
- Primary constructors
- Collection expressions
- Alias any type
**Ключевые концепции C# в контексте MAUI:**
**Асинхронное программирование (async/await):**
```csharp
// Паттерн асинхронного программирования
public async Task<List<Contact>> GetContactsAsync()
{
var contacts = await _database.Table<Contact>().ToListAsync();
return contacts.OrderBy(c => c.Name).ToList();
}
```
**LINQ (Language Integrated Query):**
```csharp
// Запросы к коллекциям на уровне языка
var results = contacts
.Where(c => c.Name.Contains(searchTerm))
.OrderBy(c => c.Name)
.Select(c => new { c.Name, c.PhoneNumber });
```
**Обнуляемые ссылочные типы:**
```csharp
// ? делает тип обнуляемым
private Contact? _contact;
private string? _photoBase64;
// ! оператор подавления null-предупреждения
_contactService.OnContactsUpdated += LoadContacts;
```
#### 2.1.4 XAML
**XAML** (Extensible Application Markup Language) — декларативный язык разметки для определения пользовательского интерфейса в .NET-приложениях.
**Синтаксис XAML:**
```xml
<!-- Пространства имён -->
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:Phonebook">
<!-- Встроенные ресурсы -->
<ContentPage.Resources>
<converters:Base64ToPhoto x:Key="Base64ToPhoto"/>
</ContentPage.Resources>
<!-- Визуальная иерархия -->
<Grid>
<Label Text="Телефонный справочник" />
</Grid>
</ContentPage>
```
**Основные элементы MAUI:**
| Категория | Элементы |
|-----------|----------|
| **Pages** | ContentPage, NavigationPage, TabbedPage, Shell |
| **Layouts** | StackLayout, Grid, FlexLayout, AbsoluteLayout |
| **Controls** | Label, Button, Entry, Image, ListView, CollectionView |
| **Views** | Frame, Border, ScrollView |
**Привязка данных (Data Binding):**
```xml
<!-- Односторонняя привязка -->
<Label Text="{Binding Name}" />
<!-- Привязка с конвертером -->
<Image Source="{Binding PhotoBase64,
Converter={StaticResource Base64ToPhoto}}" />
<!-- Привязка с AppTheme -->
<Button BackgroundColor="{AppThemeBinding Light=#4CAF50, Dark=#333}" />
```
**Ресурсы и стили:**
```xml
<ResourceDictionary>
<Color x:Key="Primary">#4CAF50</Color>
<Style TargetType="Button">
<Setter Property="BackgroundColor" Value="{StaticResource Primary}"/>
<Setter Property="TextColor" Value="White"/>
</Style>
</ResourceDictionary>
```
### 2.2 Архитектура приложения
Приложение построено на принципах **сервис-ориентированной архитектуры** (SOA):
```
┌─────────────────────────────────────────────────────────────┐
│ Pages (UI Layer) │
│ MainPage │ ContactDetailPage │ SettingsPage │ AppShell │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ Services (Business Logic) │
│ ContactService │ DatabaseService │ ThemeService │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ Models (Data Layer) │
│ Contact.cs (Entity) │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ Data Persistence Layer │
│ SQLite Database │ Preferences Storage │
└─────────────────────────────────────────────────────────────┘
```
### 2.3 Компоненты архитектуры
#### 2.3.1 Слои приложения
Приложение состоит из следующих логических слоёв:
| Слой | Описание | Компоненты |
|------|----------|------------|
| **UI Layer** | Пользовательский интерфейс | XAML-страницы, обработчики событий |
| **Business Logic Layer** | Бизнес-логика приложения | ContactService, ThemeService |
| **Data Access Layer** | Взаимодействие с хранилищем | DatabaseService |
| **Infrastructure Layer** | Системные сервисы | SQLite, Preferences, Permissions |
#### 2.3.2 Dependency Injection (DI)
Внедрение зависимостей через `MauiProgram.cs`:
```csharp
builder.Services.AddSingleton<DatabaseService>();
builder.Services.AddSingleton<ThemeService>();
builder.Services.AddSingleton<ContactService>();
```
### 2.4 Технологии хранения данных
#### 2.4.1 SQLite
**SQLite** — легковесная встраиваемая реляционная база данных, широко используемая в мобильных приложениях. SQLite хранит всю базу данных в одном файле, что делает её идеальной для автономных приложений.
**Архитектура SQLite:**
```
┌─────────────────────────────────────────┐
│ Приложение (C# код) │
├─────────────────────────────────────────┤
│ SQLite-net-pcl (ORM Layer) │
├─────────────────────────────────────────┤
│ SQLite3 (Native C Library) │
├─────────────────────────────────────────┤
│ Database File (.db3) │
└─────────────────────────────────────────┘
```
**Преимущества SQLite:**
- Нулевая конфигурация — не требует сервера или администратора
- Хранение в одном переносимом файле
- Высокая производительность для мобильных приложений
- ACID-совместимость (атомарность, согласованность, изоляция, долговечность)
- Асинхронные операции через `SQLiteAsyncConnection`
- Поддержка SQL-запросов
**NuGet-пакет:** `sqlite-net-pcl` версии 1.9.172
**Основные операции SQLite-net:**
```csharp
// Создание таблицы
await _database.CreateTableAsync<Contact>();
// Вставка записи
await _database.InsertAsync(contact);
// Обновление записи
await _database.UpdateAsync(contact);
// Удаление записи
await _database.DeleteAsync(contact);
// Выборка всех записей
var contacts = await _database.Table<Contact>().ToListAsync();
// SQL-запрос
var result = await _database.QueryAsync<Contact>(
"SELECT * FROM Contact WHERE Name LIKE ?", "%search%");
```
#### 2.4.2 Preferences API
Для хранения пользовательских настроек (выбранная тема) используется `Microsoft.Maui.Storage.Preferences`.
**Особенности Preferences:**
- Простое API для хранения пар «ключ-значение»
- Автоматическая сериализация базовых типов
- Данные хранятся в platform-specific location
- Поддержка шифрования на мобильных платформах
```csharp
// Сохранение настройки
await Preferences.SetAsync(ThemePreferenceKey, (int)theme);
// Загрузка настройки
var savedTheme = Preferences.Get(ThemePreferenceKey, -1);
// Удаление настройки
Preferences.Remove(ThemePreferenceKey);
```
#### 2.4.3 Сравнение способов хранения данных
| Способ | Объём данных | Скорость | Сложность | Применение |
|--------|--------------|----------|-----------|------------|
| **Preferences** | < 1 КБ | Быстро | Минимальная | Настройки, темы |
| **SQLite** | До ГБ | Средняя | Средняя | Структурированные данные |
| **File System** | Неограничено | Варьируется | Высокая | Файлы, кэш, экспорт |
### 2.5 Используемые NuGet-пакеты
| Пакет | Версия | Назначение |
|-------|--------|------------|
| `sqlite-net-pcl` | 1.9.172 | SQLite ORM для C# |
| `ClosedXML` | 0.104.2 | Экспорт данных в Excel (XLSX) |
| `CommunityToolkit.Maui` | 11.2.0 | Расширения MAUI (Notifications, Behaviors) |
| `Microsoft.Extensions` | * | Dependency Injection и логирование |
| `Microsoft.Extensions.Logging.Debug` | * | Логирование в Output Window |
**Подробное описание пакетов:**
**sqlite-net-pcl:**
- Кроссплатформенная ORM для SQLite
- Атрибуты для определения схемы БД ([PrimaryKey], [AutoIncrement])
- Асинхронные методы для всех операций
- Минимальный API-поверхность
**ClosedXML:**
- Обёртка над Open XML SDK для работы с Excel
- Создание и чтение файлов XLSX
- Форматирование ячеек, добавление формул
- Поддержка .NET Standard 2.0+
**CommunityToolkit.Maui:**
- Расширения для MAUI от сообщества
- Behaviors, Converters, Effects
- Popup, Snackbar, Permissions
- Различные утилиты для повседневных задач
### 2.6 Dependency Injection (DI) в .NET MAUI
**Внедрение зависимостей** — паттерн проектирования, при котором зависимости передаются объекту извне, вместо того чтобы создаваться внутри.
**Зачем нужен DI:**
1. **Слабая связанность** — классы не зависят от конкретных реализаций
2. **Тестируемость** — легко подменить зависимости на моки
3. **Управление жизненным циклом** — .NET сам создаёт и уничтожает объекты
4. **Singletons** — один экземпляр на всё приложение
**Регистрация сервисов в MauiProgram.cs:**
```csharp
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder.Services.AddSingleton<DatabaseService>(); // Один экземпляр
builder.Services.AddSingleton<ThemeService>();
builder.Services.AddSingleton<ContactService>();
return builder.Build();
}
```
**Жизненный цикл сервисов:**
| Метод | Описание |
|-------|----------|
| `AddSingleton` | Один экземпляр на всё время жизни приложения |
| `AddScoped` | Один экземпляр на окно/страницу |
| `AddTransient` | Новый экземпляр при каждом запросе |
| `AddTransient<T, TImpl>` | Привязка интерфейса к реализации |
**Получение сервисов:**
```csharp
// В code-behind страницы
var service = IPlatformApplication.Current!
.Services.GetService<ContactService>();
// Или через конструктор (рекомендуется)
public class MyPage : ContentPage
{
private readonly ContactService _contactService;
public MyPage(ContactService contactService)
{
_contactService = contactService;
}
}
```
### 2.7 Система тем оформления
Приложение поддерживает динамическую смену тем:
- **Зелёная тема** (Green) — Primary: `#4CAF50`, светлый и чистый дизайн
- **Синяя тема** (Blue) — Primary: `#2196F3`, профессиональный вид
- **Тёмная тема** (Dark) — Primary: `#BB86FC`, Background: `#121212`
**Реализация тем в MAUI:**
Темы реализованы через динамическую загрузку ResourceDictionary:
```csharp
var themeDict = new ResourceDictionary
{
Source = new Uri("resource://Phonebook.Resources.Raw.Themes.GreenTheme.xaml")
};
app.Resources.MergedDictionaries.Clear();
app.Resources.MergedDictionaries.Add(themeDict);
```
**AppThemeBinding** позволяет автоматически адаптировать цвета:
```xml
<Button BackgroundColor="{AppThemeBinding Light=#4CAF50, Dark=#333}"
TextColor="{AppThemeBinding Light=White, Dark=White}"/>
```
### 2.8 Навигация в MAUI
В приложении используется **иерархическая навигация** на основе `NavigationPage`:
```csharp
// Переход к новой странице
await Navigation.PushAsync(new ContactDetailPage(contact));
// Возврат на предыдущую страницу
await Navigation.PopAsync();
// Замена текущей страницы
await Navigation.PushModalAsync(new Page()); // Модальная навигация
```
**Типы навигации MAUI:**
| Тип | Описание | Пример |
|-----|----------|--------|
| Иерархическая | Стек страниц | `Navigation.Push/Pop` |
| Модальная | Поверх текущей страницы | `Navigation.PushModalAsync` |
| Shell | На основе оболочки с меню | FlyoutItem, Tab |
**Shell-навигация (AppShell):**
```xml
<Shell>
<FlyoutItem Title="Контакты">
<ShellContent ContentTemplate="{DataTemplate local:MainPage}" />
</FlyoutItem>
<FlyoutItem Title="Настройки">
<ShellContent ContentTemplate="{DataTemplate local:SettingsPage}" />
</FlyoutItem>
</Shell>
```
### 2.9 Обработка событий и делегаты
В приложении используется паттерн **Observer (Наблюдатель)** для оповещения об изменениях:
```csharp
// Определение события в сервисе
public event Action? OnContactsUpdated;
// Вызов события после изменения данных
public async Task SaveContact(Contact contact)
{
await _databaseService.SaveContactAsync(contact);
OnContactsUpdated?.Invoke(); // Оповещаем подписчиков
}
// Подписка на событие в UI
public MainPage()
{
_contactService.OnContactsUpdated += LoadContacts;
}
```
**Преимущества паттерна Observer:**
- Разделение компонентов (издатель и подписчик)
- Loose coupling — компоненты не зависят друг от друга
- Уведомление всех заинтересованных сторон автоматически
### 2.10 Работа с платформенными API
MAUI предоставляет кроссплатформенный доступ к нативным функциям через `Microsoft.Maui.Essentials`:
| Пространство имён | Функциональность |
|-------------------|------------------|
| `Microsoft.Maui.ApplicationModel.Communication` | Контакты, звонки, Email, SMS |
| `Microsoft.Maui.Media` | Камера, галерея, файлы |
| `Microsoft.Maui.Storage` | Файловая система, обмен данными |
| `Microsoft.Maui.Devices.Sensors` | GPS, акселерометр, компас |
| `Microsoft.Maui.ApplicationModel` | Разрешения, информация об устройстве |
**Пример работы с контактами:**
```csharp
var deviceContacts = await ContactPicker.PickContactAsync();
var contact = new Contact
{
Name = deviceContacts.DisplayName ?? "",
PhoneNumber = deviceContacts.Phones?.FirstOrDefault()?.Number ?? ""
};
```
**Пример инициации звонка:**
```csharp
PhoneDialer.Open(phoneNumber);
```
### 2.11 Конвертеры значений (Value Converters)
Конвертеры позволяют преобразовывать данные при привязке:
```csharp
// Реализация IValueConverter
public class Base64ToPhoto : IValueConverter
{
public object Convert(object value, Type targetType,
object parameter, CultureInfo culture)
{
var base64 = value as string;
if (string.IsNullOrWhiteSpace(base64))
return ImageSource.FromFile("default_contact.png");
byte[] bytes = Convert.FromBase64String(base64);
return ImageSource.FromStream(() => new MemoryStream(bytes));
}
public object ConvertBack(object value, Type targetType,
object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
// Использование в XAML
<ContentPage.Resources>
<converters:Base64ToPhoto x:Key="Base64ToPhoto"/>
</ContentPage.Resources>
<Image Source="{Binding PhotoBase64,
Converter={StaticResource Base64ToPhoto}}"/>
```
---
## 3. ПРАКТИЧЕСКАЯ ЧАСТЬ
### 3.1 Структура проекта
```
Phonebook/
├── Phonebook.sln
├── Phonebook.csproj
├── App.xaml / App.xaml.cs
├── MauiProgram.cs
├── AppShell.xaml / AppShell.xaml.cs
├── MainPage.xaml / MainPage.xaml.cs
├── ContactDetailPage.xaml / .cs
├── SettingsPage.xaml / .xaml.cs
├── Models/
│ └── Contact.cs
├── Services/
│ ├── DatabaseService.cs
│ ├── ContactService.cs
│ └── ThemeService.cs
├── Converters/
│ ├── Base64ToPhoto.cs
│ └── NullToBoolConverter.cs
├── Utils/
│ ├── ImageHelper.cs
│ └── PermissionHelper.cs
└── Resources/
├── Styles/
│ ├── Colors.xaml
│ └── Styles.xaml
└── Raw/Themes/
├── GreenTheme.xaml
├── BlueTheme.xaml
└── DarkTheme.xaml
```
**[Скриншот: Структура проекта в Visual Studio]**
### 3.2 Модель данных
Файл: `Models/Contact.cs`
```csharp
using SQLite;
namespace Phonebook.Models
{
public class Contact
{
[PrimaryKey, AutoIncrement]
public int Id { get; set; }
public string Name { get; set; }
public string PhoneNumber { get; set; }
public string Email { get; set; }
public string Address { get; set; }
public string Description { get; set; }
public string PhotoPath { get; set; }
public string PhotoBase64 { get; set; }
}
}
```
**Описание полей:**
- `Id` — первичный ключ с автоинкрементом
- `Name` — имя контакта
- `PhoneNumber` — номер телефона
- `Email` — адрес электронной почты
- `Address` — физический адрес
- `Description` — дополнительное описание
- `PhotoPath` — путь к файлу изображения (устаревший)
- `PhotoBase64` — изображение в формате Base64
**[Скриншот: Диаграмма модели Contact]**
### 3.3 Сервис базы данных
Файл: `Services/DatabaseService.cs`
```csharp
using SQLite;
using Phonebook.Models;
namespace Phonebook.Services
{
public class DatabaseService
{
private readonly SQLiteAsyncConnection _database;
public DatabaseService()
{
var dbPath = Path.Combine(
FileSystem.AppDataDirectory,
"contacts.db3"
);
_database = new SQLiteAsyncConnection(dbPath);
}
public Task Initialize()
{
return _database.CreateTableAsync<Contact>();
}
public Task<List<Contact>> GetContactsAsync()
{
return _database.Table<Contact>().ToListAsync();
}
public Task<List<Contact>> SearchContactsAsync(string searchTerm)
{
if (string.IsNullOrWhiteSpace(searchTerm))
return GetContactsAsync();
var allContacts = _database.Table<Contact>().ToListAsync();
var compareInfo = CultureInfo.CurrentCulture.CompareInfo;
return allContacts.ContinueWith(t =>
t.Result.Where(c =>
compareInfo.IndexOf(c.Name ?? "", searchTerm,
CompareOptions.IgnoreCase) >= 0 ||
compareInfo.IndexOf(c.PhoneNumber ?? "", searchTerm,
CompareOptions.IgnoreCase) >= 0 ||
compareInfo.IndexOf(c.Email ?? "", searchTerm,
CompareOptions.IgnoreCase) >= 0
).DistinctBy(c => c.Id).ToList()
);
}
public Task<int> SaveContactAsync(Contact contact)
{
if (contact.Id != 0)
return _database.UpdateAsync(contact);
return _database.InsertAsync(contact);
}
public Task<int> DeleteContactAsync(Contact contact)
{
return _database.DeleteAsync(contact);
}
public Task<int> ClearAll()
{
return _database.DeleteAllAsync<Contact>();
}
}
}
```
**Описание методов:**
| Метод | Описание | Параметры | Возвращаемое значение |
|-------|----------|-----------|----------------------|
| `Initialize()` | Создаёт таблицу контактов в БД | — | `Task` |
| `GetContactsAsync()` | Получает все контакты | — | `Task<List<Contact>>` |
| `SearchContactsAsync()` | Поиск с учётом культуры | `searchTerm: string` | `Task<List<Contact>>` |
| `SaveContactAsync()` | Сохраняет/обновляет контакт | `contact: Contact` | `Task<int>` (количество затронутых строк) |
| `DeleteContactAsync()` | Удаляет контакт | `contact: Contact` | `Task<int>` |
| `ClearAll()` | Удаляет все контакты | — | `Task<int>` |
### 3.4 Сервис контактов (бизнес-логика)
Файл: `Services/ContactService.cs`
```csharp
using Microsoft.Maui.ApplicationModel.Communication;
using Microsoft.Maui.Storage;
using ClosedXML.Excel;
using Contact = Phonebook.Models.Contact;
namespace Phonebook.Services
{
public class ContactService
{
private readonly DatabaseService _databaseService;
public ContactService(DatabaseService databaseService)
{
_databaseService = databaseService;
}
public async Task<List<Contact>> GetAllContacts()
{
var contacts = await _databaseService.GetContactsAsync();
return contacts.OrderBy(c => c.Name).ToList();
}
public async Task<List<Contact>> SearchContacts(string searchTerm)
{
var contacts = await _databaseService.SearchContactsAsync(searchTerm);
return contacts.OrderBy(c => c.Name).ToList();
}
public async Task SaveContact(Contact contact)
{
await _databaseService.SaveContactAsync(contact);
OnContactsUpdated?.Invoke();
}
public async Task DeleteContact(Contact contact)
{
await _databaseService.DeleteContactAsync(contact);
OnContactsUpdated?.Invoke();
}
public async Task ImportFromLocalContact()
{
var hasPermission = await PermissionHelper.RequestContactsPermission();
if (!hasPermission) return;
var deviceContacts = await ContactPicker.PickContactAsync();
if (deviceContacts == null) return;
var contact = new Contact
{
Name = deviceContacts.DisplayName ?? "",
PhoneNumber = deviceContacts.Phones?.FirstOrDefault()?.Number ?? "",
Email = deviceContacts.Emails?.FirstOrDefault()?.EmailAddress ?? ""
};
await SaveContact(contact);
}
public async Task ExportToXlsx()
{
var contacts = await GetAllContacts();
using var workbook = new XLWorkbook();
var worksheet = workbook.Worksheets.Add("Contacts");
worksheet.Cell(1, 1).Value = "Id";
worksheet.Cell(1, 2).Value = "Имя";
worksheet.Cell(1, 3).Value = "Телефон";
worksheet.Cell(1, 4).Value = "Email";
worksheet.Cell(1, 5).Value = "Адрес";
worksheet.Cell(1, 6).Value = "Описание";
for (int i = 0; i < contacts.Count; i++)
{
var c = contacts[i];
worksheet.Cell(i + 2, 1).Value = c.Id;
worksheet.Cell(i + 2, 2).Value = c.Name;
worksheet.Cell(i + 2, 3).Value = c.PhoneNumber;
worksheet.Cell(i + 2, 4).Value = c.Email;
worksheet.Cell(i + 2, 5).Value = c.Address;
worksheet.Cell(i + 2, 6).Value = c.Description;
}
worksheet.Columns().AdjustToContents();
var fileName = $"contacts_{DateTime.Now:yyyyMMdd_HHmmss}.xlsx";
var localPath = Path.Combine(
FileSystem.CacheDirectory,
fileName
);
workbook.SaveAs(localPath);
await Share.Default.RequestAsync(new ShareFileRequest
{
Title = "Сохранить контакты",
File = new ShareFile(localPath)
});
}
public async Task ClearAllContacts()
{
await _databaseService.ClearAll();
OnContactsUpdated?.Invoke();
}
public event Action? OnContactsUpdated;
}
}
```
**Описание методов:**
| Метод | Описание |
|-------|----------|
| `GetAllContacts()` | Получает все контакты, отсортированные по имени |
| `SearchContacts()` | Осуществляет поиск по контактам |
| `SaveContact()` | Сохраняет контакт и вызывает событие обновления |
| `DeleteContact()` | Удаляет контакт и вызывает событие обновления |
| `ImportFromLocalContact()` | Импортирует контакт из телефонной книги устройства |
| `ExportToXlsx()` | Экспортирует все контакты в Excel-файл и открывает диалог сохранения |
| `ClearAllContacts()` | Удаляет все контакты |
**[Скриншот: Процесс экспорта контактов]**
### 3.5 Сервис тем оформления
Файл: `Services/ThemeService.cs`
```csharp
using Microsoft.Maui.Storage;
namespace Phonebook.Services
{
public enum AppThemeType { Green, Blue, Dark, Light }
public class ThemeService
{
private const string ThemePreferenceKey = "user_theme_preference";
public async Task ApplyTheme(AppThemeType theme)
{
var app = Application.Current;
if (app == null) return;
var themeName = theme switch
{
AppThemeType.Green => "GreenTheme",
AppThemeType.Blue => "BlueTheme",
AppThemeType.Dark => "DarkTheme",
_ => "GreenTheme"
};
var themeDict = new ResourceDictionary
{
Source = new Uri($"resource://Phonebook.Resources.Raw.Themes.{themeName}.xaml",
UriKind.Relative)
};
app.Resources.MergedDictionaries.Clear();
app.Resources.MergedDictionaries.Add(themeDict);
await Preferences.SetAsync(ThemePreferenceKey, (int)theme);
}
public async Task LoadSavedTheme()
{
var savedTheme = Preferences.Get(ThemePreferenceKey, -1);
if (savedTheme >= 0)
{
await ApplyTheme((AppThemeType)savedTheme);
}
else
{
await ApplyTheme(AppThemeType.Green);
}
}
public AppThemeType GetCurrentTheme()
{
var savedTheme = Preferences.Get(ThemePreferenceKey, -1);
return savedTheme >= 0 ? (AppThemeType)savedTheme : AppThemeType.Green;
}
}
}
```
**Описание методов:**
| Метод | Описание |
|-------|----------|
| `ApplyTheme()` | Динамически загружает и применяет тему из XAML-ресурсов |
| `LoadSavedTheme()` | Загружает сохранённую тему из Preferences при запуске |
| `GetCurrentTheme()` | Возвращает текущую активную тему |
**[Скриншот: Страница настроек с выбором тем]**
### 3.6 Главная страница
Файл: `MainPage.xaml`
```xml
<?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"
xmlns:local="clr-namespace:Phonebook"
xmlns:converters="clr-namespace:Phonebook.Converters"
Title="Телефоный справочник">
<ContentPage.Resources>
<converters:Base64ToPhoto x:Key="Base64ToPhoto"/>
<converters:NullToBoolConverter x:Key="NullToBool"/>
</ContentPage.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<SearchBar x:Name="searchBar"
Grid.Row="0"
Placeholder="Поиск контактов..."
SearchButtonPressed="SearchBar_SearchButtonPressed"
TextChanged="SearchBar_TextChanged"/>
<CollectionView x:Name="contactsCollection"
Grid.Row="1"
SelectionMode="Single"
SelectionChanged="ContactsCollection_SelectionChanged">
<CollectionView.ItemTemplate>
<DataTemplate>
<Frame Margin="10" Padding="10" CornerRadius="10">
<Grid ColumnDefinitions="Auto,*,Auto">
<Image Grid.Column="0"
Source="{Binding PhotoBase64,
Converter={StaticResource Base64ToPhoto}}"
WidthRequest="50" HeightRequest="50"
Aspect="AspectFill"
Margin="0,0,10,0"/>
<StackLayout Grid.Column="1" VerticalOptions="Center">
<Label Text="{Binding Name}" FontSize="18"
FontAttributes="Bold"/>
<Label Text="{Binding PhoneNumber}" FontSize="14"
TextColor="Gray"/>
<Label Text="{Binding Email}" FontSize="12"
TextColor="Gray" IsVisible="{Binding Email}"/>
</StackLayout>
<ImageButton Grid.Column="2"
Source="call_icon.png"
Clicked="CallButton_Clicked"
BackgroundColor="Transparent"
WidthRequest="40" HeightRequest="40"/>
</Grid>
</Frame>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
<ImageButton x:Name="fabAdd"
Grid.Row="1"
Source="add_icon.png"
Clicked="FabAdd_Clicked"
HorizontalOptions="End"
VerticalOptions="End"
Margin="0,0,20,20"
WidthRequest="60" HeightRequest="60"
BackgroundColor="{AppThemeBinding Light=#4CAF50, Dark=#4CAF50}"
CornerRadius="30"/>
</Grid>
</ContentPage>
```
**Код-behind:** `MainPage.xaml.cs`
```csharp
namespace Phonebook;
public partial class MainPage : ContentPage
{
private readonly ContactService _contactService;
public MainPage()
{
InitializeComponent();
_contactService = IPlatformApplication.Current!
.Services.GetService<ContactService>()!;
_contactService.OnContactsUpdated += LoadContacts;
LoadContacts();
}
private async void LoadContacts()
{
var contacts = await _contactService.GetAllContacts();
contactsCollection.ItemsSource = contacts;
}
private async void SearchBar_TextChanged(object sender, TextChangedEventArgs e)
{
var searchTerm = e.NewTextValue;
if (string.IsNullOrWhiteSpace(searchTerm))
{
LoadContacts();
return;
}
var contacts = await _contactService.SearchContacts(searchTerm);
contactsCollection.ItemsSource = contacts;
}
private void ContactsCollection_SelectionChanged(object sender,
SelectionChangedEventArgs e)
{
if (e.CurrentSelection.FirstOrDefault() is Contact contact)
{
Navigation.PushAsync(new ContactDetailPage(contact));
contactsCollection.SelectedItem = null;
}
}
private void FabAdd_Clicked(object sender, EventArgs e)
{
Navigation.PushAsync(new ContactDetailPage(null));
}
private async void CallButton_Clicked(object sender, EventArgs e)
{
if (sender is ImageButton button && button.BindingContext is Contact contact)
{
try
{
PhoneDialer.Open(contact.PhoneNumber);
}
catch
{
await DisplayAlert("Ошибка",
"Невозможно совершить звонок", "OK");
}
}
}
protected override void OnAppearing()
{
base.OnAppearing();
LoadContacts();
}
}
```
**Описание обработчиков событий:**
| Метод | Описание |
|-------|----------|
| `LoadContacts()` | Загружает список контактов из сервиса и привязывает к CollectionView |
| `SearchBar_TextChanged()` | Выполняет поиск при изменении текста в строке поиска |
| `ContactsCollection_SelectionChanged()` | Открывает страницу редактирования при выборе контакта |
| `FabAdd_Clicked()` | Открывает страницу создания нового контакта |
| `CallButton_Clicked()` | Инициирует звонок по номеру телефона контакта |
| `OnAppearing()` | Обновляет список при возвращении на страницу |
**[Скриншот: Главная страница приложения со списком контактов]**
### 3.7 Страница редактирования контакта
Файл: `ContactDetailPage.xaml`
```xml
<?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"
xmlns:converters="clr-namespace:Phonebook.Converters"
Title="{Binding Name, StringFormat='Контакт: {0}'}">
<ContentPage.Resources>
<converters:Base64ToPhoto x:Key="Base64ToPhoto"/>
<converters:NullToBoolConverter x:Key="NullToBool"/>
</ContentPage.Resources>
<ScrollView>
<StackLayout Padding="20" Spacing="15">
<Frame HorizontalOptions="Center"
WidthRequest="150" HeightRequest="150"
CornerRadius="75" Padding="0" Margin="0,0,0,20">
<Image x:Name="contactPhoto"
Source="{Binding PhotoBase64,
Converter={StaticResource Base64ToPhoto}}"
Aspect="AspectFill"/>
<Frame.GestureRecognizers>
<TapGestureRecognizer Tapped="PhotoTapped"/>
</Frame.GestureRecognizers>
</Frame>
<Entry x:Name="nameEntry"
Placeholder="Имя"
Text="{Binding Name}"/>
<Entry x:Name="phoneEntry"
Placeholder="Телефон"
Text="{Binding PhoneNumber}"
Keyboard="Telephone"/>
<Entry x:Name="emailEntry"
Placeholder="Email"
Text="{Binding Email}"
Keyboard="Email"/>
<Entry x:Name="addressEntry"
Placeholder="Адрес"
Text="{Binding Address}"/>
<Editor x:Name="descriptionEditor"
Placeholder="Описание"
Text="{Binding Description}"
HeightRequest="100"/>
<Button x:Name="saveButton"
Text="Сохранить"
Clicked="SaveButton_Clicked"
Margin="0,20,0,0"/>
<Button x:Name="deleteButton"
Text="Удалить"
Clicked="DeleteButton_Clicked"
BackgroundColor="Red"
IsVisible="{Binding Id, Converter={StaticResource NullToBool}}"/>
</StackLayout>
</ScrollView>
</ContentPage>
```
**Код-behind:** `ContactDetailPage.xaml.cs`
```csharp
public partial class ContactDetailPage : ContentPage
{
private readonly ContactService _contactService;
private Contact? _contact;
private string? _photoBase64;
public ContactDetailPage(Contact? contact)
{
InitializeComponent();
_contactService = IPlatformApplication.Current!
.Services.GetService<ContactService>()!;
_contact = contact ?? new Contact();
_photoBase64 = _contact.PhotoBase64;
BindingContext = _contact;
}
private async void PhotoTapped(object sender, TappedEventArgs e)
{
var result = await FilePicker.PickAsync(new PickOptions
{
PickerTitle = "Выберите фото",
FileTypes = FilePickerFileType.Images
});
if (result != null)
{
var stream = await result.OpenReadAsync();
_photoBase64 = ImageHelper.StreamToBase64(stream);
contactPhoto.Source = ImageHelper.Base64ToStreamImageSource(_photoBase64);
}
}
private async void SaveButton_Clicked(object sender, EventArgs e)
{
if (string.IsNullOrWhiteSpace(nameEntry.Text))
{
await DisplayAlert("Ошибка", "Введите имя контакта", "OK");
return;
}
_contact!.Name = nameEntry.Text;
_contact.PhoneNumber = phoneEntry.Text ?? "";
_contact.Email = emailEntry.Text ?? "";
_contact.Address = addressEntry.Text ?? "";
_contact.Description = descriptionEditor.Text ?? "";
_contact.PhotoBase64 = _photoBase64 ?? "";
await _contactService.SaveContact(_contact!);
await Navigation.PopAsync();
}
private async void DeleteButton_Clicked(object sender, EventArgs e)
{
var confirm = await DisplayAlert("Подтверждение",
"Вы уверены, что хотите удалить контакт?", "Да", "Нет");
if (confirm && _contact != null)
{
await _contactService.DeleteContact(_contact);
await Navigation.PopAsync();
}
}
}
```
**Описание обработчиков:**
| Метод | Описание |
|-------|----------|
| `PhotoTapped()` | Позволяет выбрать фото из галереи устройства и конвертирует в Base64 |
| `SaveButton_Clicked()` | Валидирует данные, сохраняет контакт через сервис |
| `DeleteButton_Clicked()` | Запрашивает подтверждение и удаляет контакт |
**[Скриншот: Страница редактирования контакта]**
### 3.8 Вспомогательные утилиты
#### ImageHelper.cs
```csharp
public static class ImageHelper
{
public static string StreamToBase64(Stream stream)
{
using var memoryStream = new MemoryStream();
stream.CopyTo(memoryStream);
return Convert.ToBase64String(memoryStream.ToArray());
}
public static ImageSource Base64ToStreamImageSource(string base64String)
{
if (string.IsNullOrWhiteSpace(base64String))
return ImageSource.FromFile("default_contact.png");
var cleanBase64 = base64String.Contains(',')
? base64String.Split(',')[1]
: base64String;
byte[] imageBytes = Convert.FromBase64String(cleanBase64);
return ImageSource.FromStream(() => new MemoryStream(imageBytes));
}
}
```
**Функции:**
| Функция | Описание |
|---------|----------|
| `StreamToBase64()` | Конвертирует поток изображения в строку Base64 |
| `Base64ToStreamImageSource()` | Конвертирует Base64 обратно в ImageSource |
**[Скриншот: Выбор фотографии из галереи]**
### 3.9 Конфигурация приложения
Файл: `MauiProgram.cs`
```csharp
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");
})
.UseMauiCommunityToolkit();
builder.Services.AddSingleton<DatabaseService>();
builder.Services.AddSingleton<ThemeService>();
builder.Services.AddSingleton<ContactService>();
var app = builder.Build();
Task.Run(async () =>
{
var dbService = app.Services.GetRequiredService<DatabaseService>();
await dbService.Initialize();
}).Wait();
var themeService = app.Services.GetRequiredService<ThemeService>();
themeService.LoadSavedTheme().Wait();
return app;
}
```
**[Скриншот: Конфигурация DI-контейнера]**
### 3.10 Разрешения Android
Файл: `Platforms/Android/AndroidManifest.xml`
```xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.WRITE_CONTACTS" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.INTERNET" />
<application ...>
</application>
</manifest>
```
### 3.11 Ресурсы тем
Файл: `Resources/Raw/Themes/GreenTheme.xaml`
```xml
<?xml version="1.0" encoding="utf-8" ?>
<ResourceDictionary xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml">
<Color x:Key="Primary">#4CAF50</Color>
<Color x:Key="PrimaryDark">#388E3C</Color>
<Color x:Key="PrimaryLight">#C8E6C9</Color>
<Color x:Key="Accent">#8BC34A</Color>
<Color x:Key="Background">#FFFFFF</Color>
<Color x:Key="Surface">#FFFFFF</Color>
<Color x:Key="TextPrimary">#212121</Color>
<Color x:Key="TextSecondary">#757575</Color>
<Style TargetType="Button">
<Setter Property="BackgroundColor" Value="{DynamicResource Primary}"/>
<Setter Property="TextColor" Value="White"/>
</Style>
<Style TargetType="NavigationPage">
<Setter Property="BarBackgroundColor" Value="{DynamicResource Primary}"/>
<Setter Property="BarTextColor" Value="White"/>
</Style>
</ResourceDictionary>
```
---
## 4. ЗАКЛЮЧЕНИЕ
### 4.1 Результаты выполнения работы
В ходе выполнения лабораторной работы было разработано кроссплатформенное мобильное приложение «Телефонный справочник» с использованием .NET MAUI и языка программирования C#.
**Достигнутые результаты:**
| № | Задача | Статус |
|---|--------|--------|
| 1 | Изучение теоретических основ .NET MAUI | ✅ Выполнено |
| 2 | Проектирование архитектуры приложения | ✅ Выполнено |
| 3 | Реализация CRUD-операций для контактов | ✅ Выполнено |
| 4 | Настройка хранения данных с SQLite | ✅ Выполнено |
| 5 | Реализация системы смены тем оформления | ✅ Выполнено |
| 6 | Добавление функционала импорта/экспорта | ✅ Выполнено |
| 7 | Тестирование приложения | ✅ Выполнено |
### 4.2 Функциональность приложения
Разработанное приложение обеспечивает следующий функционал:
- **Создание контактов** — добавление новых записей с фото, именем, телефоном, email, адресом и описанием
- **Редактирование контактов** — изменение любых полей существующих записей
- **Удаление контактов** — удаление отдельных записей с подтверждением
- **Поиск контактов** — мгновенный поиск по всем полям с учётом локализации
- **Импорт из устройства** — получение контактов из телефонной книги
- **Экспорт в Excel** — сохранение списка контактов в формате XLSX
- **Смена темы** — переключение между зелёной, синей и тёмной темами
- **Быстрый звонок** — инициация звонка одним нажатием
### 4.3 Технические особенности
Приложение демонстрирует следующие современные подходы к разработке:
- **Кроссплатформенность** — единая кодовая база для iOS, Android, Windows, macOS
- **Архитектурный паттерн** — сервис-ориентированная архитектура с внедрением зависимостей
- **Асинхронное программирование** — использование async/await для неблокирующих операций
- **Локальное хранение данных** — SQLite для структурированных данных
- **Событийная модель** — паттерн Observer для обновления UI при изменении данных
### 4.4 Выводы
Выбор .NET MAUI и C# для разработки мобильного приложения полностью оправдал себя. Фреймворк обеспечил:
1. **Эффективность разработки** — единая кодовая база значительно сократила время разработки
2. **Качество кода** — C# с его строгой типизацией и LINQ обеспечил чистоту и поддерживаемость кода
3. **Производительность** — нативная компиляция обеспечила быстрый отклик интерфейса
4. **Масштабируемость** — архитектура приложения позволяет легко добавлять новый функционал
**Цель лабораторной работы достигнута** — создано полнофункциональное кроссплатформенное приложение «Телефонный справочник», готовое к использованию и дальнейшей доработке.
---
## 5. СПИСОК ИСПОЛЬЗОВАННЫХ ИСТОЧНИКОВ
1. Microsoft Docs. **.NET MAUI Documentation** — https://docs.microsoft.com/dotnet/maui
2. Microsoft Docs. **.NET 9 Documentation** — https://docs.microsoft.com/dotnet
3. C# Documentation. **C# 12 Guide** — https://docs.microsoft.com/dotnet/csharp
4. SQLite-net. **SQLite for .NET** — https://github.com/praeclarum/sqlite-net
5. ClosedXML. **Excel library for .NET** — https://github.com/ClosedXML/ClosedXML
6. Microsoft. **CommunityToolkit.Maui** — https://github.com/CommunityToolkit/Maui
7. Microsoft Learn. **Build mobile and desktop apps with .NET MAUI** — https://learn.microsoft.com/training/paths/build-apps-with-dotnet-maui/
8. Петцольд Ч. **Programming Windows, 6th Edition** — Microsoft Press, 2012
9. Troelsen A., Japikse P. **Pro C# 10 with .NET 6** — Apress, 2022
---
**Приложение А. Скриншоты**
| № | Описание | Статус |
|---|----------|--------|
| 1 | Структура проекта | ⬜ |
| 2 | Главная страница со списком контактов | ⬜ |
| 3 | Страница добавления контакта | ⬜ |
| 4 | Страница редактирования контакта | ⬜ |
| 5 | Выбор фотографии из галереи | ⬜ |
| 6 | Диалог поиска контактов | ⬜ |
| 7 | Экспорт в Excel | ⬜ |
| 8 | Страница настроек с выбором тем | ⬜ |
| 9 | Зелёная тема оформления | ⬜ |
| 10 | Синяя тема оформления | ⬜ |
| 11 | Тёмная тема оформления | ⬜ |
---