Optimize price-list name search for large catalogs.

Replace O(n^2) per-keystroke rebuild with indexed unique names, a single-pass filter, and debounce so typing stays responsive with ~15k items.
This commit is contained in:
Magomed 2026-07-30 18:35:57 +03:00
parent 9dfdb913cb
commit 09535e9b02

View File

@ -49,6 +49,11 @@ namespace Электронная_Фармация.UserControls
ModernSplitContainer splitNameMgBrowse;
ModernDataGridView dgvBrowseNames;
ModernDataGridView dgvBrowseMg;
List<string> _uniqueBaseNames = new List<string>();
Dictionary<string, List<string>> _mgByBaseName =
new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase);
Timer _searchDebounceTimer;
const int SearchDebounceMs = 180;
static readonly Regex DosageRegex = new Regex(
@"(\d+(?:[.,]\d+)?\s*(?:мг|мкг|г|мл|МЕ|ме)\b)",
RegexOptions.IgnoreCase | RegexOptions.Compiled);
@ -407,11 +412,77 @@ namespace Электронная_Фармация.UserControls
EnsureDerivedBrowseColumns(tablePrice);
_fullPriceTable = tablePrice;
RebuildBrowseIndex(tablePrice);
_selectedBaseName = string.Empty;
_selectedMg = string.Empty;
ShowBrowseView();
}
private void RebuildBrowseIndex(DataTable tablePrice)
{
var names = new SortedSet<string>(StringComparer.OrdinalIgnoreCase);
var mgMap = new Dictionary<string, SortedSet<string>>(StringComparer.OrdinalIgnoreCase);
foreach (DataRow row in tablePrice.Rows)
{
var baseName = row["Базовое наименование"]?.ToString() ?? string.Empty;
if (string.IsNullOrWhiteSpace(baseName))
{
continue;
}
names.Add(baseName);
if (!mgMap.TryGetValue(baseName, out var dosages))
{
dosages = new SortedSet<string>(StringComparer.OrdinalIgnoreCase);
mgMap[baseName] = dosages;
}
dosages.Add(row["МГ"]?.ToString() ?? string.Empty);
}
_uniqueBaseNames = names.ToList();
_mgByBaseName = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase);
foreach (var pair in mgMap)
{
var list = new List<string>(pair.Value.Count);
foreach (var dosage in pair.Value)
{
list.Add(string.IsNullOrEmpty(dosage) ? EmptyMgDisplay : dosage);
}
_mgByBaseName[pair.Key] = list;
}
}
private void EnsureSearchDebounceTimer()
{
if (_searchDebounceTimer != null)
{
return;
}
_searchDebounceTimer = new Timer { Interval = SearchDebounceMs };
_searchDebounceTimer.Tick += (_, __) =>
{
_searchDebounceTimer.Stop();
ResetBrowseToNames();
};
}
private void ScheduleBrowseSearchRefresh(bool immediate = false)
{
EnsureSearchDebounceTimer();
_searchDebounceTimer.Stop();
if (immediate)
{
ResetBrowseToNames();
return;
}
_searchDebounceTimer.Start();
}
private void HideOptionalColumn(string columnName)
{
if (dgvPriceList.Columns.Contains(columnName))
@ -506,55 +577,51 @@ namespace Электронная_Фармация.UserControls
}
var search = (GoodsFilter ?? string.Empty).Trim();
var namesTable = new DataTable();
namesTable.Columns.Add("Наименование", typeof(string));
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (DataRow row in _fullPriceTable.Rows)
IList<string> namesToShow;
if (string.IsNullOrWhiteSpace(search))
{
var baseName = row["Базовое наименование"]?.ToString() ?? string.Empty;
if (string.IsNullOrWhiteSpace(baseName) || !seen.Add(baseName))
namesToShow = _uniqueBaseNames;
}
else
{
var matching = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (DataRow row in _fullPriceTable.Rows)
{
continue;
}
if (!string.IsNullOrWhiteSpace(search))
{
var keep = false;
foreach (DataRow candidate in _fullPriceTable.Rows)
{
if (!string.Equals(candidate["Базовое наименование"]?.ToString(), baseName, StringComparison.OrdinalIgnoreCase))
{
continue;
}
var drugName = candidate["Наименование"]?.ToString() ?? string.Empty;
var mgText = candidate["МГ"]?.ToString() ?? string.Empty;
if (baseName.IndexOf(search, StringComparison.OrdinalIgnoreCase) >= 0
|| drugName.IndexOf(search, StringComparison.OrdinalIgnoreCase) >= 0
|| mgText.IndexOf(search, StringComparison.OrdinalIgnoreCase) >= 0)
{
keep = true;
break;
}
}
if (!keep)
var baseName = row["Базовое наименование"]?.ToString() ?? string.Empty;
if (string.IsNullOrWhiteSpace(baseName) || matching.Contains(baseName))
{
continue;
}
var drugName = row["Наименование"]?.ToString() ?? string.Empty;
var mgText = row["МГ"]?.ToString() ?? string.Empty;
if (baseName.IndexOf(search, StringComparison.OrdinalIgnoreCase) >= 0
|| drugName.IndexOf(search, StringComparison.OrdinalIgnoreCase) >= 0
|| mgText.IndexOf(search, StringComparison.OrdinalIgnoreCase) >= 0)
{
matching.Add(baseName);
}
}
namesTable.Rows.Add(baseName);
namesToShow = matching
.OrderBy(n => n, StringComparer.OrdinalIgnoreCase)
.ToList();
}
namesTable.DefaultView.Sort = "Наименование ASC";
var sortedNames = namesTable.DefaultView.ToTable();
var namesTable = new DataTable();
namesTable.Columns.Add("Наименование", typeof(string));
namesTable.BeginLoadData();
foreach (var baseName in namesToShow)
{
namesTable.Rows.Add(baseName);
}
namesTable.EndLoadData();
try
{
_suppressBrowseEvents = true;
dgvBrowseNames.DataSource = sortedNames;
dgvBrowseNames.SuspendLayout();
dgvBrowseNames.DataSource = namesTable;
ConfigureSingleColumnGrid(dgvBrowseNames, "Наименование");
ClearMgPanel();
@ -569,6 +636,7 @@ namespace Электронная_Фармация.UserControls
}
finally
{
dgvBrowseNames.ResumeLayout();
_suppressBrowseEvents = false;
}
@ -595,28 +663,18 @@ namespace Электронная_Фармация.UserControls
var mgTable = new DataTable();
mgTable.Columns.Add("МГ", typeof(string));
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (DataRow row in _fullPriceTable.Rows)
if (_mgByBaseName.TryGetValue(baseName, out var dosages))
{
if (!string.Equals(row["Базовое наименование"]?.ToString(), baseName, StringComparison.OrdinalIgnoreCase))
foreach (var dosage in dosages)
{
continue;
mgTable.Rows.Add(dosage);
}
var dosage = row["МГ"]?.ToString() ?? string.Empty;
if (!seen.Add(dosage))
{
continue;
}
mgTable.Rows.Add(string.IsNullOrEmpty(dosage) ? EmptyMgDisplay : dosage);
}
mgTable.DefaultView.Sort = "МГ ASC";
try
{
_suppressBrowseEvents = true;
dgvBrowseMg.DataSource = mgTable.DefaultView.ToTable();
dgvBrowseMg.DataSource = mgTable;
ConfigureSingleColumnGrid(dgvBrowseMg, "МГ");
dgvBrowseMg.ClearSelection();
}
@ -990,7 +1048,7 @@ namespace Электронная_Фармация.UserControls
GoodsFilter = GoodsFilter.Substring(0, GoodsFilter.Length - 1);
txtSearchGoodNameInPriceList.Text = GoodsFilter;
ResetBrowseToNames();
ScheduleBrowseSearchRefresh(immediate: GoodsFilter.Length == 0);
if (GoodsFilter.Length == 0)
{
@ -1501,7 +1559,7 @@ select strftime('%Y-%m-%d','{OrderDate}'),
{
txtSearchGoodNameInPriceList.Text = string.Empty;
GoodsFilter = string.Empty;
ResetBrowseToNames();
ScheduleBrowseSearchRefresh(immediate: true);
//loadPriceList(string.Empty);
}
@ -1560,6 +1618,27 @@ and tempOrder.GoodName = PriceList.GoodName
string GoodsFilter = string.Empty;
string stringIWantBuy = string.Empty;
private static bool IsGoodsSearchKey(char keyChar, bool allowDigitsAndSeparators = true)
{
if ((keyChar >= 'A' && keyChar <= 'Z')
|| (keyChar >= 'a' && keyChar <= 'z')
|| (keyChar >= 'А' && keyChar <= 'Я')
|| (keyChar >= 'а' && keyChar <= 'я')
|| keyChar == (char)Keys.Subtract)
{
return true;
}
if (!allowDigitsAndSeparators)
{
return false;
}
return (keyChar >= '0' && keyChar <= '9')
|| keyChar == ' '
|| keyChar == '.'
|| keyChar == ',';
}
private void dgvPriceList_KeyPress(object sender, KeyPressEventArgs e)
{
@ -1573,7 +1652,7 @@ and tempOrder.GoodName = PriceList.GoodName
{
txtSearchGoodNameInPriceList.Text = string.Empty;
GoodsFilter = string.Empty;
ResetBrowseToNames();
ScheduleBrowseSearchRefresh(immediate: true);
}
return;
@ -1613,67 +1692,27 @@ and tempOrder.GoodName = PriceList.GoodName
if (_browseMode != PriceBrowseMode.Offers)
{
if ((e.KeyChar >= 'A' && e.KeyChar <= 'Z')
|| (e.KeyChar >= 'a' && e.KeyChar <= 'z')
|| (e.KeyChar >= 'А' && e.KeyChar <= 'Я')
|| (e.KeyChar >= 'а' && e.KeyChar <= 'я')
|| (e.KeyChar == (Char)Keys.Subtract))
if (IsGoodsSearchKey(e.KeyChar))
{
dgvPriceList.ClearSelection();
GoodsFilter += e.KeyChar.ToString();
txtSearchGoodNameInPriceList.Text = GoodsFilter;
ResetBrowseToNames();
if (dgvBrowseNames != null && dgvBrowseNames.Rows.Count == 0)
{
UiDialogs.ShowInfo("Подходящий товар не найден.", "Поиск", FindForm());
GoodsFilter = string.Empty;
txtSearchGoodNameInPriceList.Text = string.Empty;
ResetBrowseToNames();
}
ScheduleBrowseSearchRefresh();
timerForResetSearch.Enabled = true;
timerForResetSearch.Start();
}
return;
}
if ((e.KeyChar >= 'A' && e.KeyChar <= 'Z') || (e.KeyChar >= 'a' && e.KeyChar <= 'z') || (e.KeyChar >= 'А' && e.KeyChar <= 'Я') || (e.KeyChar >= 'а' && e.KeyChar <= 'я') || (e.KeyChar == (Char)Keys.Subtract))
if (IsGoodsSearchKey(e.KeyChar, allowDigitsAndSeparators: false))
{
if (e.KeyChar == (Char)Keys.Enter)
{
}
else
{
dgvPriceList.ClearSelection();
GoodsFilter += e.KeyChar.ToString();
txtSearchGoodNameInPriceList.Text = GoodsFilter;
#region поиск без фильтрации (НЕ РАБОТАЕТ)
//foreach(DataGridViewRow row in dgvPriceList.Rows)
//{
// //string cellValue = row.Cells["Наименование"].Value.ToString().ToLower();
// if (row.Cells[2].Value.ToString().Equals(GoodsFilter))
// {
// int indexOfSearchValue = row.Index;
// dgvPriceList.Rows[indexOfSearchValue].Selected = true;
// }
//}
#endregion
#region старый способ поиска с фильтрацией данных
if (!string.IsNullOrEmpty(GoodsFilter))
{
ResetBrowseToNames();
if (dgvPriceList.Rows.Count == 0)
{
UiDialogs.ShowInfo("Подходящий товар не найден.", "Поиск", FindForm());
GoodsFilter = string.Empty;
txtSearchGoodNameInPriceList.Text = string.Empty;
ResetBrowseToNames();
}
}
#endregion
}
dgvPriceList.ClearSelection();
GoodsFilter += e.KeyChar.ToString();
txtSearchGoodNameInPriceList.Text = GoodsFilter;
ScheduleBrowseSearchRefresh();
timerForResetSearch.Enabled = true;
timerForResetSearch.Start();
return;
}
else if ((e.KeyChar >= (Char)Keys.D1 && e.KeyChar <= (Char)Keys.D9) ||
@ -2511,7 +2550,7 @@ and SumOrderedItems = '{SumOrder}'";
{
txtSearchGoodNameInPriceList.Text = string.Empty;
GoodsFilter = string.Empty;
ResetBrowseToNames();
ScheduleBrowseSearchRefresh(immediate: true);
timerForResetSearch.Enabled = false;
timerForResetSearch.Stop();
}