返回

Kendo UI Grid复选框筛选:如何区分大小写?

javascript

解决 Kendo UI Grid 复选框筛选器的大小写不敏感问题

问题来了:Kendo Grid 复选框筛选怎么区分大小写?

用 Kendo UI Grid 的时候,你可能遇到过这样一个情况:列头那个复选框筛选器 (Checkbox Filter),勾选 'usa',结果把 'USA' 和 'Usa' 也一股脑儿筛出来了。这默认行为有时候挺方便,但碰上需要严格区分大小写的场景,就有点头疼了。

就像下面这个例子展示的(源自社区提问),Grid 里有三行数据,国家分别是 "USA", "usa", "Usa"。期望是勾选筛选器里的 "usa" 时,只显示值为 "usa" 的那一行。可实际情况是,三行全出来了。

Kendo UI Grid Checkbox Filter Case-Insensitive Behavior
图片来源:Stack Overflow 用户提问

原始的 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();
         }
    });
}

注意要点:

  1. DOM 结构依赖: 上述代码中的选择器(如 button[type=submit].k-button-primary, [data-role='listview'], input[type=checkbox])是基于 Kendo UI 常见的 HTML 结构。不同版本或主题下,这些结构可能微调。你需要使用浏览器开发者工具检查实际生成的 HTML,确保选择器能准确命中目标元素。
  2. 获取选中值: 如何准确获取复选框对应的值也取决于 Kendo UI 的实现。它可能在 inputvalue 属性,也可能在 data-* 属性,或者关联的 labelspan 的文本里。务必确认来源。
  3. 事件解绑和重绑: filterButton.off("click.kendoFilterMenu") 尝试移除 Kendo UI 可能添加的特定事件。如果这不起作用,你可能需要更暴力地 filterButton.off("click"),但这有风险移除其他监听器。或者考虑克隆按钮替换原按钮,但这可能丢失 Kendo 附加的数据和样式。直接覆盖是另一种方法,即不用 .off() 直接 .on(),但要确保 preventDefault()stopPropagation() 被调用。
  4. 维护现有筛选: 代码中尝试保留对其他字段的筛选条件,仅修改当前字段的筛选。这对于复杂的 Grid 筛选场景很重要。
  5. 处理"Clear"按钮: 不仅要定制"Filter"按钮,"Clear"按钮的行为也需要覆盖,确保它能正确地清除我们施加的大小写敏感筛选,而不是 Kendo 默认的行为。
  6. 服务器端筛选: 如果你的 Grid 使用服务器端筛选 (serverFiltering: true),这种客户端拦截的方法就不适用了。筛选逻辑需要在服务器端实现。

进阶技巧:

  • 可以封装 enhanceFilterMenu 函数,使其更通用,能处理多个需要大小写敏感筛选的列。
  • 考虑性能:如果复选框列表非常长,DOM 操作和事件处理可能会有轻微性能影响,但在典型场景下通常可接受。

方法二:后端处理 - 把大小写敏感交给服务器

如果你的数据量较大,或者你本身就在使用服务器端筛选、分页和排序,那么在服务器端实现大小写敏感筛选是更推荐、也更高效的做法。

原理和作用:

配置 Kendo UI Grid 的 DataSource 使用服务器端操作 (serverFiltering: true)。这样,当用户在筛选菜单中进行选择并点击 "Filter" 时,Kendo UI 不会在客户端执行筛选,而是将筛选条件(哪个字段、操作符、值)作为参数发送到你指定的服务器端点 (URL)。你的服务器端代码接收这些参数,然后在数据库查询或数据处理时,强制执行大小写敏感的比较。

操作步骤和代码示例:

  1. 配置 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: [ /* ... */ ]
    });
    
  2. 实现服务器端逻辑 (概念性示例):
    后端需要解析 Kendo UI 发送过来的筛选参数(通常是一个包含 logicfilters 数组的结构),然后构建相应的数据库查询。

    • 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.parserequestEnd 事件中,对需要区分大小写的字段值做某种“标记”或转换,然后在筛选时利用这个标记。例如,将 "usa" 存储为 "usa##cs", "USA" 存储为 "USA##cs"。筛选时,你也转换筛选值并进行匹配。但这会污染原始数据,增加复杂度,并且使得显示和编辑变得困难,通常不推荐这样做。

哪种方法更适合你?

  • 如果你主要在客户端处理数据(数据量不大) ,并且只需要对少数几列实现大小写敏感筛选,那么方法一 (filterMenuInit 事件) 是一个相对轻量级的选择,因为它不涉及后端改动。但需要仔细处理 DOM 操作和事件绑定。
  • 如果你的数据量大,或者已经在用服务器端分页/排序/筛选 ,那么方法二 (服务器端处理) 是更健壮、性能更好、逻辑更清晰的选择。它将复杂性移到了后端,与数据源更近,但需要你有修改后端代码的能力。

选择哪种方法,取决于你的具体应用场景、技术栈、数据规模以及对前后端代码的控制能力。对于要求精确匹配大小写的场景,摆脱 Kendo UI 的默认行为是完全可行的。