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:
parent
9dfdb913cb
commit
09535e9b02
@ -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);
|
||||
IList<string> namesToShow;
|
||||
if (string.IsNullOrWhiteSpace(search))
|
||||
{
|
||||
namesToShow = _uniqueBaseNames;
|
||||
}
|
||||
else
|
||||
{
|
||||
var matching = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (DataRow row in _fullPriceTable.Rows)
|
||||
{
|
||||
var baseName = row["Базовое наименование"]?.ToString() ?? string.Empty;
|
||||
if (string.IsNullOrWhiteSpace(baseName) || !seen.Add(baseName))
|
||||
if (string.IsNullOrWhiteSpace(baseName) || matching.Contains(baseName))
|
||||
{
|
||||
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;
|
||||
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)
|
||||
{
|
||||
keep = true;
|
||||
break;
|
||||
matching.Add(baseName);
|
||||
}
|
||||
}
|
||||
|
||||
if (!keep)
|
||||
namesToShow = matching
|
||||
.OrderBy(n => n, StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
var namesTable = new DataTable();
|
||||
namesTable.Columns.Add("Наименование", typeof(string));
|
||||
namesTable.BeginLoadData();
|
||||
foreach (var baseName in namesToShow)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
namesTable.Rows.Add(baseName);
|
||||
}
|
||||
|
||||
namesTable.DefaultView.Sort = "Наименование ASC";
|
||||
var sortedNames = namesTable.DefaultView.ToTable();
|
||||
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 (e.KeyChar == (Char)Keys.Enter)
|
||||
{
|
||||
|
||||
}
|
||||
else
|
||||
if (IsGoodsSearchKey(e.KeyChar, allowDigitsAndSeparators: false))
|
||||
{
|
||||
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
|
||||
}
|
||||
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();
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user