forked from amkovkov/GranuSightSoftware2
1) remove unused components: camera view (due it causes segmentation faults), compute resources and all related, illuminator controller (due in useless without camera view), image storage service, temperature and humidity service 2) database schema - reduce tables references, features storing in records themselves in compressed form, add records creation and editing date and time, add separator comment column 3) analysis - rework of pipeline and ui, now database storing only raw data and all display values calculated from it 4) lights service - add reconnection if disconnected 5) add width and height command line arguments 6) fix some typos and other issues
429 lines
20 KiB
C#
429 lines
20 KiB
C#
using System.Collections.ObjectModel;
|
|
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
using CommunityToolkit.Mvvm.ComponentModel;
|
|
using CommunityToolkit.Mvvm.Input;
|
|
|
|
using GSS2.Core.Analysis.AmineContent.Database;
|
|
|
|
using GSS2.UI.Core.ViewModels;
|
|
using GSS2.UI.Core.Services;
|
|
|
|
namespace GSS2.ViewModels.AmineContent;
|
|
|
|
public partial class BrandCalibrationViewModel : ViewModelBase
|
|
{
|
|
/// <summary>
|
|
/// Внутренняя модель элементов меню
|
|
/// </summary>
|
|
public partial class MenuItemViewModel : ViewModelBase
|
|
{
|
|
[ObservableProperty] public partial int Id { get; set; }
|
|
[ObservableProperty] public partial string BrandName { get; set; }
|
|
[ObservableProperty] public partial string MixtureName { get; set; }
|
|
|
|
public MenuItemViewModel(int id, string brandName, string mixtureName)
|
|
{
|
|
Id = id;
|
|
BrandName = brandName;
|
|
MixtureName = mixtureName;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Внутренняя модель записи
|
|
/// </summary>
|
|
public partial class RecordViewModel : ViewModelBase
|
|
{
|
|
/// <summary>
|
|
/// <see cref="BrandRecord.Id"/>
|
|
/// </summary>
|
|
[ObservableProperty] public partial int Id { get; set; } = 0;
|
|
/// <summary>
|
|
/// <see cref="BrandRecord.Uuid"/>
|
|
/// </summary>
|
|
[ObservableProperty] public partial string Uuid { get; set; } = Guid.NewGuid().ToString();
|
|
/// <summary>
|
|
/// <see cref="SampleRecord.CreateDateTime"/>
|
|
/// </summary>
|
|
[ObservableProperty] public partial DateTime CreateDateTime { get; set; } = DateTime.UtcNow;
|
|
/// <summary>
|
|
/// <see cref="SampleRecord.EditDateTime"/>
|
|
/// </summary>
|
|
[ObservableProperty] public partial DateTime EditDateTime { get; set; } = DateTime.UtcNow;
|
|
/// <summary>
|
|
/// <see cref="BrandRecord.BrandName"/>
|
|
/// </summary>
|
|
[ObservableProperty] public partial string BrandName { get; set; } = "";
|
|
/// <summary>
|
|
/// <see cref="BrandRecord.MixtureName"/>
|
|
/// </summary>
|
|
[ObservableProperty] public partial string MixtureName { get; set; } = "";
|
|
/// <summary>
|
|
/// <see cref="BrandRecord.MixtureNormalRate"/>
|
|
/// </summary>
|
|
[ObservableProperty] public partial double MixtureNormalRate { get; set; } = -1;
|
|
/// <summary>
|
|
/// <see cref="BrandRecord.Comment"/>
|
|
/// </summary>
|
|
[ObservableProperty] public partial string Comment { get; set; } = "";
|
|
}
|
|
|
|
/// <summary>
|
|
/// Логгер
|
|
/// </summary>
|
|
private readonly ILogger<BrandCalibrationViewModel> _logger;
|
|
/// <summary>
|
|
/// Контекст базы данных
|
|
/// </summary>
|
|
private readonly Context _context;
|
|
/// <summary>
|
|
/// ViewLocator для получения других View по их моделям
|
|
/// </summary>
|
|
private readonly DependencyInjectionViewLocator _viewLocator;
|
|
/// <summary>
|
|
/// Сервис навигации между экранами
|
|
/// </summary>
|
|
private readonly NavigationService _navigation;
|
|
|
|
/// <summary>
|
|
/// Меню открыто
|
|
/// </summary>
|
|
[ObservableProperty] public partial bool MenuOpened { get; set; } = false;
|
|
/// <summary>
|
|
/// Элементы меню
|
|
/// </summary>
|
|
[ObservableProperty] public partial ObservableCollection<MenuItemViewModel> MenuItems { get; set; } = new();
|
|
/// <summary>
|
|
/// Текущий выбранный элемент меню. Равен -1 если элемент не выбран
|
|
/// </summary>
|
|
[ObservableProperty] public partial int MenuSelectedIndex { get; set; } = -1;
|
|
|
|
/// <summary>
|
|
/// Запись
|
|
/// </summary>
|
|
[ObservableProperty] public partial RecordViewModel Record { get; set; } = new();
|
|
|
|
/// <summary>
|
|
/// Текущий экран информационной секции
|
|
/// </summary>
|
|
[ObservableProperty] public partial int InfoSectionCurrentIndex { get; set; } = 0;
|
|
/// <summary>
|
|
/// Можно ли переходить на следующий экран информационной секции
|
|
/// </summary>
|
|
[ObservableProperty] public partial bool InfoSectionCanScrollBackward { get; set; } = false;
|
|
/// <summary>
|
|
/// Можно ли переходить на предыдущий экран информационной секции
|
|
/// </summary>
|
|
[ObservableProperty] public partial bool InfoSectionCanScrollForward { get; set; } = false;
|
|
/// <summary>
|
|
/// Происходит ли переход между экранами информационной секции
|
|
/// </summary>
|
|
[ObservableProperty] public partial bool InfoSectionIsScrolling { get; set; } = false;
|
|
|
|
/// <summary>
|
|
/// Можно ли взаимодействовать с объектом
|
|
/// </summary>
|
|
[ObservableProperty] public partial bool CanInteract { get; set; } = true;
|
|
|
|
public BrandCalibrationViewModel(
|
|
ILogger<BrandCalibrationViewModel> logger,
|
|
Context context,
|
|
DependencyInjectionViewLocator viewLocator,
|
|
NavigationService navigation
|
|
)
|
|
{
|
|
// Устанавливаем readonly поля
|
|
_logger = logger;
|
|
_context = context;
|
|
_viewLocator = viewLocator;
|
|
_navigation = navigation;
|
|
_logger.LogInformation("Инициализация");
|
|
|
|
// Получаем записи из базы данных
|
|
MenuItems = new ObservableCollection<MenuItemViewModel>(_context.BrandRecords.Select(r => new MenuItemViewModel(r.Id, r.BrandName, r.MixtureName)));
|
|
if (MenuItems.Count() > 0) // Если записи есть,
|
|
MenuSelectedIndex = 0; // Выбираем первую запись
|
|
else // Если записей нет,
|
|
MenuSelectedIndex = -1; // Устанавливаем, что запись не выбрана
|
|
|
|
// Подписываемся на изменения свойств для обновления CanScroll
|
|
PropertyChanged += (_, ea) =>
|
|
{
|
|
if (ea.PropertyName == nameof(MenuOpened) ||
|
|
ea.PropertyName == nameof(InfoSectionIsScrolling))
|
|
UpdateCanInteract();
|
|
};
|
|
|
|
_logger.LogInformation("Инициализировано");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Вызывается при изменении <see cref="MenuSelectedIndex"/>
|
|
/// </summary>
|
|
partial void OnMenuSelectedIndexChanged(int value)
|
|
{
|
|
// Если значение выходит за верхнюю границу списка элементов меню, то логгируем как ошибку
|
|
if (value >= MenuItems.Count())
|
|
_logger.LogError("Значение {}({}) больше количества элементов {}({})", nameof(MenuSelectedIndex), MenuSelectedIndex, nameof(MenuItems), MenuItems.Count());
|
|
// Если значение больше или равно 0, то значит выбран один из элементов меню
|
|
if (value >= 0 && value < MenuItems.Count())
|
|
{
|
|
_logger.LogTrace("Выбран элемент из списка: {}", value);
|
|
// Находим запись в базе данных
|
|
var selectedRecord = _context.BrandRecords.Find(MenuItems[value].Id);
|
|
// Если запись найдена, то заполняем поля из базы данных
|
|
if (selectedRecord is not null)
|
|
{
|
|
Record.Id = selectedRecord.Id;
|
|
Record.Uuid = selectedRecord.Uuid;
|
|
Record.CreateDateTime = selectedRecord.CreateDateTime;
|
|
Record.EditDateTime = selectedRecord.EditDateTime;
|
|
Record.BrandName = selectedRecord.BrandName;
|
|
Record.MixtureName = selectedRecord.MixtureName;
|
|
Record.MixtureNormalRate = selectedRecord.MixtureNormalRate;
|
|
Record.Comment = selectedRecord.Comment;
|
|
return;
|
|
}
|
|
// Если значение не найдено в базе данных
|
|
else
|
|
{
|
|
// Логгируем как ошибку
|
|
_logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", MenuItems[value].Id, nameof(_context.BrandRecords));
|
|
var nonExistingIndex = MenuSelectedIndex; // Сохраняем несуществующий индекс
|
|
// Устанавливаем, что запись не выбрана, этот метод вызовется повторно и заполнит поля пустыми значениями
|
|
MenuSelectedIndex = -1;
|
|
// Удаляем несуществующий элемент из списка
|
|
MenuItems.RemoveAt(nonExistingIndex);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Если элемент не выбран, заполняем поля пустыми значениями
|
|
_logger.LogTrace("Выбран новый элемент");
|
|
var dateTime = DateTime.UtcNow;
|
|
Record.Id = 0;
|
|
Record.Uuid = Guid.NewGuid().ToString();
|
|
Record.CreateDateTime = dateTime;
|
|
Record.EditDateTime = dateTime;
|
|
Record.BrandName = "";
|
|
Record.MixtureName = "";
|
|
Record.MixtureNormalRate = -1;
|
|
Record.Comment = "";
|
|
}
|
|
|
|
/// <summary>
|
|
/// Обновить значение <see cref="CanInteract"/>
|
|
/// </summary>
|
|
private void UpdateCanInteract()
|
|
{
|
|
CanInteract = !MenuOpened;
|
|
CanInteract &= !InfoSectionIsScrolling;
|
|
}
|
|
|
|
|
|
// Команды управления меню
|
|
/// <summary>
|
|
/// Открыть меню
|
|
/// </summary>
|
|
[RelayCommand] private void OpenMenu() => MenuOpened = true;
|
|
/// <summary>
|
|
/// Закрыть меню
|
|
/// </summary>
|
|
[RelayCommand] private void CloseMenu() => MenuOpened = false;
|
|
/// <summary>
|
|
/// Переключить видимость меню
|
|
/// </summary>
|
|
[RelayCommand] private void ToggleMenu() => MenuOpened = !MenuOpened;
|
|
|
|
/// <summary>
|
|
/// Переключить информационную секцию на следующую
|
|
/// </summary>
|
|
[RelayCommand] private void InfoSectionPrevious() => InfoSectionCurrentIndex--;
|
|
/// <summary>
|
|
/// Переключить информационную секцию на предыдущую
|
|
/// </summary>
|
|
[RelayCommand] private void InfoSectionNext() => InfoSectionCurrentIndex++;
|
|
|
|
/// <summary>
|
|
/// Создать новую запись
|
|
/// </summary>
|
|
[RelayCommand]
|
|
private void CreateRecord()
|
|
{
|
|
_logger.LogInformation("Создание новой записи");
|
|
// Если уже создана новая запись, то MenuSelectedIndex может быть равен -1 и
|
|
// при простом присвоении MenuSelectedIndex = -1 событие OnMenuSelectedIndexChanged не вызовется,
|
|
// потому что событие срабатывает только при изменении значения, а значение было -1 и привилось ему -1,
|
|
// поэтому если SelectedRecord != -1, то нужно явно вызвать метод OnMenuSelectedIndexChanged
|
|
if (MenuSelectedIndex != -1)
|
|
MenuSelectedIndex = -1;
|
|
else
|
|
OnMenuSelectedIndexChanged(-1);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Удалить выбранную запись из базы данных или очистить значения, если запись не выбрана
|
|
/// </summary>
|
|
[RelayCommand]
|
|
private void DeleteRecord()
|
|
{
|
|
// TODO: Добавить определение и отображение количества затрагиваемых, записей которые будут повреждены при удалении этой записи
|
|
try
|
|
{
|
|
_logger.LogTrace("Удаление записи: {}={}, {}={}, {}={}", nameof(BrandRecord.Id), Record.Id, nameof(BrandRecord.BrandName), Record.BrandName, nameof(BrandRecord.MixtureName), Record.MixtureName);
|
|
// Если запись не выбрана, то просто создаём новую запись
|
|
if (MenuSelectedIndex < 0)
|
|
{
|
|
_logger.LogTrace("Запись не выбрана, создаём новую");
|
|
CreateRecord();
|
|
return;
|
|
}
|
|
|
|
// Создаём модель для экрана подтверждения
|
|
var confirmViewModel = new DeleteConfirmViewModel(_navigation)
|
|
{
|
|
Question = "Вы уверены, что хотите удалить запись?"
|
|
};
|
|
|
|
// Создаём коллбек который вызовется при экрана подтверждения
|
|
void OnConfirmCloses(object? sender, bool result)
|
|
{
|
|
// Если пользователь отменил удаление, то выходим из коллбека
|
|
if (!result)
|
|
{
|
|
_logger.LogTrace("Пользователь отменил удаление");
|
|
return;
|
|
}
|
|
|
|
// Если запись не выбрана, то просто создаём новую запись
|
|
if (MenuSelectedIndex < 0)
|
|
{
|
|
_logger.LogTrace("Запись не выбрана, создаём новую");
|
|
CreateRecord();
|
|
return;
|
|
}
|
|
|
|
// Сохраняем индекс удалённый записи и находим её в базе данных
|
|
var deletedMenuIndex = MenuSelectedIndex;
|
|
var deletedRecordId = Record.Id;
|
|
var deletedRecord = _context.BrandRecords.Find(deletedRecordId);
|
|
// Создаём новую запись
|
|
CreateRecord();
|
|
|
|
// Удаляем запись из списка
|
|
MenuItems.RemoveAt(deletedMenuIndex);
|
|
// Если удалённая запись не найдена в базе данных, то логгируем как ошибку
|
|
if (deletedRecord is null)
|
|
{
|
|
_logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", deletedRecordId, nameof(_context.BrandRecords));
|
|
return;
|
|
}
|
|
|
|
// Удаляем запись из базы данных и сохраняем изменения в базе данных
|
|
_context.BrandRecords.Remove(deletedRecord);
|
|
_context.SaveChanges();
|
|
_logger.LogTrace("Запись удалена");
|
|
|
|
// Отписываемся от события закрытия экрана подтверждения
|
|
confirmViewModel.Closes -= OnConfirmCloses;
|
|
}
|
|
|
|
// Создаём View экрана подтверждения
|
|
var confirmView = _viewLocator.Build(confirmViewModel);
|
|
// Если View не найден, то логгируем как ошибку
|
|
if (confirmView is null)
|
|
{
|
|
_logger.LogError("Не удалось найти View для экрана подтверждения");
|
|
return;
|
|
}
|
|
|
|
_logger.LogTrace("Задаём вопрос");
|
|
// Подписываемся на событие закрытия экрана подтверждения
|
|
confirmViewModel.Closes += OnConfirmCloses;
|
|
// Преходим на экран подтверждения, при закрытии он сам вернёт экран обратно на текущий
|
|
_navigation.NavigateTo(confirmView);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Исключение при удалении записи");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Сохранить текущие значения в новую запись или перезаписать значения в выдранной записи
|
|
/// </summary>
|
|
[RelayCommand]
|
|
private void SaveRecord()
|
|
{
|
|
try
|
|
{
|
|
_logger.LogTrace("Сохранение записи: {}={}, {}={}, {}={}", nameof(BrandRecord.Id), Record.Id, nameof(BrandRecord.BrandName), Record.BrandName, nameof(BrandRecord.MixtureName), Record.MixtureName);
|
|
// Если запись не выбрана
|
|
if (MenuSelectedIndex < 0)
|
|
{
|
|
_logger.LogTrace("Создаём новую запись");
|
|
var dateTime = DateTime.UtcNow;
|
|
// Создаём новую запись
|
|
var newRecord = new BrandRecord(
|
|
uuid: Record.Uuid,
|
|
createDateTime: dateTime,
|
|
editDateTime: dateTime,
|
|
brandName: Record.BrandName,
|
|
mixtureName: Record.MixtureName,
|
|
mixtureNormalRate: Record.MixtureNormalRate,
|
|
comment: Record.Comment
|
|
);
|
|
|
|
// Добавляем запись в базу данных
|
|
var attachedRecord = _context.BrandRecords.Add(newRecord);
|
|
|
|
// Сохраняем изменения в базе данных
|
|
_context.SaveChanges();
|
|
_logger.LogTrace("Запись сохранена");
|
|
|
|
// Добавляем новый элемент в меню
|
|
MenuItems.Add(new MenuItemViewModel(attachedRecord.Entity.Id, attachedRecord.Entity.BrandName, attachedRecord.Entity.MixtureName));
|
|
// Выбираем последний элемент из меню
|
|
MenuSelectedIndex = MenuItems.Count() - 1;
|
|
}
|
|
// Если запись выбрана
|
|
else
|
|
{
|
|
// Находим запись в базе данных
|
|
var attachedRecord = _context.BrandRecords.Find(Record.Id);
|
|
// Если запись не найдена, то логгируем как ошибку
|
|
if (attachedRecord is null)
|
|
{
|
|
_logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", Record.Id, nameof(_context.BrandRecords));
|
|
return;
|
|
}
|
|
|
|
_logger.LogTrace("Записываем изменения в базу данных");
|
|
|
|
// Записываем изменения
|
|
attachedRecord.EditDateTime = DateTime.UtcNow;
|
|
attachedRecord.BrandName = Record.BrandName;
|
|
attachedRecord.MixtureName = Record.MixtureName;
|
|
attachedRecord.MixtureNormalRate = Record.MixtureNormalRate;
|
|
attachedRecord.Comment = Record.Comment;
|
|
|
|
// Сохраняем изменения в базе данных
|
|
_context.SaveChanges();
|
|
_logger.LogTrace("Запись сохранена");
|
|
|
|
// Записываем изменения в меню
|
|
_logger.LogTrace("Записываем изменения в меню");
|
|
MenuItems[MenuSelectedIndex].BrandName = Record.BrandName;
|
|
MenuItems[MenuSelectedIndex].MixtureName = Record.MixtureName;
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Исключение при сохранении записи");
|
|
}
|
|
}
|
|
|
|
}
|