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

62 KiB
Raw Permalink Blame History

ЛАБОРАТОРНАЯ РАБОТА

Разработка кроссплатформенного мобильного приложения «Телефонный справочник» с использованием .NET MAUI и C#


СОДЕРЖАНИЕ

  1. Введение
  2. Теоретическая часть
  3. Практическая часть
  4. Заключение
  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):

// Паттерн асинхронного программирования
public async Task<List<Contact>> GetContactsAsync()
{
    var contacts = await _database.Table<Contact>().ToListAsync();
    return contacts.OrderBy(c => c.Name).ToList();
}

LINQ (Language Integrated Query):

// Запросы к коллекциям на уровне языка
var results = contacts
    .Where(c => c.Name.Contains(searchTerm))
    .OrderBy(c => c.Name)
    .Select(c => new { c.Name, c.PhoneNumber });

Обнуляемые ссылочные типы:

// ? делает тип обнуляемым
private Contact? _contact;
private string? _photoBase64;

// ! оператор подавления null-предупреждения
_contactService.OnContactsUpdated += LoadContacts;

2.1.4 XAML

XAML (Extensible Application Markup Language) — декларативный язык разметки для определения пользовательского интерфейса в .NET-приложениях.

Синтаксис XAML:

<!-- Пространства имён -->
<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):

<!-- Односторонняя привязка -->
<Label Text="{Binding Name}" />

<!-- Привязка с конвертером -->
<Image Source="{Binding PhotoBase64, 
         Converter={StaticResource Base64ToPhoto}}" />

<!-- Привязка с AppTheme -->
<Button BackgroundColor="{AppThemeBinding Light=#4CAF50, Dark=#333}" />

Ресурсы и стили:

<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:

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:

// Создание таблицы
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
  • Поддержка шифрования на мобильных платформах
// Сохранение настройки
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:

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> Привязка интерфейса к реализации

Получение сервисов:

// В 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:

var themeDict = new ResourceDictionary
{
    Source = new Uri("resource://Phonebook.Resources.Raw.Themes.GreenTheme.xaml")
};

app.Resources.MergedDictionaries.Clear();
app.Resources.MergedDictionaries.Add(themeDict);

AppThemeBinding позволяет автоматически адаптировать цвета:

<Button BackgroundColor="{AppThemeBinding Light=#4CAF50, Dark=#333}" 
        TextColor="{AppThemeBinding Light=White, Dark=White}"/>

2.8 Навигация в MAUI

В приложении используется иерархическая навигация на основе NavigationPage:

// Переход к новой странице
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):

<Shell>
    <FlyoutItem Title="Контакты">
        <ShellContent ContentTemplate="{DataTemplate local:MainPage}" />
    </FlyoutItem>
    <FlyoutItem Title="Настройки">
        <ShellContent ContentTemplate="{DataTemplate local:SettingsPage}" />
    </FlyoutItem>
</Shell>

2.9 Обработка событий и делегаты

В приложении используется паттерн Observer (Наблюдатель) для оповещения об изменениях:

// Определение события в сервисе
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 Разрешения, информация об устройстве

Пример работы с контактами:

var deviceContacts = await ContactPicker.PickContactAsync();
var contact = new Contact
{
    Name = deviceContacts.DisplayName ?? "",
    PhoneNumber = deviceContacts.Phones?.FirstOrDefault()?.Number ?? ""
};

Пример инициации звонка:

PhoneDialer.Open(phoneNumber);

2.11 Конвертеры значений (Value Converters)

Конвертеры позволяют преобразовывать данные при привязке:

// Реализация 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

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

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

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

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 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

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 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

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

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

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

<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 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 Documentationhttps://docs.microsoft.com/dotnet/maui
  2. Microsoft Docs. .NET 9 Documentationhttps://docs.microsoft.com/dotnet
  3. C# Documentation. C# 12 Guidehttps://docs.microsoft.com/dotnet/csharp
  4. SQLite-net. SQLite for .NEThttps://github.com/praeclarum/sqlite-net
  5. ClosedXML. Excel library for .NEThttps://github.com/ClosedXML/ClosedXML
  6. Microsoft. CommunityToolkit.Mauihttps://github.com/CommunityToolkit/Maui
  7. Microsoft Learn. Build mobile and desktop apps with .NET MAUIhttps://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 Тёмная тема оформления