feat(control): add extended return search for tables and forms

This commit is contained in:
2026-07-29 15:57:29 +08:00
parent b893e8d8e4
commit 1bbe9c9a4c
13 changed files with 2818 additions and 645 deletions
@@ -139,6 +139,10 @@ namespace Lskj.Control.Model
/// <value>The text member.</value>
public string TextMember { get; set; }
/// <summary>
/// 扩展返回字段映射,格式为“业务控件字段=返回数据字段”。
/// </summary>
public string ResultFields { get; set; }
/// <summary>
/// Gets or sets the source SQL.
/// </summary>
/// <value>The source SQL.</value>
+6 -12
View File
@@ -493,21 +493,13 @@ namespace Lskj.Control.Model
/// <summary>
/// 快速搜索框返回id(字典模式)
/// 模块选择返回ID-扩展
/// </summary>
public const int DictionarySearchBoxToId = 181;
public const int LabModuleSelectReturnIdExtended = 180;
/// <summary>
/// 快速搜索返回text(字典模式)
/// 搜索返回ID-扩展
/// </summary>
public const int DictionarySearchBoxToText = 182;
/// <summary>
/// 快速搜索框返回id 带参数(字典模式)
/// </summary>
public const int DictionarySearchBoxToIdParam = 183;
/// <summary>
/// 快速搜索框返回text 带参数(字典模式)
/// </summary>
public const int DictionarySearchBoxToTextParam = 184;
public const int LabSearchReturnIdExtended = 181;
/// <summary>
/// <para>说明:是否为Value类型</para>
@@ -628,6 +620,8 @@ namespace Lskj.Control.Model
fieldType == LabPhone ||
fieldType == LabSelectReturnIdNew ||
fieldType == LabSelectReturnTextNew ||
fieldType == LabModuleSelectReturnIdExtended ||
fieldType == LabSearchReturnIdExtended ||
fieldType == LabApiBtuton||
fieldType == LabModuleAddRowsID ||
fieldType == LabModuleAddRowsText;
@@ -0,0 +1,151 @@
using System;
using System.Collections.Generic;
using System.Data;
using System.Text;
using System.Text.RegularExpressions;
namespace Lskj.Control.Model
{
internal sealed class ExtendedReturnFieldMapping
{
public ExtendedReturnFieldMapping(string targetField, string sourceField)
{
TargetField = targetField;
SourceField = sourceField;
}
public string TargetField { get; private set; }
public string SourceField { get; private set; }
}
internal static class ExtendedReturnSupport
{
internal const string SearchAlias = "LS_ExtendedLookup";
internal const string SearchParameterName = "@lookupKeyword";
public static IList<ExtendedReturnFieldMapping> ParseResultFields(string resultFields)
{
List<ExtendedReturnFieldMapping> mappings = new List<ExtendedReturnFieldMapping>();
if (string.IsNullOrWhiteSpace(resultFields)) return mappings;
HashSet<string> targetFields = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
string[] items = resultFields.Split(new char[] { ';', '' });
foreach (string rawItem in items)
{
string item = (rawItem ?? string.Empty).Trim();
if (item.Length == 0) continue;
int separatorIndex = item.IndexOf('=');
if (separatorIndex <= 0 || separatorIndex == item.Length - 1 || item.IndexOf('=', separatorIndex + 1) >= 0)
{
throw new FormatException(string.Format("返回字段配置“{0}”格式错误,应为“业务表字段=返回数据字段”。", item));
}
string targetField = item.Substring(0, separatorIndex).Trim();
string sourceField = item.Substring(separatorIndex + 1).Trim();
if (targetField.Length == 0 || sourceField.Length == 0)
{
throw new FormatException(string.Format("返回字段配置“{0}”存在空字段名。", item));
}
if (!targetFields.Add(targetField))
{
throw new FormatException(string.Format("返回字段配置中的业务表字段“{0}”重复。", targetField));
}
mappings.Add(new ExtendedReturnFieldMapping(targetField, sourceField));
}
return mappings;
}
public static string FindTargetField(IEnumerable<ExtendedReturnFieldMapping> mappings, string sourceField)
{
if (mappings == null || string.IsNullOrWhiteSpace(sourceField)) return string.Empty;
foreach (ExtendedReturnFieldMapping mapping in mappings)
{
if (string.Equals(mapping.SourceField, sourceField, StringComparison.OrdinalIgnoreCase))
{
return mapping.TargetField;
}
}
return string.Empty;
}
public static string BuildStructureSql(string sourceSql)
{
return string.Format("select top 0 * from ({0}) as [{1}]", NormalizeSourceSql(sourceSql), SearchAlias);
}
public static string BuildSearchSql(string sourceSql, DataColumnCollection columns, int maxRows)
{
if (columns == null || columns.Count == 0)
{
throw new InvalidOperationException("搜索 SQL 没有返回任何可查询列。");
}
if (maxRows <= 0)
{
throw new ArgumentOutOfRangeException("maxRows", "最大返回行数必须大于零。");
}
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++)
{
if (i > 0) builder.Append(" or ");
builder.Append("convert(nvarchar(4000), ");
builder.Append(alias);
builder.Append('.');
builder.Append(QuoteIdentifier(columns[i].ColumnName));
builder.Append(") like ");
builder.Append(SearchParameterName);
builder.Append(" escape N'\\'");
}
return builder.ToString();
}
public static string BuildLikeParameterValue(string keyword)
{
string value = keyword ?? string.Empty;
value = value.Replace("\\", "\\\\");
value = value.Replace("%", "\\%");
value = value.Replace("_", "\\_");
value = value.Replace("[", "\\[");
return "%" + value + "%";
}
private static string NormalizeSourceSql(string sourceSql)
{
if (string.IsNullOrWhiteSpace(sourceSql))
{
throw new InvalidOperationException("搜索数据源 SQL 不能为空。");
}
string normalized = sourceSql.Trim();
if (normalized.EndsWith(";", StringComparison.Ordinal))
{
normalized = normalized.Substring(0, normalized.Length - 1).TrimEnd();
}
if (normalized.IndexOf(';') >= 0)
{
throw new InvalidOperationException("扩展搜索只支持一条可组合的 SELECT 语句。");
}
if (!Regex.IsMatch(normalized, @"^select\b", RegexOptions.IgnoreCase))
{
throw new InvalidOperationException("扩展搜索数据源必须是可作为派生表使用的 SELECT 语句。");
}
return normalized;
}
private static string QuoteIdentifier(string identifier)
{
if (string.IsNullOrWhiteSpace(identifier))
{
throw new InvalidOperationException("搜索 SQL 返回了空列名。");
}
return "[" + identifier.Replace("]", "]]") + "]";
}
}
}
@@ -57,6 +57,7 @@ namespace Lskj.Control.Model
private bool _frozenFlag;
private int _lookUpWidth;
private string _lookUpFieldsWidth;
private string _resultFields;
#endregion
@@ -326,6 +327,10 @@ namespace Lskj.Control.Model
/// </summary>
public string LookUpFieldsWidth { get { return this._lookUpFieldsWidth; } }
/// <summary>
/// 扩展选择结果映射,格式:业务表字段=返回数据字段;业务表字段1=返回数据字段1。
/// </summary>
public string ResultFields { get { return this._resultFields; } }
/// <summary>
/// 动态列
/// </summary>
public bool isDynamic;
@@ -459,6 +464,7 @@ namespace Lskj.Control.Model
}
this.addModuleResult = item.Table.Columns.Contains("addModuleResult") ? item["addModuleResult"] + "" : string.Empty;
this._resultFields = item.Table.Columns.Contains("resultfields") ? item["resultfields"] + "" : string.Empty;
this.addModuleld = item.Table.Columns.Contains("addModuleId") ? item["addModuleId"] + "" : string.Empty;
this.unionCompare = item.Table.Columns.Contains("UnionCompare") ? item["UnionCompare"] + "" : string.Empty;
this.isExportColumns = item.Table.Columns.Contains("isExportColumns") && !string.IsNullOrWhiteSpace(item["isExportColumns"] + "") ? "1".Equals(item["isExportColumns"] + "") : false;
+181 -22
View File
@@ -136,6 +136,11 @@ namespace Lskj.Control.Model
/// 下拉框、搜索框控件对象
/// </summary>
public List<ControlModel> mControlList = new List<ControlModel>();
private readonly List<LabelExtendedReturnSearchEdit> mExtendedReturnSearchControls =
new List<LabelExtendedReturnSearchEdit>();
private readonly List<KeyValuePair<object, EventArgs>> mExtendedReturnPendingChanges =
new List<KeyValuePair<object, EventArgs>>();
private int mExtendedReturnBatchDepth;
/// <summary>
/// 数据源任务对象
/// </summary>
@@ -2253,6 +2258,10 @@ namespace Lskj.Control.Model
LabelMultiAutoTextEdit4 moduleReturnsIdNew = ctr as LabelMultiAutoTextEdit4;
value = ControlType.LabSelectReturnIdNew == model.FieldType ? moduleReturnsIdNew.EditValue : moduleReturnsIdNew.EditText;
break;
case ControlType.LabSearchReturnIdExtended:
LabelExtendedReturnSearchEdit extendedReturnSearch = ctr as LabelExtendedReturnSearchEdit;
value = extendedReturnSearch == null ? string.Empty : extendedReturnSearch.EditValue;
break;
case ControlType.DynamicallyGeneratedSql:
LabelMultiAutoTextEdit3 moduleSqlGenerated = ctr as LabelMultiAutoTextEdit3;
value = moduleSqlGenerated.EditText;
@@ -2679,6 +2688,7 @@ namespace Lskj.Control.Model
}
}
RefreshExtendedReturnDisplayControls();
foreach (ControlModel model in this.ControlModels)
{
BaseUserControl control = FindControl(model);
@@ -3710,6 +3720,14 @@ namespace Lskj.Control.Model
}
pictureEdit.TextEdit.Image = image;
}
else if (baseControl is LabelExtendedReturnSearchEdit)
{
LabelExtendedReturnSearchEdit extendedReturnSearch = baseControl as LabelExtendedReturnSearchEdit;
extendedReturnSearch.EditValue = controlValue == null || controlValue == DBNull.Value
? string.Empty
: controlValue + "";
baseControl.Model.Text = extendedReturnSearch.EditValue;
}
else if (baseControl.Model.FieldType == 7 && !"****".Equals(controlValue))
{
string dfmt = baseControl.Model.DataFormat;
@@ -3750,6 +3768,113 @@ namespace Lskj.Control.Model
//baseControl.Parent.Focus();
}
}
RefreshExtendedReturnDisplayControls();
}
/// <summary>
/// 用户从扩展搜索弹窗选择或清空时,批量写入所有映射字段。
/// 中间值改变事件会延迟到全部字段提交成功后执行,且不改变原始值基线。
/// </summary>
public void ApplyExtendedReturnValues(IList<KeyValuePair<string, object>> values)
{
if (values == null || values.Count == 0) return;
if (mExtendedReturnBatchDepth > 0)
{
throw new InvalidOperationException("扩展返回字段正在批量赋值,不能重复执行。");
}
List<KeyValuePair<string, object>> normalizedValues =
new List<KeyValuePair<string, object>>();
Dictionary<string, string> originalValues =
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
Dictionary<BaseUserControl, string> originalModelTexts =
new Dictionary<BaseUserControl, string>();
HashSet<string> targetFields = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (KeyValuePair<string, object> value in values)
{
string targetField = (value.Key ?? string.Empty).Trim();
if (targetField.Length == 0)
{
throw new InvalidOperationException("扩展返回字段名称不能为空。");
}
if (!targetFields.Add(targetField))
{
throw new InvalidOperationException(
string.Format("扩展返回字段“{0}”重复。", targetField));
}
BaseUserControl targetControl = FindControl(targetField);
if (targetControl == null)
{
throw new InvalidOperationException(
string.Format("MyControl 中不存在业务控件“{0}”。", targetField));
}
object normalizedValue = value.Value == null || value.Value == DBNull.Value
? string.Empty
: value.Value;
normalizedValues.Add(new KeyValuePair<string, object>(targetField, normalizedValue));
originalValues[targetField] = GetControlValue(targetField);
originalModelTexts[targetControl] = targetControl.Model == null
? null
: targetControl.Model.Text;
}
bool success = false;
mExtendedReturnPendingChanges.Clear();
mExtendedReturnBatchDepth++;
try
{
foreach (KeyValuePair<string, object> value in normalizedValues)
{
SetControlValue(value.Key, value.Value);
}
RestoreExtendedReturnModelTexts(originalModelTexts);
RefreshExtendedReturnDisplayControls(true);
success = true;
}
catch
{
foreach (KeyValuePair<string, string> originalValue in originalValues)
{
SetControlValue(originalValue.Key, originalValue.Value);
}
RestoreExtendedReturnModelTexts(originalModelTexts);
RefreshExtendedReturnDisplayControls(true);
throw;
}
finally
{
mExtendedReturnBatchDepth--;
if (!success) mExtendedReturnPendingChanges.Clear();
}
FlushExtendedReturnPendingChanges();
}
private static void RestoreExtendedReturnModelTexts(
IDictionary<BaseUserControl, string> originalModelTexts)
{
foreach (KeyValuePair<BaseUserControl, string> item in originalModelTexts)
{
if (item.Key != null && item.Key.Model != null)
{
item.Key.Model.Text = item.Value;
}
}
}
private void RefreshExtendedReturnDisplayControls(bool force = false)
{
if (mExtendedReturnBatchDepth > 0 && !force) return;
foreach (LabelExtendedReturnSearchEdit control in mExtendedReturnSearchControls)
{
if (control != null && !control.IsDisposed && control.Model != null)
{
control.RefreshDisplayText();
}
}
}
/// <summary>
@@ -4152,6 +4277,7 @@ namespace Lskj.Control.Model
model.Sign = item.Table.Columns.Contains("TM_tagID") && !string.IsNullOrEmpty(item["TM_tagID"] + "") ? Convert.ToInt32(item["TM_tagID"] + "") : 0;
model.TextMember = item["lookupResult"] + "";
model.ValueMember = item["lookupKeyField"] + "";
model.ResultFields = item.Table.Columns.Contains("resultfields") ? item["resultfields"] + "" : string.Empty;
if (!dataCaches.GetValue(item, "LookupSql", out string lookupSql))
{
@@ -4239,28 +4365,6 @@ namespace Lskj.Control.Model
model.FieldType = 161;
model.IsRadio = true;
}
//快速搜索框在控件中转成普通搜索框
if (model.FieldType == 181 || model.FieldType == 182 || model.FieldType == 183 || model.FieldType == 184)
{
switch (model.FieldType)
{
case ControlType.DictionarySearchBoxToId:
model.FieldType = 5;
break;
case ControlType.DictionarySearchBoxToText:
model.FieldType = 6;
break;
case ControlType.DictionarySearchBoxToIdParam:
model.FieldType = 15;
break;
case ControlType.DictionarySearchBoxToTextParam:
model.FieldType = 16;
break;
}
}
return model;
}
/// <summary>
@@ -4830,6 +4934,18 @@ namespace Lskj.Control.Model
if (!isLoadBorder) moduleReturnsIdNew.TextEdit.BorderStyle = DevExpress.XtraEditors.Controls.BorderStyles.NoBorder;
baseControl = moduleReturnsIdNew;
break;
case ControlType.LabSearchReturnIdExtended:
LabelExtendedReturnSearchEdit extendedReturnSearch = new LabelExtendedReturnSearchEdit();
extendedReturnSearch.ControlObj = this;
mExtendedReturnSearchControls.Add(extendedReturnSearch);
extendedReturnSearch.TextEdit.KeyDown += new KeyEventHandler(OnTextEditKeyDown);
extendedReturnSearch.TextEdit.TextChanged += new EventHandler(OnTextEditTextChanged);
if (!isLoadBorder)
{
extendedReturnSearch.TextEdit.BorderStyle = DevExpress.XtraEditors.Controls.BorderStyles.NoBorder;
}
baseControl = extendedReturnSearch;
break;
case ControlType.DynamicallyGeneratedSql:
LabelMultiAutoTextEdit3 moduleSqlGenerated = new LabelMultiAutoTextEdit3();
// moduleSqlGenerated.SetDataSource();
@@ -5884,6 +6000,49 @@ namespace Lskj.Control.Model
/// <param name="sender">The source of the event.</param>
/// <param name="e">The <see cref="EventArgs"/> instance containing the event data.</param>
protected void OnTextEditTextChanged(object sender, EventArgs e)
{
if (mExtendedReturnBatchDepth > 0)
{
QueueExtendedReturnPendingChange(sender, e);
return;
}
ProcessTextEditTextChanged(sender, e);
}
private void QueueExtendedReturnPendingChange(object sender, EventArgs e)
{
System.Windows.Forms.Control changedControl = sender as System.Windows.Forms.Control;
BaseUserControl baseControl = changedControl == null
? null
: changedControl.ToBaseUserControl();
foreach (KeyValuePair<object, EventArgs> pendingChange in mExtendedReturnPendingChanges)
{
System.Windows.Forms.Control pendingControl =
pendingChange.Key as System.Windows.Forms.Control;
BaseUserControl pendingBaseControl = pendingControl == null
? null
: pendingControl.ToBaseUserControl();
if (baseControl != null && object.ReferenceEquals(baseControl, pendingBaseControl))
{
return;
}
}
mExtendedReturnPendingChanges.Add(
new KeyValuePair<object, EventArgs>(sender, e ?? EventArgs.Empty));
}
private void FlushExtendedReturnPendingChanges()
{
KeyValuePair<object, EventArgs>[] pendingChanges =
mExtendedReturnPendingChanges.ToArray();
mExtendedReturnPendingChanges.Clear();
foreach (KeyValuePair<object, EventArgs> pendingChange in pendingChanges)
{
ProcessTextEditTextChanged(pendingChange.Key, pendingChange.Value);
}
}
private void ProcessTextEditTextChanged(object sender, EventArgs e)
{
BaseUserControl baseControl = (sender as System.Windows.Forms.Control).ToBaseUserControl();
this.SetUnionAncCalcControl(baseControl);