7 Commits

7 changed files with 1135 additions and 48 deletions
@@ -0,0 +1,652 @@
# 173/174 Extended Return Search Column and Local Filter Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Add a persistent search-column selector and DevExpress in-memory auto-filter row to the shared 173/174 popup without changing existing value display, ResultFields mapping, clearing, or selection behavior.
**Architecture:** Extend the shared SQL builder to distinguish visible searchable columns from hidden technical columns, then extend the shared popup to own column selection and local filtering state. The MyControl and grid hosts continue to own caption translation and asynchronous schema/query lifecycles; each request snapshots the selected real field name so background execution is deterministic.
**Tech Stack:** C# on .NET Framework 4.0, WinForms, DevExpress `PopupContainerEdit`/`ComboBoxEdit`/`GridControl`/`GridView`, ADO.NET provider factory, SQL Server-compatible derived-table SQL.
## Global Constraints
- Work in `E:\lserp_project\lserp_cs_6.0` on the current `main` branch and preserve unrelated work.
- Do not add NuGet dependencies or require a framework newer than .NET Framework 4.0.
- Keep 173 module SQL resolution and 174 configured SourceSql resolution unchanged.
- Keep ResultFields, ValueMember, TextMember, display-value translation, clear, mouse selection, arrow navigation, and Enter selection unchanged.
- Do not load complete lookup data during parent control initialization.
- Opening the popup may asynchronously execute only the existing `top 0` structure query.
- The selector displays translated result-grid captions but stores real source field names.
- Columns whose real names start with `_` are hidden from the selector and excluded from “所有列” database predicates.
- A new database search clears old DevExpress local filters and old results.
- DevExpress auto-filtering operates only on the returned result table and must not issue a database request.
- Preserve the last valid selector choice per popup instance; fall back to “所有列” only after a loaded schema proves that field no longer exists.
- The automatic popup-open lookup for an existing business value always searches all visible columns; it must not consume or reset the user's preserved selector choice.
- Keep the database result limit at 100 rows and continue using `@lookupKeyword` with existing LIKE escaping.
---
## File Map
- Modify `插件库/Lskj.Control/Model/ExtendedReturnSupport.cs`: central hidden-column rule and selected-column SQL generation.
- Modify `插件库/Lskj.Control/AutoGridLookUp/ExtendedReturnSearchPopup.cs`: selector UI, selection persistence, result/filter reset, and auto-filter row.
- Modify `插件库/Lskj.Control/AutoGridLookUp/LabelExtendedReturnSearchEdit.cs`: MyControl schema-only requests, translated selector options, and selected-field request snapshots.
- Modify `插件库/Lskj.Control/GridControlEx.ExtendedReturn.cs`: grid schema-only requests, source-aware schema cache, translated selector options, and selected-field request snapshots.
- Do not add production source files or change `Lskj.Control.csproj`.
---
### Task 1: Make selected-column SQL generation a tested shared contract
**Files:**
- Modify: `插件库/Lskj.Control/Model/ExtendedReturnSupport.cs:22-149`
- Test temporarily: `E:\lserp_project\codex_extended_return_support_tests.cs`
**Interfaces:**
- Consumes: `string sourceSql`, `DataColumnCollection columns`, `int maxRows`, and optional real `string searchField`.
- Produces: `IsSearchableColumn(string columnName)` and the overload `BuildSearchSql(string sourceSql, DataColumnCollection columns, int maxRows, string searchField)` while preserving the existing three-argument overload.
- [ ] **Step 1: Write the failing SQL contract harness**
Create the temporary test source with this exact behavior:
```csharp
using System;
using System.Data;
using Lskj.Control.Model;
internal static class ExtendedReturnSupportContractTests
{
private static void Require(bool value, string message)
{
if (!value) throw new InvalidOperationException(message);
}
private static void RequireThrows(Action action, string message)
{
try
{
action();
}
catch (InvalidOperationException)
{
return;
}
throw new InvalidOperationException(message);
}
public static int Main()
{
DataTable schema = new DataTable();
schema.Columns.Add("productid");
schema.Columns.Add("appellation");
schema.Columns.Add("_rowid");
string allSql = ExtendedReturnSupport.BuildSearchSql(
"select productid, appellation, _rowid from p_ProductTab",
schema.Columns,
100,
string.Empty);
Require(allSql.Contains("[productid]"), "all columns omitted productid");
Require(allSql.Contains("[appellation]"), "all columns omitted appellation");
Require(!allSql.Contains("[_rowid]"), "all columns included hidden field");
string oneSql = ExtendedReturnSupport.BuildSearchSql(
"select productid, appellation, _rowid from p_ProductTab",
schema.Columns,
100,
"appellation");
Require(oneSql.Contains("[appellation]"), "selected column omitted appellation");
Require(!oneSql.Contains("[productid]"), "selected column included productid");
Require(!oneSql.Contains("[_rowid]"), "selected column included hidden field");
Require(!ExtendedReturnSupport.IsSearchableColumn("_rowid"), "hidden field is searchable");
Require(ExtendedReturnSupport.IsSearchableColumn("productid"), "business field is not searchable");
RequireThrows(delegate
{
ExtendedReturnSupport.BuildSearchSql("select 1", schema.Columns, 100, "_rowid");
}, "hidden selected field did not fail");
RequireThrows(delegate
{
ExtendedReturnSupport.BuildSearchSql("select 1", schema.Columns, 100, "missing");
}, "missing selected field did not fail");
Console.WriteLine("ExtendedReturnSupport contract tests passed.");
return 0;
}
}
```
- [ ] **Step 2: Compile the harness to prove it fails before implementation**
Run the production support file and harness through the installed Roslyn compiler with .NET Framework 4.0 references:
```bat
dotnet "C:\Program Files\dotnet\sdk\10.0.201\Roslyn\bincore\csc.dll" /nologo /target:exe /out:"E:\lserp_project\codex_extended_return_support_tests.exe" /reference:"C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.0\mscorlib.dll" /reference:"C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.0\System.dll" /reference:"C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.0\System.Data.dll" "插件库\Lskj.Control\Model\ExtendedReturnSupport.cs" "E:\lserp_project\codex_extended_return_support_tests.cs"
```
Expected: compilation fails because the four-argument overload and `IsSearchableColumn` do not exist.
- [ ] **Step 3: Implement the visible-column rule and overload**
Keep the old overload as a compatibility wrapper and select columns using case-insensitive real field names:
```csharp
public static bool IsSearchableColumn(string columnName)
{
return !string.IsNullOrWhiteSpace(columnName) &&
!columnName.StartsWith("_", StringComparison.Ordinal);
}
public static string BuildSearchSql(
string sourceSql,
DataColumnCollection columns,
int maxRows)
{
return BuildSearchSql(sourceSql, columns, maxRows, string.Empty);
}
public static string BuildSearchSql(
string sourceSql,
DataColumnCollection columns,
int maxRows,
string searchField)
{
if (columns == null || columns.Count == 0)
throw new InvalidOperationException("搜索 SQL 没有返回任何可查询列。");
if (maxRows <= 0)
throw new ArgumentOutOfRangeException("maxRows", "最大返回行数必须大于零。");
List<DataColumn> selectedColumns = new List<DataColumn>();
foreach (DataColumn column in columns)
{
if (!IsSearchableColumn(column.ColumnName)) continue;
if (string.IsNullOrWhiteSpace(searchField) ||
string.Equals(column.ColumnName, searchField, StringComparison.OrdinalIgnoreCase))
{
selectedColumns.Add(column);
}
}
if (selectedColumns.Count == 0)
{
throw new InvalidOperationException(string.IsNullOrWhiteSpace(searchField)
? "搜索 SQL 没有返回可见业务列。"
: string.Format("搜索字段“{0}”不存在或属于隐藏技术列。", searchField));
}
string alias = QuoteIdentifier(SearchAlias);
StringBuilder builder = new StringBuilder();
builder.AppendFormat("select top ({0}) * from ({1}) as {2} where ",
maxRows, NormalizeSourceSql(sourceSql), alias);
for (int i = 0; i < selectedColumns.Count; i++)
{
if (i > 0) builder.Append(" or ");
builder.Append("convert(nvarchar(4000), ");
builder.Append(alias);
builder.Append('.');
builder.Append(QuoteIdentifier(selectedColumns[i].ColumnName));
builder.Append(") like ");
builder.Append(SearchParameterName);
builder.Append(" escape N'\\'");
}
return builder.ToString();
}
```
- [ ] **Step 4: Compile and run the contract harness**
Run the same compiler command, then:
```bat
E:\lserp_project\codex_extended_return_support_tests.exe
```
Expected: exit code `0` and `ExtendedReturnSupport contract tests passed.`
- [ ] **Step 5: Remove temporary harness artifacts and commit Task 1**
```bat
del "E:\lserp_project\codex_extended_return_support_tests.cs"
del "E:\lserp_project\codex_extended_return_support_tests.exe"
git add -- "插件库/Lskj.Control/Model/ExtendedReturnSupport.cs"
git diff --cached --check
git commit -m "feat(control): support selected extended search columns"
```
---
### Task 2: Add selector persistence and local filter UI to the shared popup
**Files:**
- Modify: `插件库/Lskj.Control/AutoGridLookUp/ExtendedReturnSearchPopup.cs:14-232`
**Interfaces:**
- Consumes: translated column options as `IEnumerable<KeyValuePair<string, string>>`, where key is the real field and value is the displayed caption.
- Produces: `SelectedSearchField`, `SetSearchColumns(...)`, and `ClearResultAndFilter()` for both hosts.
- [ ] **Step 1: Record failing structural assertions**
Run this PowerShell check before editing:
```powershell
$path = '插件库\Lskj.Control\AutoGridLookUp\ExtendedReturnSearchPopup.cs'
$text = Get-Content -LiteralPath $path -Raw
$required = @(
'ComboBoxEdit searchFieldEdit',
'searchPanel.ColumnCount = 4',
'ShowAutoFilterRow = true',
'SelectedSearchField',
'SetSearchColumns',
'ClearResultAndFilter'
)
foreach ($item in $required) {
if (-not $text.Contains($item)) { throw "Missing popup contract: $item" }
}
```
Expected: failure on the first missing popup contract.
- [ ] **Step 2: Add the selector option model and public popup contract**
Add `using Lskj.Control.Model;` and `using System.Collections.Generic;`. Inside `ExtendedReturnSearchPopup`, add a private option object and the host-facing API:
```csharp
private sealed class SearchColumnOption
{
public SearchColumnOption(string fieldName, string caption)
{
FieldName = fieldName ?? string.Empty;
Caption = caption ?? string.Empty;
}
public string FieldName { get; private set; }
public string Caption { get; private set; }
public override string ToString() { return Caption; }
}
public string SelectedSearchField
{
get
{
SearchColumnOption option = searchFieldEdit.SelectedItem as SearchColumnOption;
return option == null ? string.Empty : option.FieldName;
}
}
public void SetSearchColumns(IEnumerable<KeyValuePair<string, string>> columns)
{
string preferredField = selectedSearchField;
searchFieldEdit.Properties.BeginUpdate();
try
{
searchFieldEdit.Properties.Items.Clear();
SearchColumnOption all = new SearchColumnOption(string.Empty, "所有列");
searchFieldEdit.Properties.Items.Add(all);
SearchColumnOption selected = null;
if (columns != null)
{
foreach (KeyValuePair<string, string> column in columns)
{
if (!ExtendedReturnSupport.IsSearchableColumn(column.Key)) continue;
SearchColumnOption option = new SearchColumnOption(column.Key, column.Value);
searchFieldEdit.Properties.Items.Add(option);
if (string.Equals(option.FieldName, preferredField, StringComparison.OrdinalIgnoreCase))
selected = option;
}
}
searchFieldEdit.SelectedItem = selected ?? all;
selectedSearchField = selected == null ? string.Empty : selected.FieldName;
}
finally
{
searchFieldEdit.Properties.EndUpdate();
}
}
```
Add `selectedSearchField` and update it from `SelectedIndexChanged`; `PrepareForOpen()` must not reset it.
- [ ] **Step 3: Change the top layout and keep input focus behavior**
Instantiate a `ComboBoxEdit`, set `TextEditStyle = TextEditStyles.DisableTextEditor`, and change the layout to four columns:
```csharp
searchPanel.ColumnCount = 4;
searchPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 120F));
searchPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F));
searchPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 72F));
searchPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 72F));
searchPanel.Controls.Add(searchFieldEdit, 0, 0);
searchPanel.Controls.Add(searchEdit, 1, 0);
searchPanel.Controls.Add(queryButton, 2, 0);
searchPanel.Controls.Add(clearButton, 3, 0);
```
Set selector/input/query/clear tab indices to `0/1/2/3`. Keep `FocusSearchEditor()` focused on `searchEdit` so opening the popup still allows immediate typing.
- [ ] **Step 4: Enable and reset DevExpress local filtering**
Enable the built-in filter row and centralize clearing:
```csharp
resultView.OptionsView.ShowAutoFilterRow = true;
public void ClearResultAndFilter()
{
resultView.ClearColumnsFilter();
resultGrid.DataSource = null;
}
public void PrepareForOpen()
{
searchEdit.Text = string.Empty;
ClearResultAndFilter();
}
```
Do not attach the selector or auto-filter row to `SearchRequested`; only Enter in `searchEdit` and the query button raise database search events.
- [ ] **Step 5: Run structural assertions and compile `Lskj.Control`**
Run the Step 1 PowerShell check and the repositorys isolated C# compile helper. Expected: structural check exit `0` and `Lskj.Control` compile exit `0`.
- [ ] **Step 6: Commit Task 2**
```bat
git add -- "插件库/Lskj.Control/AutoGridLookUp/ExtendedReturnSearchPopup.cs"
git diff --cached --check
git commit -m "feat(control): add extended lookup filter UI"
```
---
### Task 3: Integrate schema-only loading and selected fields in MyControl
**Files:**
- Modify: `插件库/Lskj.Control/AutoGridLookUp/LabelExtendedReturnSearchEdit.cs:37-60,294-360,616-760`
**Interfaces:**
- Consumes: `popup.SelectedSearchField`, `popup.SetSearchColumns(...)`, the existing `GetSchema(...)`, and the four-argument SQL builder.
- Produces: schema-only asynchronous requests and result delivery that configures selector options before optionally binding rows.
- [ ] **Step 1: Record failing MyControl integration assertions**
```powershell
$path = '插件库\Lskj.Control\AutoGridLookUp\LabelExtendedReturnSearchEdit.cs'
$text = Get-Content -LiteralPath $path -Raw
$required = @(
'public string SearchField;',
'public bool SchemaOnly;',
'public DataTable Schema;',
'popup.SelectedSearchField',
'popup.SetSearchColumns',
'MaxRows, request.SearchField'
)
foreach ($item in $required) {
if (-not $text.Contains($item)) { throw "Missing MyControl integration: $item" }
}
```
Expected: failure before implementation.
- [ ] **Step 2: Extend request/result snapshots**
Add exact state to the existing nested classes:
```csharp
private sealed class SearchRequest
{
public int Version;
public string SourceSql;
public string Keyword;
public string SearchField;
public string ConnectionString;
public bool SchemaOnly;
}
private sealed class QueryResult
{
public DataTable Schema;
public DataTable Table;
public Exception Error;
}
```
- [ ] **Step 3: Queue schema-only work on popup open**
Resolve `sourceSql` once in `PopupEdit_QueryPopUp`. After `PrepareForOpen()`:
```csharp
DataTable cachedSchema = TryGetSchema(sourceSql);
if (cachedSchema != null) ConfigureSearchColumns(cachedSchema);
if (!string.IsNullOrWhiteSpace(EditValue))
QueueSearch(sourceSql, EditValue, string.Empty, false);
else if (cachedSchema == null)
QueueSearch(sourceSql, string.Empty, string.Empty, true);
else
CancelSearch();
```
`TryGetSchema` must take `schemaSyncRoot` and return the cached table only when `schemaSql` equals the resolved SQL. `QueueSearch` replaces the old `StartSearch` request construction and snapshots `SearchField` and `SchemaOnly`. The popup-open lookup deliberately uses an empty search field so an existing actual ID is found independently of the preserved selector; only explicit Enter/query actions use `popup.SelectedSearchField`.
- [ ] **Step 4: Build translated options from schema**
Use existing caption translation and shared visibility rules:
```csharp
private void ConfigureSearchColumns(DataTable sourceSchema)
{
List<KeyValuePair<string, string>> options = new List<KeyValuePair<string, string>>();
IList<ExtendedReturnFieldMapping> mappings =
ExtendedReturnSupport.ParseResultFields(Model.ResultFields);
foreach (DataColumn column in sourceSchema.Columns)
{
if (!ExtendedReturnSupport.IsSearchableColumn(column.ColumnName)) continue;
options.Add(new KeyValuePair<string, string>(
column.ColumnName,
GetColumnCaption(column.ColumnName, mappings)));
}
popup.SetSearchColumns(options);
}
```
- [ ] **Step 5: Make query and delivery schema-aware**
`Query(SearchRequest request)` must always load and validate schema, set `result.Schema`, and execute row SQL only when `!request.SchemaOnly`:
```csharp
DataTable sourceSchema = GetSchema(request.SourceSql, request.ConnectionString);
result.Schema = sourceSchema;
IList<ExtendedReturnFieldMapping> mappings =
ExtendedReturnSupport.ParseResultFields(Model.ResultFields);
ValidateSourceColumns(sourceSchema.Columns, mappings);
if (!request.SchemaOnly)
{
string searchSql = ExtendedReturnSupport.BuildSearchSql(
request.SourceSql, sourceSchema.Columns, MaxRows, request.SearchField);
result.Table = ExecuteQuery(
searchSql,
ExtendedReturnSupport.BuildLikeParameterValue(request.Keyword),
request.ConnectionString);
}
```
Change delivery to receive the request, reject stale versions, call `ConfigureSearchColumns(result.Schema)`, and bind/configure result rows only when `result.Table != null`.
- [ ] **Step 6: Clear local filters on explicit database search**
In `Popup_SearchRequested`, call `popup.ClearResultAndFilter()` before queuing any non-empty keyword. Capture `popup.SelectedSearchField` in that same UI event. Empty keywords cancel work and leave a cleared result.
- [ ] **Step 7: Run assertions and compile through `Lskj.Control`**
Run the Step 1 check. Compile `Lskj.Model`, `Lskj.Util`, `Lskj.Business`, then `Lskj.Control` into an isolated dependency directory. Expected: all exits `0`.
- [ ] **Step 8: Commit Task 3**
```bat
git add -- "插件库/Lskj.Control/AutoGridLookUp/LabelExtendedReturnSearchEdit.cs"
git diff --cached --check
git commit -m "feat(control): add form extended lookup column search"
```
---
### Task 4: Integrate schema-only loading and selected fields in GridControlEx
**Files:**
- Modify: `插件库/Lskj.Control/GridControlEx.ExtendedReturn.cs:32-85,323-449,621-905,1189-1220`
**Interfaces:**
- Consumes: the shared popup contract and selected-column SQL builder from Tasks 1-2.
- Produces: per-grid-column source-aware schema caching, schema-only requests, and selected-field request snapshots within the existing serialized request state machine.
- [ ] **Step 1: Record failing grid integration assertions**
```powershell
$path = '插件库\Lskj.Control\GridControlEx.ExtendedReturn.cs'
$text = Get-Content -LiteralPath $path -Raw
$required = @(
'mExtendedReturnSchemaSqls',
'public string SearchField;',
'public bool SchemaOnly;',
'public DataTable Schema;',
'popup.SelectedSearchField',
'popup.SetSearchColumns',
'request.SearchField'
)
foreach ($item in $required) {
if (-not $text.Contains($item)) { throw "Missing grid integration: $item" }
}
```
Expected: failure before implementation.
- [ ] **Step 2: Make cached schemas source-aware**
Add:
```csharp
private readonly Dictionary<string, string> mExtendedReturnSchemaSqls =
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
```
`GetExtendedReturnSchema` and a new `TryGetExtendedReturnSchema` must return a cached table only when both field name and resolved `sourceSql` match. Store the SQL beside each cached table and clear both dictionaries in `ResetExtendedReturnState()`.
- [ ] **Step 3: Extend grid request/result snapshots**
Add `SearchField` and `SchemaOnly` to `ExtendedLookupRequest`, plus `Schema` to `ExtendedLookupQueryResult`. Preserve the existing version, pending-request, and single-worker-per-field rules.
- [ ] **Step 4: Configure or asynchronously load schema on popup open**
In `ExtendedReturnSearch_QueryPopUp`, resolve source SQL after the business row is known. Configure selector options immediately from a matching cache. If the actual business value is empty and no schema is cached, enqueue a schema-only request; if it is non-empty, enqueue the normal initial search with an empty search field so it searches all visible columns without changing the preserved selector.
Use the existing caption method when building options:
```csharp
private void ConfigureExtendedReturnSearchColumns(
ExtendedReturnPopupContext context,
DataTable sourceSchema)
{
List<KeyValuePair<string, string>> options = new List<KeyValuePair<string, string>>();
IList<ExtendedReturnFieldMapping> mappings =
ExtendedReturnSupport.ParseResultFields(context.Model.ResultFields);
foreach (DataColumn column in sourceSchema.Columns)
{
if (!ExtendedReturnSupport.IsSearchableColumn(column.ColumnName)) continue;
options.Add(new KeyValuePair<string, string>(
column.ColumnName,
GetExtendedReturnColumnCaption(column.ColumnName, mappings)));
}
context.Popup.SetSearchColumns(options);
}
```
- [ ] **Step 5: Snapshot selector state for explicit searches**
`ExtendedReturnSearch_SearchRequested` must pass `popup.SelectedSearchField`. `StartExtendedReturnSearch` must clear the popups filters/results, reject an empty keyword, validate business configuration, resolve SQL, and enqueue a non-schema-only request containing the selected field.
- [ ] **Step 6: Make the worker result schema-aware without weakening stale-result guards**
`QueryExtendedReturn` must always populate `result.Schema`; it calls the four-argument SQL builder only for non-schema-only requests. In `BeginInvokeExtendedReturnResult`:
1. Keep every existing editor, focused column, business row, and request-version guard.
2. Configure selector options from `result.Schema`.
3. For schema-only requests, stop without setting `DeliveredVersion` or binding a result table.
4. For row requests, set `DeliveredVersion`, bind the result table, configure result columns, and retain existing popup visibility behavior.
- [ ] **Step 7: Run assertions and compile through `Lskj.Control`**
Run the Step 1 check and the isolated four-project compile chain. Expected: all exits `0`.
- [ ] **Step 8: Commit Task 4**
```bat
git add -- "插件库/Lskj.Control/GridControlEx.ExtendedReturn.cs"
git diff --cached --check
git commit -m "feat(control): add grid extended lookup column search"
```
---
### Task 5: Final compatibility verification and delivery
**Files:**
- Verify: `插件库/Lskj.Control/Model/ExtendedReturnSupport.cs`
- Verify: `插件库/Lskj.Control/AutoGridLookUp/ExtendedReturnSearchPopup.cs`
- Verify: `插件库/Lskj.Control/AutoGridLookUp/LabelExtendedReturnSearchEdit.cs`
- Verify: `插件库/Lskj.Control/GridControlEx.ExtendedReturn.cs`
- Verify: `docs/superpowers/specs/2026-07-31-extended-return-search-column-filter-design.md`
**Interfaces:**
- Consumes: all contracts created in Tasks 1-4.
- Produces: a clean, compiled local `main` ready for user runtime verification and later push authorization.
- [ ] **Step 1: Re-run the SQL contract harness from a clean temporary file**
Recreate the exact Task 1 harness, compile it with the final support source, run it, and confirm exit `0`. Delete the temporary `.cs` and `.exe` afterward.
- [ ] **Step 2: Run final structural behavior checks**
Assert all of the following exact invariants with PowerShell `Get-Content -Raw` checks:
- Popup layout has four columns and `ShowAutoFilterRow = true`.
- `PrepareForOpen()` does not reset `selectedSearchField`.
- Both explicit search handlers read `SelectedSearchField` and call `ClearResultAndFilter()`.
- Both query paths call the four-argument SQL builder.
- Both schema delivery paths call `SetSearchColumns`.
- Grid reset clears both schema dictionaries.
- Existing `ClearRequested`, `ResultSelected`, row indicator, arrow-key, and Enter-key event hookups remain present.
- [ ] **Step 3: Compile the final dependency chain in isolation**
Use `E:\lserp_project\codex_compile_project.ps1` with a fresh `DebugCodexExtendedFilter` directory. Compile in this exact order:
1. `插件库\Lskj.Model\Lskj.Model.csproj`
2. `插件库\Lskj.Util\Lskj.Util.csproj`
3. `插件库\Lskj.Business\Lskj.Business.csproj`
4. `插件库\Lskj.Control\Lskj.Control.csproj`
Expected: each compiler exit is `0`; the known `CS2023 /noconfig` warning is acceptable.
- [ ] **Step 4: Inspect repository scope**
```bat
git diff --check origin/main...main
git status --short
git log --oneline --decorate -8
```
Expected: no whitespace errors, no temporary artifacts, and only the approved design/plan documents plus four production source files differ from the pre-feature base.
- [ ] **Step 5: Hand off runtime verification scenarios**
Report the ten scenarios in the design spec, emphasizing:
- selector captions and hidden-column exclusion;
- all-column versus single-column database behavior;
- selector persistence and fallback;
- auto-filter row local-only behavior and reset on new search;
- mouse/keyboard selection, clear, display translation, and ResultFields mapping for MyControl/grid with both 173 and 174.
Do not push until the user explicitly requests it.
@@ -0,0 +1,130 @@
# 173/174 扩展返回搜索列与本地筛选设计
## 背景
173“模块选择返回 Id-扩展”和 174“搜索返回 Id-扩展”目前共用 `ExtendedReturnSearchPopup`。弹窗只提供一个搜索输入框,数据库查询会对数据源返回的全部列拼接 `OR LIKE`。结果表格已使用 DevExpress `GridView`,但没有显示自动筛选行。
本次改动在不改变现有数据源解析、ResultFields 回填、显示值翻译和键盘选择行为的前提下,增加数据库搜索列选择及结果集本地二次筛选。
## 目标
- 弹窗顶部增加搜索列下拉框。
- 支持“所有列”数据库搜索和指定单列数据库搜索。
- 下拉项显示与结果表头一致的中文标题,内部使用真实数据源字段名。
- `_` 开头的隐藏技术列不进入下拉,也不参与“所有列”搜索。
- 下方 DevExpress 表格显示自动筛选行,对数据库已返回的数据进行本地二次筛选。
- 173、174 在表格控件和 MyControl 中保持一致行为。
- 保留原有清空、选择、回填、显示和键盘操作。
## 非目标
- 不修改 173 的模块数据源解析规则。
- 不修改 174 的 SourceSql 配置规则。
- 不修改 ResultFields、ValueMember、TextMember 的含义或校验。
- 不把 DevExpress 自动筛选条件发送到数据库。
- 不在控件初始化阶段加载完整业务数据。
- 不跨应用重启持久化搜索列选择。
## 交互设计
弹窗顶部从左到右排列:
1. 搜索列下拉框。
2. 搜索文本输入框。
3. “查询”按钮。
4. “清空”按钮。
下拉框规则:
- 第一项固定为“所有列”。
- 后续项来自实际数据源结构,只包含名称不以 `_` 开头的列。
- 显示文本复用结果表格现有列标题翻译逻辑;选项内部保存真实字段名。
- 每个 173/174 控件实例在内存中保留最后一次选择,关闭并重新打开弹窗时不重置。
- 数据源结构变化且原选择字段已不存在时,自动回退到“所有列”。
输入框仍按现有方式仅在回车或点击“查询”时访问数据库。上下键进入结果表、结果表回车选择、鼠标点击选择和“清空”按钮行为保持不变。
## 数据结构加载
弹窗打开时只异步执行现有 `top 0` 结构查询,用于取得列名,不加载完整业务数据。
- 已有缓存结构时立即构建下拉项,不重复访问数据库。
- 首次打开且尚无缓存时,下拉先保留“所有列”,结构查询完成后补充列项。
- 如果当前业务值非空,现有首次搜索请求同时取得并缓存结构,无需额外加载完整数据。
- 弹窗关闭、控件销毁或请求版本已过期时丢弃后台返回结果,不更新已经失效的界面。
## 数据库搜索
`ExtendedReturnSupport.BuildSearchSql` 增加可选搜索字段参数:
- “所有列”:只对所有可见业务列生成 `OR LIKE` 条件。
- 指定列:校验字段存在且不是隐藏列,只为该列生成 `LIKE` 条件。
- 返回结果仍保留数据源的全部列,包括隐藏技术列,以免破坏 ResultFields 回填。
- 继续使用现有 `@lookupKeyword` 参数、LIKE 转义和字段名引用,不把输入文本或下拉显示标题直接拼入 SQL。
- 最大返回行数继续保持 100。
搜索请求必须在提交时同时快照关键字和真实字段名,避免后台查询期间用户切换下拉导致请求含义变化。
## 本地二次筛选
共享结果 `GridView` 启用 DevExpress `OptionsView.ShowAutoFilterRow`
- 自动筛选只作用于当前绑定的最多 100 行结果,不触发数据库查询。
- 每次按回车或点击“查询”发起新的数据库搜索前,清除全部本地筛选条件和旧结果。
- 关闭并重新打开弹窗时不保留上一次的本地筛选条件。
- 自动筛选后的焦点行仍可通过回车或鼠标点击执行原有 ResultFields 回填。
## 代码边界
### `ExtendedReturnSearchPopup`
- 负责下拉框、输入框、按钮和结果表的布局。
- 暴露当前真实搜索字段,不包含 SQL 或业务映射逻辑。
- 接收调用方生成的“真实字段名 + 中文标题”选项。
- 负责保留有效选择、无效选择回退、清空本地筛选及启用自动筛选行。
### `ExtendedReturnSupport`
- 统一生成“所有可见列”或“指定可见列”的参数化搜索 SQL。
- 统一判定 `_` 开头的隐藏技术列,避免表格和 MyControl 产生不同规则。
- 对不存在、隐藏或空白的指定字段给出明确配置错误。
### `LabelExtendedReturnSearchEdit`
- 为 MyControl 173/174 异步加载和缓存数据源结构。
- 使用现有业务控件标签生成中文列标题。
- 把下拉选择快照带入后台搜索请求。
### `GridControlEx.ExtendedReturn`
- 为表格 173/174 异步加载和缓存每个字段的数据源结构。
- 使用现有表格列标题翻译逻辑生成中文列标题。
- 把下拉选择快照带入现有每列串行查询状态机。
## 兼容性与错误处理
- 默认选择“所有列”时,用户操作方式与现有版本一致,但隐藏技术列不再参与模糊搜索。
- 173 继续通过模块编号解析 SQL,174 继续直接使用配置 SQL。
- 结构查询或业务查询失败时沿用现有提示机制,不能覆盖当前业务值或 ResultFields 字段。
- 后台返回旧版本结果时继续丢弃,不覆盖更新的搜索结果。
- 清空按钮仍清空当前业务行全部 ResultFields 映射字段并关闭弹窗。
- 原业务列仍为只读弹出编辑器,弹窗搜索文本不参与业务字段显示或保存。
## 验收场景
1. 首次打开空值控件,只执行结构查询,下拉最终显示“所有列”和所有可见中文列名。
2. 选择“所有列”搜索时,SQL 条件只包含非 `_` 开头的列。
3. 选择单列搜索时,SQL 条件只包含该真实字段。
4. 关闭后重开同一控件,保留上次有效搜索列;数据源缺少该列时回退到“所有列”。
5. 新数据库搜索会清除旧自动筛选条件。
6. 自动筛选行改变可见结果但不产生数据库请求。
7. 筛选后鼠标点击、上下键和回车仍能选择正确 DataRow 并完成 ResultFields 回填。
8. `_` 开头的列不显示、不进入下拉、不参与“所有列”搜索,但仍可作为 ResultFields 映射来源。
9. 表格和 MyControl 的 173、174 四种组合行为一致。
10. 原有清空、显示值翻译、实际值保存和模块数据源加载保持正常。
## 验证范围
- 对 SQL 构建规则、隐藏列排除、指定列校验和选择回退进行结构化检查。
- 编译 `Lskj.Model``Lskj.Util``Lskj.Business``Lskj.Control`
- 运行时验收由实际 ERP 界面对上述十个场景进行人工验证。
@@ -3,22 +3,44 @@ using DevExpress.XtraGrid;
using DevExpress.XtraGrid.Views.Grid;
using DevExpress.XtraGrid.Views.Grid.ViewInfo;
using DevExpress.Utils;
using Lskj.Control.Model;
using System;
using System.Collections.Generic;
using System.Windows.Forms;
namespace Lskj.Control
{
/// <summary>
/// 181 扩展搜索专用弹窗。查询文本只存在于本控件中,不参与业务列绑定。
/// 173/174 扩展搜索专用弹窗。查询文本只存在于本控件中,不参与业务列绑定。
/// </summary>
internal sealed class ExtendedReturnSearchPopup : UserControl
{
private sealed class SearchColumnOption
{
public SearchColumnOption(string fieldName, string caption)
{
FieldName = fieldName ?? string.Empty;
Caption = caption ?? string.Empty;
}
public string FieldName { get; private set; }
public string Caption { get; private set; }
public override string ToString()
{
return Caption;
}
}
private readonly TableLayoutPanel searchPanel;
private readonly ComboBoxEdit searchFieldEdit;
private readonly TextEdit searchEdit;
private readonly SimpleButton queryButton;
private readonly SimpleButton clearButton;
private readonly GridControl resultGrid;
private readonly GridView resultView;
private string selectedSearchField = string.Empty;
public event EventHandler SearchRequested;
public event EventHandler ClearRequested;
@@ -27,6 +49,7 @@ namespace Lskj.Control
public ExtendedReturnSearchPopup()
{
searchPanel = new TableLayoutPanel();
searchFieldEdit = new ComboBoxEdit();
searchEdit = new TextEdit();
queryButton = new SimpleButton();
clearButton = new SimpleButton();
@@ -35,6 +58,7 @@ namespace Lskj.Control
SuspendLayout();
searchPanel.SuspendLayout();
((System.ComponentModel.ISupportInitialize)(searchFieldEdit.Properties)).BeginInit();
((System.ComponentModel.ISupportInitialize)(searchEdit.Properties)).BeginInit();
((System.ComponentModel.ISupportInitialize)(resultGrid)).BeginInit();
((System.ComponentModel.ISupportInitialize)(resultView)).BeginInit();
@@ -42,7 +66,8 @@ namespace Lskj.Control
searchPanel.Dock = DockStyle.Top;
searchPanel.Height = 36;
searchPanel.Padding = new Padding(4);
searchPanel.ColumnCount = 3;
searchPanel.ColumnCount = 4;
searchPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 120F));
searchPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F));
searchPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 72F));
searchPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 72F));
@@ -53,26 +78,34 @@ namespace Lskj.Control
queryButton.Dock = DockStyle.Fill;
queryButton.Margin = new Padding(4, 0, 0, 0);
queryButton.Text = "查询";
queryButton.TabIndex = 1;
queryButton.TabIndex = 2;
queryButton.Click += QueryButton_Click;
clearButton.Dock = DockStyle.Fill;
clearButton.Margin = new Padding(4, 0, 0, 0);
clearButton.Text = "清空";
clearButton.TabIndex = 2;
clearButton.TabIndex = 3;
clearButton.Click += ClearButton_Click;
searchFieldEdit.Dock = DockStyle.Fill;
searchFieldEdit.Margin = new Padding(0, 0, 4, 0);
searchFieldEdit.TabIndex = 0;
searchFieldEdit.Properties.AutoHeight = false;
searchFieldEdit.Properties.TextEditStyle = DevExpress.XtraEditors.Controls.TextEditStyles.DisableTextEditor;
searchFieldEdit.SelectedIndexChanged += SearchFieldEdit_SelectedIndexChanged;
searchEdit.Dock = DockStyle.Fill;
searchEdit.Margin = new Padding(0);
searchEdit.TabIndex = 0;
searchEdit.TabIndex = 1;
searchEdit.Properties.AutoHeight = false;
searchEdit.Properties.NullValuePrompt = "输入查询内容后按回车";
searchEdit.Properties.NullValuePromptShowForEmptyValue = true;
searchEdit.KeyDown += SearchEdit_KeyDown;
searchPanel.Controls.Add(searchEdit, 0, 0);
searchPanel.Controls.Add(queryButton, 1, 0);
searchPanel.Controls.Add(clearButton, 2, 0);
searchPanel.Controls.Add(searchFieldEdit, 0, 0);
searchPanel.Controls.Add(searchEdit, 1, 0);
searchPanel.Controls.Add(queryButton, 2, 0);
searchPanel.Controls.Add(clearButton, 3, 0);
resultGrid.Dock = DockStyle.Fill;
resultGrid.MainView = resultView;
@@ -83,6 +116,7 @@ namespace Lskj.Control
resultView.OptionsSelection.EnableAppearanceFocusedCell = false;
resultView.OptionsView.ShowGroupPanel = false;
resultView.OptionsView.ShowIndicator = true;
resultView.OptionsView.ShowAutoFilterRow = true;
resultView.OptionsView.ColumnAutoWidth = false;
resultView.IndicatorWidth = 40;
resultView.CustomDrawRowIndicator += ResultView_CustomDrawRowIndicator;
@@ -97,8 +131,11 @@ namespace Lskj.Control
((System.ComponentModel.ISupportInitialize)(resultView)).EndInit();
((System.ComponentModel.ISupportInitialize)(resultGrid)).EndInit();
((System.ComponentModel.ISupportInitialize)(searchEdit.Properties)).EndInit();
((System.ComponentModel.ISupportInitialize)(searchFieldEdit.Properties)).EndInit();
searchPanel.ResumeLayout(false);
ResumeLayout(false);
SetSearchColumns(null);
}
public GridControl ResultGrid
@@ -116,9 +153,64 @@ namespace Lskj.Control
get { return (searchEdit.Text ?? string.Empty).Trim(); }
}
public string SelectedSearchField
{
get
{
SearchColumnOption option = searchFieldEdit.SelectedItem as SearchColumnOption;
return option == null ? string.Empty : option.FieldName;
}
}
public void SetSearchColumns(IEnumerable<KeyValuePair<string, string>> columns)
{
string preferredField = selectedSearchField;
SearchColumnOption all = new SearchColumnOption(string.Empty, "所有列");
SearchColumnOption selected = null;
HashSet<string> fields = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
searchFieldEdit.Properties.BeginUpdate();
try
{
searchFieldEdit.Properties.Items.Clear();
searchFieldEdit.Properties.Items.Add(all);
if (columns != null)
{
foreach (KeyValuePair<string, string> column in columns)
{
if (!ExtendedReturnSupport.IsSearchableColumn(column.Key) || !fields.Add(column.Key))
{
continue;
}
string caption = string.IsNullOrWhiteSpace(column.Value) ? column.Key : column.Value;
SearchColumnOption option = new SearchColumnOption(column.Key, caption);
searchFieldEdit.Properties.Items.Add(option);
if (string.Equals(option.FieldName, preferredField, StringComparison.OrdinalIgnoreCase))
{
selected = option;
}
}
}
searchFieldEdit.SelectedItem = selected ?? all;
selectedSearchField = selected == null ? string.Empty : selected.FieldName;
}
finally
{
searchFieldEdit.Properties.EndUpdate();
}
}
public void PrepareForOpen()
{
searchEdit.Text = string.Empty;
ClearResultAndFilter();
}
public void ClearResultAndFilter()
{
resultView.ClearColumnsFilter();
resultGrid.DataSource = null;
}
@@ -154,6 +246,12 @@ namespace Lskj.Control
RaiseSearchRequested();
}
private void SearchFieldEdit_SelectedIndexChanged(object sender, EventArgs e)
{
SearchColumnOption option = searchFieldEdit.SelectedItem as SearchColumnOption;
selectedSearchField = option == null ? string.Empty : option.FieldName;
}
private void FocusResultRow(bool focusLastRow)
{
if (resultView.DataRowCount <= 0) return;
@@ -50,11 +50,14 @@ namespace Lskj.Control
public int Version;
public string SourceSql;
public string Keyword;
public string SearchField;
public string ConnectionString;
public bool SchemaOnly;
}
private sealed class QueryResult
{
public DataTable Schema;
public DataTable Table;
public Exception Error;
}
@@ -302,10 +305,20 @@ namespace Lskj.Control
popupEdit.Properties.PopupFormSize = popupSize;
popupEdit.Properties.PopupFormMinSize = popupSize;
popup.PrepareForOpen();
string sourceSql = BuildSourceSql();
DataTable cachedSchema = TryGetSchema(sourceSql);
if (cachedSchema != null)
{
ConfigureSearchColumns(cachedSchema);
}
if (!string.IsNullOrWhiteSpace(EditValue))
{
StartSearch(EditValue);
QueueSearch(sourceSql, EditValue, string.Empty, false);
}
else if (cachedSchema == null)
{
QueueSearch(sourceSql, string.Empty, string.Empty, true);
}
else
{
@@ -336,7 +349,7 @@ namespace Lskj.Control
private void PopupEdit_Closed(object sender, ClosedEventArgs e)
{
CancelSearch();
popup.ResultGrid.DataSource = null;
popup.ClearResultAndFilter();
RefreshDisplayText();
}
@@ -346,13 +359,14 @@ namespace Lskj.Control
{
ValidateBusinessConfiguration();
string keyword = popup.SearchText;
string searchField = popup.SelectedSearchField;
popup.ClearResultAndFilter();
if (string.IsNullOrWhiteSpace(keyword))
{
CancelSearch();
popup.ResultGrid.DataSource = null;
return;
}
StartSearch(keyword);
QueueSearch(BuildSourceSql(), keyword, searchField, false);
}
catch (Exception ex)
{
@@ -613,13 +627,15 @@ namespace Lskj.Control
return Model.SourceSql;
}
private void StartSearch(string keyword)
private void QueueSearch(string sourceSql, string keyword, string searchField, bool schemaOnly)
{
SearchRequest request = new SearchRequest();
request.Version = Interlocked.Increment(ref queryVersion);
request.SourceSql = BuildSourceSql();
request.SourceSql = sourceSql;
request.Keyword = keyword;
request.SearchField = searchField;
request.ConnectionString = SqlHelper._connection.ConnectionString;
request.SchemaOnly = schemaOnly;
bool startWorker = false;
lock (querySyncRoot)
@@ -653,27 +669,29 @@ namespace Lskj.Control
}
}
QueryResult result = Query(
request.SourceSql,
request.Keyword,
request.ConnectionString);
DeliverResult(request.Version, result);
QueryResult result = Query(request);
DeliverResult(request, result);
}
}
private QueryResult Query(string sourceSql, string keyword, string connectionString)
private QueryResult Query(SearchRequest request)
{
QueryResult result = new QueryResult();
try
{
DataTable sourceSchema = GetSchema(sourceSql, connectionString);
DataTable sourceSchema = GetSchema(request.SourceSql, request.ConnectionString);
result.Schema = sourceSchema;
IList<ExtendedReturnFieldMapping> mappings = ExtendedReturnSupport.ParseResultFields(Model.ResultFields);
ValidateSourceColumns(sourceSchema.Columns, mappings);
string searchSql = ExtendedReturnSupport.BuildSearchSql(sourceSql, sourceSchema.Columns, MaxRows);
result.Table = ExecuteQuery(
searchSql,
ExtendedReturnSupport.BuildLikeParameterValue(keyword),
connectionString);
if (!request.SchemaOnly)
{
string searchSql = ExtendedReturnSupport.BuildSearchSql(
request.SourceSql, sourceSchema.Columns, MaxRows, request.SearchField);
result.Table = ExecuteQuery(
searchSql,
ExtendedReturnSupport.BuildLikeParameterValue(request.Keyword),
request.ConnectionString);
}
}
catch (Exception ex)
{
@@ -682,6 +700,18 @@ namespace Lskj.Control
return result;
}
private DataTable TryGetSchema(string sourceSql)
{
lock (schemaSyncRoot)
{
if (schema != null && string.Equals(schemaSql, sourceSql, StringComparison.Ordinal))
{
return schema;
}
return null;
}
}
private DataTable GetSchema(string sourceSql, string connectionString)
{
lock (schemaSyncRoot)
@@ -730,21 +760,25 @@ namespace Lskj.Control
return table;
}
private void DeliverResult(int version, QueryResult result)
private void DeliverResult(SearchRequest request, QueryResult result)
{
if (disposed || !IsHandleCreated) return;
MethodInvoker deliver = new MethodInvoker(delegate
{
if (disposed || IsDisposed || version != queryVersion || !popupEdit.IsPopupOpen) return;
if (disposed || IsDisposed || request.Version != queryVersion || !popupEdit.IsPopupOpen) return;
ConfigureSearchColumns(result.Schema);
if (result.Error != null)
{
MessageUtil.Show("扩展搜索查询失败:" + result.Error.Message);
return;
}
popup.ResultGrid.DataSource = result.Table;
ConfigureResultColumns(result.Table);
if (result.Table != null)
{
popup.ResultGrid.DataSource = result.Table;
ConfigureResultColumns(result.Table);
}
});
try
@@ -759,6 +793,24 @@ namespace Lskj.Control
}
}
private void ConfigureSearchColumns(DataTable sourceSchema)
{
if (sourceSchema == null) return;
List<KeyValuePair<string, string>> options = new List<KeyValuePair<string, string>>();
IList<ExtendedReturnFieldMapping> mappings =
ExtendedReturnSupport.ParseResultFields(Model.ResultFields);
foreach (DataColumn column in sourceSchema.Columns)
{
if (!ExtendedReturnSupport.IsSearchableColumn(column.ColumnName)) continue;
options.Add(new KeyValuePair<string, string>(
column.ColumnName,
GetColumnCaption(column.ColumnName, mappings)));
}
popup.SetSearchColumns(options);
}
private void ConfigureResultColumns(DataTable table)
{
if (table == null) return;
@@ -38,6 +38,8 @@ namespace Lskj.Control
new Dictionary<string, ExtendedReturnPopupContext>(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<string, DataTable> mExtendedReturnSchemas =
new Dictionary<string, DataTable>(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<string, string> mExtendedReturnSchemaSqls =
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<string, string> mExtendedReturnDisplayFields =
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<string, ExtendedLookupRequestState> mExtendedReturnQueryStates =
@@ -70,8 +72,10 @@ namespace Lskj.Control
public DateTime DueTime;
public string FieldName;
public string Keyword;
public string SearchField;
public string SourceSql;
public string ConnectionString;
public bool SchemaOnly;
public GridColumnModel Model;
public PopupContainerEdit Editor;
public ExtendedReturnPopupContext PopupContext;
@@ -80,6 +84,7 @@ namespace Lskj.Control
private sealed class ExtendedLookupQueryResult
{
public DataTable Schema;
public DataTable Table;
public Exception Error;
}
@@ -340,9 +345,31 @@ namespace Lskj.Control
string actualValueText = GetExtendedReturnBusinessValueText(context);
context.Popup.PrepareForOpen();
string sourceSql = ResolveExtendedReturnSourceSql(model, context.BusinessRow);
DataTable cachedSchema = TryGetExtendedReturnSchema(model.FieldName, sourceSql);
if (cachedSchema != null)
{
ConfigureExtendedReturnSearchColumns(context, cachedSchema);
}
if (actualValueText.Length > 0)
{
StartExtendedReturnSearch(context, actualValueText);
QueueExtendedReturnSearch(
context.OwnerEdit,
context,
sourceSql,
actualValueText,
string.Empty,
false);
}
else if (cachedSchema == null)
{
QueueExtendedReturnSearch(
context.OwnerEdit,
context,
sourceSql,
string.Empty,
string.Empty,
true);
}
else
{
@@ -426,7 +453,7 @@ namespace Lskj.Control
try
{
StartExtendedReturnSearch(context, popup.SearchText);
StartExtendedReturnSearch(context, popup.SearchText, popup.SelectedSearchField);
}
catch (Exception ex)
{
@@ -434,18 +461,27 @@ namespace Lskj.Control
}
}
private void StartExtendedReturnSearch(ExtendedReturnPopupContext context, string keyword)
private void StartExtendedReturnSearch(
ExtendedReturnPopupContext context,
string keyword,
string searchField)
{
if (context == null || context.Model == null || context.OwnerEdit == null) return;
keyword = (keyword ?? string.Empty).Trim();
CancelExtendedReturnSearch(context.Model.FieldName);
context.Popup.ResultGrid.DataSource = null;
context.Popup.ClearResultAndFilter();
if (keyword.Length == 0) return;
ValidateExtendedReturnBusinessConfiguration(context.Model, context.BusinessRow);
string sourceSql = ResolveExtendedReturnSourceSql(context.Model, context.BusinessRow);
QueueExtendedReturnSearch(context.OwnerEdit, context, sourceSql, keyword);
QueueExtendedReturnSearch(
context.OwnerEdit,
context,
sourceSql,
keyword,
searchField,
false);
}
private void ExtendedReturnSearch_ClearRequested(object sender, EventArgs e)
@@ -622,7 +658,9 @@ namespace Lskj.Control
PopupContainerEdit edit,
ExtendedReturnPopupContext context,
string sourceSql,
string keyword)
string keyword,
string searchField,
bool schemaOnly)
{
GridColumnModel model = context.Model;
ExtendedLookupRequest request;
@@ -636,8 +674,10 @@ namespace Lskj.Control
DueTime = DateTime.UtcNow,
FieldName = model.FieldName,
Keyword = keyword,
SearchField = searchField,
SourceSql = sourceSql,
ConnectionString = SqlHelper._connection.ConnectionString,
SchemaOnly = schemaOnly,
Model = model,
Editor = edit,
PopupContext = context,
@@ -781,13 +821,18 @@ namespace Lskj.Control
try
{
DataTable schema = GetExtendedReturnSchema(request.FieldName, request.SourceSql, request.ConnectionString);
result.Schema = schema;
IList<ExtendedReturnFieldMapping> mappings = ExtendedReturnSupport.ParseResultFields(request.Model.ResultFields);
ValidateExtendedReturnSourceColumns(schema.Columns, request.Model, mappings);
string searchSql = ExtendedReturnSupport.BuildSearchSql(request.SourceSql, schema.Columns, ExtendedLookupMaxRows);
result.Table = ExecuteExtendedReturnQuery(
searchSql,
ExtendedReturnSupport.BuildLikeParameterValue(request.Keyword),
request.ConnectionString);
if (!request.SchemaOnly)
{
string searchSql = ExtendedReturnSupport.BuildSearchSql(
request.SourceSql, schema.Columns, ExtendedLookupMaxRows, request.SearchField);
result.Table = ExecuteExtendedReturnQuery(
searchSql,
ExtendedReturnSupport.BuildLikeParameterValue(request.Keyword),
request.ConnectionString);
}
}
catch (Exception ex)
{
@@ -802,7 +847,13 @@ namespace Lskj.Control
lock (mExtendedReturnSyncRoot)
{
DataTable cachedSchema;
if (mExtendedReturnSchemas.TryGetValue(fieldName, out cachedSchema)) return cachedSchema;
string cachedSql;
if (mExtendedReturnSchemas.TryGetValue(fieldName, out cachedSchema) &&
mExtendedReturnSchemaSqls.TryGetValue(fieldName, out cachedSql) &&
string.Equals(cachedSql, sourceSql, StringComparison.Ordinal))
{
return cachedSchema;
}
schemaGeneration = mExtendedReturnSchemaGeneration;
}
@@ -810,15 +861,38 @@ namespace Lskj.Control
lock (mExtendedReturnSyncRoot)
{
DataTable cachedSchema;
if (mExtendedReturnSchemas.TryGetValue(fieldName, out cachedSchema)) return cachedSchema;
string cachedSql;
if (mExtendedReturnSchemas.TryGetValue(fieldName, out cachedSchema) &&
mExtendedReturnSchemaSqls.TryGetValue(fieldName, out cachedSql) &&
string.Equals(cachedSql, sourceSql, StringComparison.Ordinal))
{
return cachedSchema;
}
if (schemaGeneration == mExtendedReturnSchemaGeneration)
{
mExtendedReturnSchemas[fieldName] = schema;
mExtendedReturnSchemaSqls[fieldName] = sourceSql;
}
}
return schema;
}
private DataTable TryGetExtendedReturnSchema(string fieldName, string sourceSql)
{
lock (mExtendedReturnSyncRoot)
{
DataTable cachedSchema;
string cachedSql;
if (mExtendedReturnSchemas.TryGetValue(fieldName, out cachedSchema) &&
mExtendedReturnSchemaSqls.TryGetValue(fieldName, out cachedSql) &&
string.Equals(cachedSql, sourceSql, StringComparison.Ordinal))
{
return cachedSchema;
}
return null;
}
}
private static DataTable ExecuteExtendedReturnQuery(
string commandText,
string keywordParameterValue,
@@ -870,11 +944,13 @@ namespace Lskj.Control
if (state.Version != request.Version) return;
}
ConfigureExtendedReturnSearchColumns(request.PopupContext, result.Schema);
if (result.Error != null)
{
MessageUtil.Show("扩展搜索查询失败:" + result.Error.Message);
return;
}
if (request.SchemaOnly) return;
lock (mExtendedReturnSyncRoot)
{
@@ -904,6 +980,27 @@ namespace Lskj.Control
}
}
private void ConfigureExtendedReturnSearchColumns(
ExtendedReturnPopupContext context,
DataTable sourceSchema)
{
if (context == null || context.Popup == null || context.Model == null || sourceSchema == null) return;
ExtendedReturnSearchPopup popup = context.Popup;
List<KeyValuePair<string, string>> options = new List<KeyValuePair<string, string>>();
IList<ExtendedReturnFieldMapping> mappings =
ExtendedReturnSupport.ParseResultFields(context.Model.ResultFields);
foreach (DataColumn column in sourceSchema.Columns)
{
if (!ExtendedReturnSupport.IsSearchableColumn(column.ColumnName)) continue;
options.Add(new KeyValuePair<string, string>(
column.ColumnName,
GetExtendedReturnColumnCaption(column.ColumnName, mappings)));
}
popup.SetSearchColumns(options);
}
private void ConfigureExtendedReturnViewColumns(ExtendedReturnPopupContext context, DataTable table)
{
if (context == null || context.Popup == null || table == null || context.Model == null) return;
@@ -972,7 +1069,7 @@ namespace Lskj.Control
}
context.OwnerEdit.EditValue = null;
context.Popup.ResultGrid.DataSource = null;
context.Popup.ClearResultAndFilter();
if (context.OwnerEdit.IsPopupOpen) context.OwnerEdit.ClosePopup();
this.GridView.PostEditor();
this.GridView.UpdateCurrentRow();
@@ -1002,7 +1099,7 @@ namespace Lskj.Control
ExtendedReturnPopupContext context = GetExtendedReturnPopupContext(model.FieldName);
CancelExtendedReturnSearch(model.FieldName);
SetExtendedReturnEditorBusinessValue(context);
context.Popup.ResultGrid.DataSource = null;
context.Popup.ClearResultAndFilter();
this.GridView.RefreshRow(this.GridView.FocusedRowHandle);
}
catch (Exception ex)
@@ -1192,12 +1289,13 @@ namespace Lskj.Control
lock (mExtendedReturnSyncRoot)
{
mExtendedReturnSchemas.Clear();
mExtendedReturnSchemaSqls.Clear();
mExtendedReturnSchemaGeneration++;
}
foreach (ExtendedReturnPopupContext context in mExtendedReturnPopupContexts.Values)
{
context.Popup.ResultGrid.DataSource = null;
context.Popup.ClearResultAndFilter();
}
}
@@ -78,6 +78,11 @@ namespace Lskj.Control.Model
}
public static string BuildSearchSql(string sourceSql, DataColumnCollection columns, int maxRows)
{
return BuildSearchSql(sourceSql, columns, maxRows, string.Empty);
}
public static string BuildSearchSql(string sourceSql, DataColumnCollection columns, int maxRows, string searchField)
{
if (columns == null || columns.Count == 0)
{
@@ -88,17 +93,50 @@ namespace Lskj.Control.Model
throw new ArgumentOutOfRangeException("maxRows", "最大返回行数必须大于零。");
}
List<DataColumn> searchColumns = new List<DataColumn>();
string selectedField = (searchField ?? string.Empty).Trim();
if (selectedField.Length == 0)
{
foreach (DataColumn column in columns)
{
if (IsSearchableColumn(column.ColumnName))
{
searchColumns.Add(column);
}
}
}
else
{
foreach (DataColumn column in columns)
{
if (string.Equals(column.ColumnName, selectedField, StringComparison.OrdinalIgnoreCase) &&
IsSearchableColumn(column.ColumnName))
{
searchColumns.Add(column);
break;
}
}
}
if (searchColumns.Count == 0)
{
if (selectedField.Length > 0)
{
throw new InvalidOperationException(string.Format("搜索字段“{0}”不存在或不允许查询。", selectedField));
}
throw new InvalidOperationException("搜索 SQL 没有返回任何可查询列。");
}
string alias = QuoteIdentifier(SearchAlias);
StringBuilder builder = new StringBuilder();
builder.AppendFormat("select top ({0}) * from ({1}) as {2} where ", maxRows, NormalizeSourceSql(sourceSql), alias);
for (int i = 0; i < columns.Count; i++)
for (int i = 0; i < searchColumns.Count; i++)
{
if (i > 0) builder.Append(" or ");
builder.Append("convert(nvarchar(4000), ");
builder.Append(alias);
builder.Append('.');
builder.Append(QuoteIdentifier(columns[i].ColumnName));
builder.Append(QuoteIdentifier(searchColumns[i].ColumnName));
builder.Append(") like ");
builder.Append(SearchParameterName);
builder.Append(" escape N'\\'");
@@ -106,6 +144,12 @@ namespace Lskj.Control.Model
return builder.ToString();
}
public static bool IsSearchableColumn(string columnName)
{
return !string.IsNullOrWhiteSpace(columnName) &&
!columnName.StartsWith("_", StringComparison.Ordinal);
}
public static string BuildLikeParameterValue(string keyword)
{
string value = keyword ?? string.Empty;
+13
View File
@@ -381,6 +381,15 @@ namespace Lskj.Core
{
return Internet ? AESUtil.Decrypt(Connection3) : AESUtil.Decrypt(Connection4);
}
private static string EnableSqlServerIdleConnectionResilience(string connectionString)
{
SqlConnectionStringBuilder builder =
new SqlConnectionStringBuilder(connectionString);
builder["ConnectRetryCount"] = 1;
return builder.ConnectionString;
}
/// <summary>
/// <para>说明:创建数据库连接</para>
/// <para>创建人:龚宇超</para>
@@ -420,6 +429,10 @@ namespace Lskj.Core
providerName = "Dm";
SqlHelper.ConnectionType = ConnectionType.DmServer;
}
if (connectionType == ConnectionType.SqlServer)
{
connStr = EnableSqlServerIdleConnectionResilience(connStr);
}
SqlHelper.dbFactory = DbProviderFactories.GetFactory(providerName);
if (SqlHelper._connection != null)
{