using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using Pf2eModel; using Pf2eModel.Model.Base; using Pf2eModel.Model.Common.I18n; using Pf2eModel.Model.Enum; using Timer = System.Windows.Forms.Timer; namespace Pf2eViewer.DatabaseViewer; /// /// Класс элемента управления, который отображает сущности в виде таблицы. /// public partial class DatabaseViewerControl : UserControl { #region "Поля и свойства" /// /// Контекст базы данных, который используется для получения сущностей. /// public readonly Pf2eDbContext Context = new(); /// /// Количество элементов на странице. /// [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public int PerPage { get => _perPage; set { _perPage = value; UpdateView(); } } private int _perPage = 30; /// /// Номер страницы, которую необходимо отобразить. /// [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public int Page { get => _page; set { _page = value; UpdateView(); } } private int _page = 1; /// /// Тип сущности, которую необходимо отобразить. /// [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public EntityType Type { get => _type; set { _page = 1; _type = value; UpdateView(); } } private EntityType _type; /// /// Количество элементов, которые были получены из базы данных. /// [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public int Count { get; private set; } /// /// Номер последней страницы, которая может быть отображена. /// public int MaxPage => Convert.ToInt32(Math.Ceiling(1.0 * Count / PerPage)); /// /// Выбранная в элементе управления сущность. /// [AllowNull] [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public EntityView CurrentEntity { get; private set; } /// /// Флаг, указывающий, что элемент управления занят выполнением запроса к базе данных. /// private bool _isBusy = true; /// /// Объект, который выполняет запрос к базе данных. /// private DatabaseQueryPerformer? _performer; /// /// Таймер, который используется для задержки обработки события изменения текста в поле поиска. /// private Timer? _typingTimer; /// /// Строка, которая хранит последнее значение, введенное в поле поиска. /// private string _lastSearch = ""; #endregion /// /// Конструктор класса, который инициализирует элемент управления и связывает его с источником данных. /// public DatabaseViewerControl() { InitializeComponent(); BindingNavigator.BindingSource = DbBindingSource; //DataGridView.AutoGenerateColumns = true; DataGridView.SelectionMode = DataGridViewSelectionMode.FullRowSelect; _isBusy = false; } #region "События элемента управления" /// /// Обработчик события, который вызывается при изменении текущей страницы в элементе управления. /// /// Источник события. /// Объект, содержащий данные события. private void BindingNavigator_RefreshItems(object sender, EventArgs e) { if (_isBusy) { return; } Page = (int)(BindingNavigator?.BindingSource?.Current ?? 1); UpdateView(); } /// /// Обработчик события, который вызывается при изменении выделенной строки в элементе управления DataGridView. /// /// Источник события. /// Объект, содержащий данные события. private void DataGridView_RowStateChanged(object sender, DataGridViewRowStateChangedEventArgs e) { if (_isBusy) { return; } UpdateEntity(); } /// /// Обработчик события, который вызывается когда поле поиска теряет фокус. /// /// Источник события. /// Объект, содержащий данные события. private void SearchTextBox_Leave(object sender, EventArgs e) { if (_isBusy || SearchTextBox.Text == _lastSearch) { return; } UpdateView(); } /// /// Обработчик события, который вызывается при изменении текста в поле поиска. /// /// Источник события. /// Объект, содержащий данные события. private void SearchTextBox_TextChanged(object sender, EventArgs e) { if (_isBusy) { return; } if (_typingTimer == null) { _typingTimer = new(); _typingTimer.Interval = 3000; _typingTimer.Tick += HandleTypingTimerTimeout; } _typingTimer.Stop(); // Resets the timer _typingTimer.Tag = (sender as TextBox)!.Text; // This should be done with EventArgs _typingTimer.Start(); } /// /// Обработчик события, который вызывается при истечении времени таймера ввода текста. /// /// Источник события. /// Объект, содержащий данные события. private void HandleTypingTimerTimeout(object? sender, EventArgs e) { if (_isBusy || sender is not Timer timer) { return; } UpdateView(); timer.Stop(); } #endregion #region "Обновление данных" /// /// Устанавливает статус занятости элемента управления. /// /// Новый статус занятости. private void SetBusyStatus(bool busy) { if (InvokeRequired) { Invoke(SetBusyStatus, busy); return; } _isBusy = busy; BindingNavigator.Enabled = !busy; DataGridView.Enabled = !busy; SearchTextBox.Enabled = !busy; DataGridView.ColumnHeadersHeightSizeMode = !busy ? DataGridViewColumnHeadersHeightSizeMode.AutoSize : DataGridViewColumnHeadersHeightSizeMode.DisableResizing; } /// /// Обновляет данные в элементе управления. /// /// Проигнорировать флаг занятости. public void UpdateView(bool force = false) { if (_isBusy && !force) { return; } SetBusyStatus(true); _performer?.Abort(); _performer = new() { Type = Type, Page = Page, PerPage = PerPage, Search = SearchTextBox.Text, }; _performer.OnQueryCompleted += HandleQueryCompleted; _performer.Query(); } /// /// Обработчик события, который вызывается по завершении выполнения запроса к базе данных. /// /// Источник события. /// Результат выполнения события private void HandleQueryCompleted(DatabaseQueryPerformer sender, DatabaseQueryResult result) { if (sender != _performer) { return; } Count = result.Count; _lastSearch = SearchTextBox.Text; InvokeControl( BindingNavigator, b => { PagesBindingSource.DataSource = Enumerable.Range(1, MaxPage).ToList(); b.BindingSource = PagesBindingSource; } ); InvokeControl( DataGridView, view => { view.DataSource = result .Entities.Select(e => new EntityGridRow(e)) .OrderBy(r => r.Id) .ToList(); SetBusyStatus(false); if (view.Rows.Count > 0) { view.Rows[0].Selected = true; UpdateEntity(); } } ); } /// /// Вызывает действие на указанном элементе управления в нужном потоке , если это необходимо. /// /// /// /// private void InvokeControl(T control, Action action) where T : Control { if (control.InvokeRequired) { control.Invoke(action, control); } else { action(control); } } /// /// Обновить сущность, которая была выбрана в элементе управления DataGridView. /// private void UpdateEntity() { if (DataGridView.SelectedRows.Count == 0) { return; } EntityGridRow? data = (EntityGridRow?)DataGridView.SelectedRows[0].DataBoundItem; try { CurrentEntity = data is not null ? new(Context, Type, data.Id) : null; } catch (Exception) { // ignored } EntityChange?.Invoke(CurrentEntity); } #endregion #region Output events /// /// Делегат, который будет вызван при изменении выбранной сущности. /// /// Новая выбранная сущность. public delegate void EntityChangeHandler(EntityView newEntity); /// /// Событие, которое вызывается при изменении выбранной сущности. /// public event EntityChangeHandler? EntityChange; #endregion /// /// Класс, который представляет строку в таблице сущностей. /// /// Сущность, связанная со строкой internal class EntityGridRow(Entity entity) { /// /// Идентификатор сущности. /// public int Id { get; set; } = entity.Id; /// /// Название сущности. /// public string Name { get; set; } = entity.Name[LocaleType.English]; /// /// Редкость сущности. /// public EntityRarity Rarity { get; set; } = entity.Rarity; } }