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