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