Lab 16
Console UI
Spectre.Console: таблиці, панелі, дерево, меню
Лаба 16 — Console UI (Spectre.Console)
Мета
Навчитися підключати сторонні NuGet-пакети і відокремлювати UI-логіку від бізнес-логіки. Замінити ручне малювання текстового інтерфейсу на готову бібліотеку Spectre.Console: інтерактивні меню, таблиці, панелі, дерева.
Контекст
Відкрийте ClinicApp/Program.cs і знайдіть будь-яку функцію меню — наприклад, PatientsMenu. Порахуйте, скільки там Console.WriteLine: кожен рядок меню виводиться вручну, кожен список — це цикл із Console.WriteLine. Звідси дві проблеми:
- Дублювання. Показ списку пацієнтів є в кількох меню — і скрізь однаковий. Нова колонка — шукаємо по всьому файлу.
- Змішування.
Program.csодночасно малює UI, читає ввід і викликає бізнес-методи. Будь-яка зміна у відображенні зачіпає логіку.
Рішення: виділити UI в окремий клас. Program.cs вирішує, що показати, а ClinicRenderer — як.
Структура проєкту на початку лаби
Це результат Лаби 15 — стан main після її злиття:
oop-course/ ← гілка main (після злиття Лаби 15)
├── .gitignore
├── oop-course.slnx
└── ClinicApp/
├── ClinicApp.csproj
├── Program.cs
├── Clinic.cs
├── Enums/ (4 файли)
├── Models/ (16 файлів)
├── Managers/ (13 файлів)
├── Utils/ (12 файлів)
├── Interfaces/ (4 файли)
├── Comparators/ (4 файли)
├── Attributes/ (3 файли)
├── Events/ (4 файли)
└── Extensions/ (3 файли)Структуру наприкінці лаби (з позначками, що створюється і змінюється в кожній задачі) наведено в розділі «Структура проєкту наприкінці лаби» перед перевіркою.
Сторонній пакет і фасад над ним
Досі ви використовували тільки вбудоване в .NET: Console, File, List<T>, LINQ. NuGet — менеджер пакетів .NET — дозволяє підключати бібліотеки інших розробників. Spectre.Console — бібліотека для консольного інтерфейсу: кольори, таблиці з рамками, меню зі стрілками, дерева, діаграми. Після dotnet add package у .csproj з'являється рядок <PackageReference>, і компілятор «бачить» класи бібліотеки так само, як вбудовані.
Можна одразу писати AnsiConsole.Write(...) у Program.cs, але тоді Program.cs залежить від Spectre.Console напряму — заміна бібліотеки означатиме переписати все. Правильно — статичний клас-фасад ClinicRenderer між ними:
Program.cs → ClinicRenderer → Spectre.ConsoleProgram.cs просить «покажи пацієнтів», а як саме — справа рендерера. Це той самий принцип єдиної відповідальності, якого ми дотримуємось з Лаби 03.
Що нового дозволено (і тільки воно)
- NuGet-пакет
Spectre.Consoleі його класи (AnsiConsole,Markup,Rule,SelectionPrompt,TextPrompt,ConfirmationPrompt,Table,Panel,Tree,BarChart,Status); switchза рядком вибраного пункту меню.
Крок 1. Гілка
Робочий процес (повністю — Git Воркшоп): лаба = гілка
Lab-XXвідmain, коміт на кожне завдання (LabXX TaskYY), у кінці — злиття вmain.
Проєкт ClinicApp/ уже існує. Тут лише нова гілка від main:
git checkout main
git checkout -b Lab-16Коміт — на кожне завдання (Lab16 TaskNN).
Ваш домен
За замовчуванням виконуйте завдання як написано (домен «клініка»). Для власного домену дивіться таблицю «Адаптація до вашого домену» в кінці кожного завдання.
Як користуватися підказками
Підказки — напрям думки, не готовий код. «Що реалізувати» і «Специфікація» кажуть що; підказки — як міркувати; блок 📖 Документація — де прочитати синтаксис. Спершу документація і власна спроба.
Задача 1. Підключити Spectre.Console і базові повідомлення ⭐⭐
Умова
Підключіть бібліотеку і створіть рендерер із базовими методами виводу. Замініть ними заголовки розділів, повідомлення про помилки й успіх у Program.cs.
Що реалізувати:
- Додати пакет у проєкт:
dotnet add ClinicApp package Spectre.Console. - Створити теку
ClinicApp/UI/і статичний класClinicRenderer(простір іменClinicApp.UI) з п'ятьма методами зі специфікації. - У
Program.csзамінити рядки-заголовки"── Назва ──"наPrintHeader, повідомлення вcatch— наPrintError, повідомлення про успіх — наPrintSuccess.
Специфікація
| Метод | Що робить |
|---|---|
PrintHeader(string title) |
горизонтальна лінія Rule з назвою розділу, вирівняною ліворуч |
PrintSuccess(string message) |
зелений текст із префіксом ✓ |
PrintError(string message) |
червоний текст із префіксом ✗ |
PrintWarning(string message) |
жовтий текст із префіксом ⚠ |
PrintInfo(string message) |
приглушений ([dim]) текст |
Приклад
AnsiConsole.MarkupLine("[red]✗ Помилка[/]"); // розмітка [колір]…[/] — як теги в HTML── Пацієнти ────────────────────────────────
✓ Пацієнта [6] Марія Ткач додано.
✗ Телефон має містити рівно 10 цифр.Підказки
AnsiConsole.MarkupLineрозбирає[red]…[/]як розмітку; звичайнийWriteLine— ні.Markup.Escape(text)— обов'язковий для будь-якого тексту з даних: якщо в імені є[або], вони стануть розміткою і зламають вивід (або програму). Екрануйте текст перед вставкою в розмітку.Ruleмає властивість вирівнювання заголовка — подивіться в документації.
📖 Документація:
Адаптація до вашого домену
| Клініка | Готель | Ресторан | Університет | Прокат авто | Бібліотека | Спортзал |
|---|---|---|---|---|---|---|
ClinicRenderer |
HotelRenderer |
RestaurantRenderer |
UniversityRenderer |
RentalRenderer |
LibraryRenderer |
GymRenderer |
Коміт
git add ClinicApp/ClinicApp.csproj ClinicApp/UI/ClinicRenderer.cs ClinicApp/Program.cs
git commit -m "Lab16 Task01"Задача 2. Меню зі стрілками і типізоване введення ⭐⭐
Умова
Зараз користувач бачить цифри і вводить 1; набрав 11 — потрапив у default. Замініть числові меню на вибір стрілками, а ручний розбір введення — на типізовані запити, що самі повторюють запит при помилці.
Що реалізувати:
- У
ClinicRendererдодати методи зі специфікації. - У
Program.csзамінити числові меню наSelectMenu; кожне підменю має пункт"← Назад". - Замінити
Console.Write(...)+int.TryParse(Console.ReadLine(), …)наPromptInt(і аналоги для рядків і чисел). - Замінити запити
y/nнаPromptConfirm.
Специфікація
| Метод | Повертає | На основі |
|---|---|---|
SelectMenu(string title, string[] options) |
вибраний рядок | SelectionPrompt<string> |
PromptInt(string label) |
int, повторює запит при невалідному вводі |
TextPrompt<int> |
PromptString(string label, bool allowEmpty = false) |
string |
TextPrompt<string> |
PromptDecimal(string label) |
decimal |
TextPrompt<decimal> |
PromptConfirm(string question) |
bool |
ConfirmationPrompt |
Приклад
Пацієнти
> Показати всіх
Додати пацієнта
Знайти за ім'ям
← НазадПідказки
SelectionPromptповертає той самий рядок, що був у варіантах, — томуswitchтепер перемикається за рядком.- Колір виділеного пункту задає
HighlightStyle. TextPrompt<int>сам показує повідомлення про помилку і повторює запит — текст повідомлення задаєValidationErrorMessage.- Необов'язковий рядок:
TextPrompt<string>зAllowEmpty(). "← Назад"повторюється в кожному меню — винесіть його в константу.
📖 Документація:
Адаптація до вашого домену
| Клініка | Готель | Ресторан | Університет | Прокат авто | Бібліотека | Спортзал |
|---|---|---|---|---|---|---|
| усі меню клініки | усі меню готелю | усі меню ресторану | усі меню університету | усі меню прокату | усі меню бібліотеки | усі меню спортзалу |
Коміт
git add ClinicApp/UI/ClinicRenderer.cs ClinicApp/Program.cs
git commit -m "Lab16 Task02"Задача 3. Таблиці для списків ⭐⭐
Умова
Списки виводяться рядками без вирівнювання: колонки «розповзаються», немає заголовків. Замініть їх таблицями, що самі вирівнюють колонки і малюють рамку.
Що реалізувати:
- У
ClinicRendererдодати три методи зі специфікації. - У
Program.csзамінитиclinic.Patients.DisplayAll()наClinicRenderer.RenderPatients(clinic.Patients.GetAll()); так само для лікарів і записів.
Специфікація
| Метод | Колонки | Кольори |
|---|---|---|
RenderPatients(IEnumerable<Patient>) |
ID, Ім'я, Вік, Група крові, Телефон | вік неповнолітнього — жовтим |
RenderDoctors(IEnumerable<Doctor>) |
ID, Ім'я, Спеціальність, Доступний, Розклад | доступний — зелене «Так», ні — приглушене «Ні» |
RenderAppointments(IEnumerable<Appointment>) |
ID, Тип, Пацієнт, Лікар, Дата/час, Вартість, Статус | скасовано — червоним; оплачено — зеленим; прострочено — приглушено; заплановано — жовтим; тип «терміновий» — жирним червоним |
Приклад
╭────┬───────────────┬─────┬───────┬────────────────╮
│ ID │ Ім'я │ Вік │ Кров │ Телефон │
├────┼───────────────┼─────┼───────┼────────────────┤
│ 1 │ Іван Петренко │ 41 │ A+ │ (050) 123-4567 │
│ 3 │ Максим Бойко │ 16 │ O+ │ (093) 345-6789 │
╰────┴───────────────┴─────┴───────┴────────────────╯Підказки
Tableсам обчислює ширину колонок; рамка —Border(TableBorder.Rounded); вирівнювання колонки —TableColumnзCentered()/RightAligned().- Кожна комірка
AddRow— рядок розмітки: значення з даних екрануйтеMarkup.Escape. - Тип запису —
switch-вираз за підкласом (UrgentAppointment => …). - Імена пацієнта й лікаря для записів беріть через менеджери, як у
DisplayAppointment.
📖 Документація:
Адаптація до вашого домену
| Клініка | Готель | Ресторан | Університет | Прокат авто | Бібліотека | Спортзал |
|---|---|---|---|---|---|---|
RenderPatients / RenderDoctors / RenderAppointments |
RenderGuests / RenderStaff / RenderBookings |
RenderCustomers / RenderWaiters / RenderReservations |
RenderStudents / RenderLecturers / RenderEnrollments |
RenderClients / RenderManagers / RenderRentals |
RenderReaders / RenderLibrarians / RenderLoans |
RenderMembers / RenderTrainers / RenderSessions |
Коміт
git add ClinicApp/UI/ClinicRenderer.cs ClinicApp/Program.cs
git commit -m "Lab16 Task03"Задача 4. Картки, дерево, діаграма і спіннер ⭐⭐⭐
Умова
Таблиця добре показує список, але для однієї сутності краще картка в рамці, для медичної картки — дерево за типами записів, для місячної виручки — діаграма. Додайте ці віджети і застосуйте їх у відповідних меню.
Що реалізувати:
- У
ClinicRendererдодати шість методів зі специфікації. - Застосувати їх у меню (таблиця «Де застосувати» нижче).
Специфікація
| Метод | Віджет | Вміст |
|---|---|---|
RenderPatientCard(Patient) |
Panel |
ID, дата народження, вік (з позначкою повнолітній/ні), група крові, телефон |
RenderDoctorCard(Doctor) |
Panel |
ID, спеціальність, ліцензія, телефон, розклад, доступність |
RenderMedicalRecord(Patient, IEnumerable<MedicalRecord>) |
Tree |
три гілки: діагнози, аналізи, рецепти; порожня гілка — «немає» |
RenderSpecialityStats(IEnumerable<SpecialityReport>) |
Table |
звіт по спеціальностях (Лаба 14) |
RenderMonthlyRevenue(IEnumerable<(int Year, int Month, decimal Total)>) |
BarChart |
виручка по місяцях (Лаба 14) |
WithSpinner(string message, Action action) |
Status |
показує спіннер, поки виконується дія |
| Де застосувати | Метод |
|---|---|
| «Медична картка» → «Картка пацієнта» | RenderMedicalRecord |
| «Черга» → «Хто перший?» | RenderPatientCard |
| «Файли» → «Експортувати всі звіти» | WithSpinner |
| «Звіти» → «Спеціальності» / «Виручка по місяцях» | RenderSpecialityStats / RenderMonthlyRevenue |
Приклад
Медична картка: Іван Петренко
├── Діагнози (2)
│ ├── I10 — Гіпертонічна хвороба ⟳ хронічне
│ └── J06.9 — Гострий ринофарингіт
├── Аналізи (2)
│ ├── ✓ Гемоглобін: 145 г/л (норма: 120–160)
│ └── ⚠ Холестерин: 6.2 ммоль/л (норма: < 5.2)
└── Рецепти (1)
└── Лізиноприл 10 мг × 30 днів [активний]Підказки
Panel— рамка навколо тексту з розміткою; заголовок —PanelHeader.Tree: гілки додаються до кореня, елементи — до гілок (AddNode).- Розкласти записи за типами допоможе
OfType<Diagnosis>()тощо (Лаба 14). BarChartприймає числаdouble— перетворітьdecimal.WithSpinnerприймаєAction— будь-яку дію, як у Лабі 15.
📖 Документація:
Адаптація до вашого домену
| Клініка | Готель | Ресторан | Університет | Прокат авто | Бібліотека | Спортзал |
|---|---|---|---|---|---|---|
медична картка (Tree) |
історія проживань | історія замовлень | залікова книжка | історія оренд | формуляр читача | щоденник тренувань |
Коміт
git add ClinicApp/UI/ClinicRenderer.cs ClinicApp/Program.cs
git commit -m "Lab16 Task04"Структура проєкту наприкінці лаби
Так має виглядати ClinicApp/, коли всі завдання виконано:
oop-course/ ← гілка Lab-16 (після злиття — main)
├── .gitignore
├── oop-course.slnx
└── ClinicApp/
├── ClinicApp.csproj ✏ Т1
├── Program.cs ✏ Т1 Т2 Т3 Т4
├── Clinic.cs
├── Enums/ (4 файли)
├── Models/ (16 файлів)
├── Managers/ (13 файлів)
├── Utils/ (12 файлів)
├── Interfaces/ (4 файли)
├── Comparators/ (4 файли)
├── Attributes/ (3 файли)
├── Events/ (4 файли)
├── Extensions/ (3 файли)
└── UI/
└── ClinicRenderer.cs 🆕 Т1 ✏ Т2 Т3 Т4Легенда: 🆕 — новий файл · ✏ — змінено вміст · Тn — номер задачі, у якій ви працюєте з файлом. Файли без позначки лишились такими, як були після Лаби 15.
Назви файлів наведено для домену «клініка»; у власному домені назви ваші — важливо, що саме створюється й змінюється.
Перевірка перед здачею
dotnet build ClinicApp
dotnet run --project ClinicAppПереконайтесь, що:
- Структура проєкту збігається зі схемою вище; у
ClinicApp.csprojєPackageReferenceнаSpectre.Console - Головне меню і підменю — вибір стрілками
- «Пацієнти» → «Показати всіх» — таблиця з рамкою, вік неповнолітніх жовтим
- «Записи» — таблиця з кольоровими статусами
- «Медична картка» → «Картка пацієнта» — дерево
- «Черга» → «Хто перший?» — картка в рамці
- «Файли» → «Експорт» — спіннер під час операції
- «Звіти» → «Виручка по місяцях» — стовпчаста діаграма
- Пацієнт з ім'ям, що містить
[або], виводиться без помилок - У
Program.csне лишилось прямих викликівAnsiConsole— лишеClinicRenderer
Питання для самоперевірки
- Що таке NuGet-пакет? Чим він відрізняється від стандартних бібліотек .NET?
- Навіщо
ClinicRenderer, якщо можна писатиAnsiConsole.Write(...)безпосередньо вProgram.cs? - Чому
Markup.Escape(text)обов'язковий для даних від користувача? Що станеться з ім'ям"Іван [молодший]"без нього? SelectionPromptповертає рядок, томуswitchпорівнює рядки. Скільки місць у коді треба оновити, якщо змінити текст пункту? Як це спростити?- Чим
TextPrompt<int>кращий заint.TryParse(Console.ReadLine())? - Чи варто залишати
DisplayAll()у менеджерах, якщо його роль тепер виконує рендерер?
Статус гілки
Після всіх 4 завдань (кожне — окремий коміт Lab16 TaskNN на гілці Lab-16):
git push -u origin Lab-16
git checkout main
git merge --no-ff Lab-16 -m "Merge Lab-16: Console UI"
git pushНаступна лаба:
git checkout main→git checkout -b Lab-17.