Kendo UI Grid复选框筛选:如何区分大小写?
2025-04-08 07:52:19
解决 Kendo UI Grid 复选框筛选器的大小写不敏感问题
问题来了:Kendo Grid 复选框筛选怎么区分大小写?
用 Kendo UI Grid 的时候,你可能遇到过这样一个情况:列头那个复选框筛选器 (Checkbox Filter),勾选 'usa',结果把 'USA' 和 'Usa' 也一股脑儿筛出来了。这默认行为有时候挺方便,但碰上需要严格区分大小写的场景,就有点头疼了。
就像下面这个例子展示的(源自社区提问),Grid 里有三行数据,国家分别是 "USA", "usa", "Usa"。期望是勾选筛选器里的 "usa" 时,只显示值为 "usa" 的那一行。可实际情况是,三行全出来了。
原始的 Dojo 示例也复现了这个问题:https://dojo.telerik.com/DRPHQtOE
这到底是 Kendo UI 的一个疏忽,还是说有什么配置能让它变得“火眼金睛”,严格区分大小写呢?
为啥默认不区分大小写?
先弄明白为啥会这样。
Kendo UI Grid 的筛选功能,特别是字符串比较,默认设计成大小写不敏感 (case-insensitive)。这主要是从普遍的用户体验角度出发的。多数场景下,用户搜 "apple" 和 "Apple",心里想的可能是同一个东西,不区分大小写能返回更全的结果,减少用户的挫败感。
实现上,Kendo UI 在进行客户端筛选时,其内部的字符串比较逻辑很可能转换成了统一的大小写(比如全转小写)再进行比较。如果是服务器端筛选,它生成的查询请求或者依赖的后端处理逻辑,也可能默认使用了不区分大小写的数据库排序规则 (Collation) 或比较方式。
所以,这严格来说不算是个 Bug,更像是一个基于普遍适用性考虑的设计决策。只不过,这个决策在需要精确匹配的特定场景下,就不那么合适了。
动手解决:让筛选器“明察秋毫”
既然默认行为不满足需求,我们就得自己动手改造一下。有几种思路可以实现大小写敏感的筛选。
方法一:巧用 filterMenuInit
事件 - 精准拦截
这是针对客户端筛选比较直接的一种方法。Kendo UI Grid 提供了一个 filterMenuInit
事件,这个事件在每次列的筛选菜单(就是那个带漏斗图标点开的菜单)初始化时触发。我们可以在这个时机介入,修改筛选菜单的行为。
原理和作用:
filterMenuInit
事件允许你访问和操作即将显示的筛选菜单的 DOM 元素及其关联的 Kendo UI 组件。对于复选框筛选列表(通常是一个内嵌的 ListView),我们可以找到触发表筛选操作的按钮(通常是“Filter”按钮),移除 Kendo UI 默认绑定的事件处理器,然后重新绑定一个我们自己的处理器。在这个自定义处理器里,我们手动收集选中的复选框的值,并构造一个严格区分大小写的筛选条件,最后调用 dataSource.filter()
方法应用这个条件。
操作步骤和代码示例:
假设你的 Grid 实例叫 grid
,并且你希望对名为 Country
的列实现大小写敏感的复选框筛选。
$(document).ready(function() {
$("#grid").kendoGrid({
dataSource: {
data: [
{ Country: "USA", Name: "John Doe" },
{ Country: "usa", Name: "Jane Smith" },
{ Country: "Usa", Name: "Peter Jones" },
{ Country: "Canada", Name: "Alice Brown" }
],
schema: {
model: {
fields: {
Country: { type: "string" },
Name: { type: "string" }
}
}
},
// 注意:如果数据量大,建议开启 serverFiltering,并将逻辑移到后端
// serverFiltering: true,
// transport: { ... }
},
filterable: {
mode: "menu", // 或者 "row, menu"
extra: true // 显示复选框筛选需要这个
},
columns: [
{
field: "Country",
title: "Country",
filterable: {
multi: true, // 允许多选复选框
// Kendo UI 会自动根据数据生成复选框选项
// 如果需要手动指定,可以用 dataSource
// dataSource: ["USA", "usa", "Usa", "Canada"]
}
},
{ field: "Name", title: "Name" }
],
// 关键在这里:定义 filterMenuInit 事件处理器
filterMenuInit: function(e) {
// 只针对 "Country" 列进行处理
if (e.field === "Country") {
enhanceFilterMenu(e);
}
}
});
});
function enhanceFilterMenu(e) {
// e.container 是筛选菜单的 jQuery 对象
var container = e.container;
// 找到菜单里的“Filter”按钮
// Kendo UI 内部按钮的类名可能会随版本变化,检查 DOM 确认
var filterButton = container.find("button[type=submit].k-button-primary");
if (filterButton.length > 0) {
// 找到 Kendo ListView 或类似的包含复选框的组件
// Kendo UI 通常会用一个内部的 MultiCheck / ListView 来展示复选框
// 需要检查实际生成的 DOM 结构来确定如何准确获取选中的值
var checkListWidget = container.find("[data-role='listview'], .k-multicheck-wrap").first(); // 尝试常见的选择器
if (!checkListWidget.length) {
console.warn("Could not find the checklist widget inside the filter menu for field:", e.field);
return;
}
// 先移除 Kendo 默认的点击事件处理器,防止它执行不区分大小写的筛选
// 使用 .off() 清理,需要小心,避免移除其他必要的事件
// 为了更精确,最好只移除 Kendo UI 附加的特定事件命名空间(如果知道的话)
// 一个相对安全的方式是克隆按钮再替换,或者直接覆盖事件
filterButton.off("click.kendoFilterMenu"); // 尝试移除 Kendo 可能使用的命名空间
// 如果无效,可能需要更彻底地解绑或采用其他策略
// 重新绑定我们自己的点击事件
filterButton.on("click", function(ev) {
ev.preventDefault(); // 阻止默认的表单提交或 Kendo 行为
ev.stopPropagation(); // 阻止事件冒泡
var selectedValues = [];
// 从复选框列表获取选中的值 - 这部分逻辑依赖于具体 DOM 结构
// 假设复选框是 input[type=checkbox]
checkListWidget.find("input[type=checkbox]:checked").each(function() {
// 获取复选框关联的精确值。可能存储在 value 属性,或 data-* 属性,或相邻的文本
// 需要检查DOM确认!这里假设值在 input 的 value 属性
selectedValues.push($(this).val());
// 或者如果值在旁边 span 的 text:
// selectedValues.push($(this).closest('label').find('span').text());
});
var grid = $("#grid").data("kendoGrid");
var dataSource = grid.dataSource;
var currentFilter = dataSource.filter() || { logic: "and", filters: [] };
var field = e.field;
// 移除之前对该字段设置的任何筛选条件(无论是我们加的还是 Kendo 默认的)
var otherFilters = [];
if (currentFilter && currentFilter.filters) {
otherFilters = currentFilter.filters.filter(function(f) {
// 保留非本字段的过滤器,或者不是由 checkbox filter 产生的过滤器
return f.field !== field;
});
}
var newFilters = otherFilters;
// 如果用户勾选了任何复选框
if (selectedValues.length > 0) {
// 构建区分大小写的筛选条件 (OR 逻辑)
var caseSensitiveFilters = {
logic: "or",
filters: selectedValues.map(function(value) {
// 使用 'eq' 操作符进行精确匹配
return { field: field, operator: "eq", value: value };
})
};
newFilters.push(caseSensitiveFilters);
}
console.log("Applying case-sensitive filter:", JSON.stringify(newFilters));
// 应用新的筛选条件
dataSource.filter(newFilters);
// 关闭筛选菜单
var filterMenu = container.data("kendoPopup");
if (filterMenu) {
filterMenu.close();
}
// Kendo R3 2016之后用 kendoFilterMenu 获取实例
// var filterMenuWidget = e.sender.filterMenu;
// if (filterMenuWidget) {
// filterMenuWidget.close();
// }
});
} else {
console.warn("Could not find the primary filter button in the menu for field:", e.field);
}
// 可能还需要处理 "Clear" 按钮的行为,确保它也能正确清除我们自定义的筛选逻辑
var clearButton = container.find("button[type=reset]");
clearButton.off("click.kendoFilterMenu").on("click", function(ev){
ev.preventDefault();
ev.stopPropagation();
var grid = $("#grid").data("kendoGrid");
var dataSource = grid.dataSource;
var currentFilter = dataSource.filter() || { logic: "and", filters: [] };
var field = e.field;
// 移除该字段相关的筛选条件
var remainingFilters = [];
if (currentFilter && currentFilter.filters) {
remainingFilters = currentFilter.filters.filter(function(f) {
return f.field !== field;
});
}
// 重置复选框状态
checkListWidget.find("input[type=checkbox]").prop('checked', false);
// Kendo UI 内部可能需要更新状态,直接操作 DOM 不一定完全同步内部状态
dataSource.filter(remainingFilters);
var filterMenu = container.data("kendoPopup");
if (filterMenu) {
filterMenu.close();
}
});
}
注意要点:
- DOM 结构依赖: 上述代码中的选择器(如
button[type=submit].k-button-primary
,[data-role='listview']
,input[type=checkbox]
)是基于 Kendo UI 常见的 HTML 结构。不同版本或主题下,这些结构可能微调。你需要使用浏览器开发者工具检查实际生成的 HTML,确保选择器能准确命中目标元素。 - 获取选中值: 如何准确获取复选框对应的值也取决于 Kendo UI 的实现。它可能在
input
的value
属性,也可能在data-*
属性,或者关联的label
或span
的文本里。务必确认来源。 - 事件解绑和重绑:
filterButton.off("click.kendoFilterMenu")
尝试移除 Kendo UI 可能添加的特定事件。如果这不起作用,你可能需要更暴力地filterButton.off("click")
,但这有风险移除其他监听器。或者考虑克隆按钮替换原按钮,但这可能丢失 Kendo 附加的数据和样式。直接覆盖是另一种方法,即不用.off()
直接.on()
,但要确保preventDefault()
和stopPropagation()
被调用。 - 维护现有筛选: 代码中尝试保留对其他字段的筛选条件,仅修改当前字段的筛选。这对于复杂的 Grid 筛选场景很重要。
- 处理"Clear"按钮: 不仅要定制"Filter"按钮,"Clear"按钮的行为也需要覆盖,确保它能正确地清除我们施加的大小写敏感筛选,而不是 Kendo 默认的行为。
- 服务器端筛选: 如果你的 Grid 使用服务器端筛选 (
serverFiltering: true
),这种客户端拦截的方法就不适用了。筛选逻辑需要在服务器端实现。
进阶技巧:
- 可以封装
enhanceFilterMenu
函数,使其更通用,能处理多个需要大小写敏感筛选的列。 - 考虑性能:如果复选框列表非常长,DOM 操作和事件处理可能会有轻微性能影响,但在典型场景下通常可接受。
方法二:后端处理 - 把大小写敏感交给服务器
如果你的数据量较大,或者你本身就在使用服务器端筛选、分页和排序,那么在服务器端实现大小写敏感筛选是更推荐、也更高效的做法。
原理和作用:
配置 Kendo UI Grid 的 DataSource 使用服务器端操作 (serverFiltering: true
)。这样,当用户在筛选菜单中进行选择并点击 "Filter" 时,Kendo UI 不会在客户端执行筛选,而是将筛选条件(哪个字段、操作符、值)作为参数发送到你指定的服务器端点 (URL)。你的服务器端代码接收这些参数,然后在数据库查询或数据处理时,强制执行大小写敏感的比较。
操作步骤和代码示例:
-
配置 Kendo UI DataSource:
$("#grid").kendoGrid({ dataSource: { transport: { read: { url: "/api/data/read", // 你的数据读取 API 端点 dataType: "json", type: "POST" // 或者 GET,根据你的 API 设计 }, parameterMap: function(options, operation) { if (operation === "read") { // Kendo UI 会自动将 filter, sort, page, pageSize 等信息 // 放在 options 对象里。通常直接返回即可,Kendo 会格式化。 // 如果你的后端需要特定格式,可以在这里转换。 // 关键是确保 filter 信息能传递到后端。 return JSON.stringify(options); // 或者 return options; 如果后端能处理 Kendo 默认格式 } } }, schema: { data: "Data", // 根据你的 API 返回结构调整 total: "Total" // 根据你的 API 返回结构调整 }, serverFiltering: true, // 启用服务器端筛选 serverPaging: true, // 通常一起启用 serverSorting: true, // 通常一起启用 pageSize: 20 }, // ... 其他 Grid 配置 ... filterable: { /* ... */ }, columns: [ /* ... */ ] });
-
实现服务器端逻辑 (概念性示例):
后端需要解析 Kendo UI 发送过来的筛选参数(通常是一个包含logic
和filters
数组的结构),然后构建相应的数据库查询。-
SQL Server 示例:
在 SQL 查询的WHERE
子句中,对需要大小写敏感的列使用COLLATE
子句指定一个区分大小写的排序规则。-- 假设接收到的参数是 field='Country', value='usa' -- 使用区分大小写的排序规则,如 Latin1_General_CS_AS (CS=Case Sensitive) SELECT * FROM Products WHERE Country = 'usa' COLLATE Latin1_General_CS_AS; -- 如果是多选 ('usa', 'Usa') 并且是 OR 逻辑 SELECT * FROM Products WHERE (Country = 'usa' COLLATE Latin1_General_CS_AS OR Country = 'Usa' COLLATE Latin1_General_CS_AS); -- 或者使用 IN 操作符,同样需要 COLLATE SELECT * FROM Products WHERE Country COLLATE Latin1_General_CS_AS IN ('usa', 'Usa');
-
PostgreSQL/MySQL 示例:
PostgreSQL 默认可能区分大小写,但也依赖于列的 Collation。MySQL 默认通常不区分,可以使用BINARY
操作符或设置列的 Collation 为utf8mb4_bin
等。-- PostgreSQL (如果默认不是 case-sensitive) 或 MySQL SELECT * FROM Products WHERE BINARY Country = 'usa'; -- 或使用 IN SELECT * FROM Products WHERE BINARY Country IN ('usa', 'Usa'); -- PostgreSQL 也可以用 COLLATE -- SELECT * FROM Products WHERE Country = 'usa' COLLATE "C";
-
C# (Entity Framework Core) 示例:
在使用 EF Core 时,字符串比较的默认行为可能受数据库影响。可以明确使用区分大小写的方法。// 假设 query 是 IQueryable<Product> // 对于 'eq' 操作符 query = query.Where(p => EF.Functions.Collate(p.Country, "Latin1_General_CS_AS") == "usa"); // 或者使用 string.Equals with StringComparison for client-side evaluation simulation // 但最好是在数据库层面处理 // query = query.Where(p => p.Country.Equals("usa", StringComparison.Ordinal)); // Ordinal is case-sensitive // 对于多选 ('usa', 'Usa') 的 IN 操作 (OR 逻辑) var selectedValues = new List<string> { "usa", "Usa" }; // EF Core 6+ 自动将 .Contains 翻译成 SQL IN // 但需要确保数据库或 Collation 支持大小写敏感比较 query = query.Where(p => selectedValues.Select(v => EF.Functions.Collate(v, "Latin1_General_CS_AS")).Contains(EF.Functions.Collate(p.Country, "Latin1_General_CS_AS"))); // 一个更直接可能有效的方式(取决于EF Core版本和Provider): // query = query.Where(p => selectedValues.Contains(p.Country, StringComparer.Ordinal));
-
安全建议:
- SQL 注入防护: 在服务器端构建 SQL 查询时,绝对不要 直接拼接用户输入(包括筛选值)到 SQL 字符串中。必须使用参数化查询 (Parameterized Queries) 或 ORM 提供的安全机制来防止 SQL 注入攻击。
- 数据库 Collation: 了解你数据库、表、列的默认排序规则 (Collation)。有时,将相关列的 Collation 直接设置为大小写敏感的类型,可以简化查询逻辑。
进阶技巧:
- 后端框架通常有库可以帮助解析 Kendo UI 的 DataSource 请求参数,简化开发。
- 对于非常复杂的筛选逻辑,服务器端提供了更大的灵活性和控制力。
方法三:修改数据源(不推荐,但可能)
还有一种理论上的方法是在加载数据时,或者在 DataSource 的 schema.parse
或 requestEnd
事件中,对需要区分大小写的字段值做某种“标记”或转换,然后在筛选时利用这个标记。例如,将 "usa" 存储为 "usa##cs", "USA" 存储为 "USA##cs"。筛选时,你也转换筛选值并进行匹配。但这会污染原始数据,增加复杂度,并且使得显示和编辑变得困难,通常不推荐这样做。
哪种方法更适合你?
- 如果你主要在客户端处理数据(数据量不大) ,并且只需要对少数几列实现大小写敏感筛选,那么方法一 (
filterMenuInit
事件) 是一个相对轻量级的选择,因为它不涉及后端改动。但需要仔细处理 DOM 操作和事件绑定。 - 如果你的数据量大,或者已经在用服务器端分页/排序/筛选 ,那么方法二 (服务器端处理) 是更健壮、性能更好、逻辑更清晰的选择。它将复杂性移到了后端,与数据源更近,但需要你有修改后端代码的能力。
选择哪种方法,取决于你的具体应用场景、技术栈、数据规模以及对前后端代码的控制能力。对于要求精确匹配大小写的场景,摆脱 Kendo UI 的默认行为是完全可行的。