diff --git a/src/BootstrapBlazor/Components/Filters/MultiFilter.razor b/src/BootstrapBlazor/Components/Filters/MultiFilter.razor
index df3a9cd937a..6c0fdcfda36 100644
--- a/src/BootstrapBlazor/Components/Filters/MultiFilter.razor
+++ b/src/BootstrapBlazor/Components/Filters/MultiFilter.razor
@@ -12,7 +12,7 @@ else
@if (ShowSearch)
{
}
diff --git a/src/BootstrapBlazor/Components/Filters/MultiSelectFilter.razor b/src/BootstrapBlazor/Components/Filters/MultiSelectFilter.razor
new file mode 100644
index 00000000000..951bee5182d
--- /dev/null
+++ b/src/BootstrapBlazor/Components/Filters/MultiSelectFilter.razor
@@ -0,0 +1,15 @@
+@namespace BootstrapBlazor.Components
+@inherits FilterBase
+@typeparam TType
+
+@if (IsHeaderRow)
+{
+
+
+
+}
+else
+{
+
+}
diff --git a/src/BootstrapBlazor/Components/Filters/MultiSelectFilter.razor.cs b/src/BootstrapBlazor/Components/Filters/MultiSelectFilter.razor.cs
new file mode 100644
index 00000000000..6f96bb7e325
--- /dev/null
+++ b/src/BootstrapBlazor/Components/Filters/MultiSelectFilter.razor.cs
@@ -0,0 +1,90 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the Apache 2.0 License
+// See the LICENSE file in the project root for more information.
+// Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone
+
+namespace BootstrapBlazor.Components;
+
+///
+/// 多项选择下拉框过滤组件
+///
+public partial class MultiSelectFilter
+{
+ private string? FilterRowClassString => CssBuilder.Default("filter-row")
+ .AddClass("active", TableColumnFilter.HasFilter())
+ .Build();
+
+ private TType? _value1;
+ private FilterAction _action1 = FilterAction.Equal;
+
+ ///
+ /// Gets or sets the filter items.
+ ///
+ [Parameter]
+ public List? Items { get; set; }
+
+ ///
+ ///
+ ///
+ public override void Reset()
+ {
+ _value1 = default;
+ _action1 = FilterAction.Equal;
+ Count = 0;
+ StateHasChanged();
+ }
+
+ ///
+ ///
+ ///
+ ///
+ public override FilterKeyValueAction GetFilterConditions()
+ {
+ var filter = new FilterKeyValueAction() { FilterLogic = FilterLogic.Or };
+ if (_value1 is string v)
+ {
+ var items = v.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
+ foreach (var item in items)
+ {
+ filter.Filters.Add(new FilterKeyValueAction
+ {
+ FieldKey = FieldKey,
+ FieldValue = item,
+ FilterAction = _action1
+ });
+ }
+ }
+ else if (_value1 is IEnumerable values)
+ {
+ foreach (var item in values)
+ {
+ filter.Filters.Add(new FilterKeyValueAction
+ {
+ FieldKey = FieldKey,
+ FieldValue = item,
+ FilterAction = _action1
+ });
+ }
+ }
+ return filter;
+ }
+
+ ///
+ ///
+ ///
+ public override async Task SetFilterConditionsAsync(FilterKeyValueAction filter)
+ {
+ var first = filter.Filters.FirstOrDefault() ?? filter;
+ if (first.FieldValue is TType value)
+ {
+ _value1 = value;
+ }
+ else
+ {
+ _value1 = default;
+ }
+ _action1 = first.FilterAction;
+
+ await base.SetFilterConditionsAsync(filter);
+ }
+}
diff --git a/test/UnitTest/Components/TableMultiSelectFilterTest.cs b/test/UnitTest/Components/TableMultiSelectFilterTest.cs
new file mode 100644
index 00000000000..003b40f13aa
--- /dev/null
+++ b/test/UnitTest/Components/TableMultiSelectFilterTest.cs
@@ -0,0 +1,149 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the Apache 2.0 License
+// See the LICENSE file in the project root for more information.
+// Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone
+
+namespace UnitTest.Components;
+
+public class TableMultiSelectFilterTest : BootstrapBlazorTestBase
+{
+ [Fact]
+ public async Task IsHeaderRow_Ok()
+ {
+ var cut = Context.RenderComponent(pb =>
+ {
+ pb.Add(a => a.Table, new MockTable());
+ pb.Add(a => a.Column, new MockColumn());
+ pb.Add(a => a.IsHeaderRow, true);
+ });
+ cut.Contains("filter-row");
+
+ var actions = cut.FindAll(".dropdown-item");
+ await cut.InvokeAsync(() => actions[1].Click());
+
+ // check filter
+ var filter = cut.Instance;
+ var conditions = filter.FilterAction.GetFilterConditions();
+ Assert.Single(conditions.Filters);
+ }
+
+ [Fact]
+ public async Task OnFilterAsync_Ok()
+ {
+ var cut = Context.RenderComponent(pb =>
+ {
+ pb.Add(a => a.Table, new MockTable());
+ pb.Add(a => a.Column, new MockColumn());
+ pb.Add(a => a.IsHeaderRow, false);
+ });
+ var item = cut.Find(".dropdown-menu .dropdown-item");
+ await cut.InvokeAsync(() => item.Click());
+
+ var filter = cut.FindComponent>();
+ var conditions = filter.Instance.GetFilterConditions();
+
+ Assert.Single(conditions.Filters);
+
+ await cut.InvokeAsync(() => filter.Instance.Reset());
+ conditions = filter.Instance.GetFilterConditions();
+ Assert.Empty(conditions.Filters);
+
+ await cut.InvokeAsync(() => filter.Instance.SetFilterConditionsAsync(new FilterKeyValueAction()
+ {
+ FieldValue = "v1,v2",
+ }));
+ conditions = filter.Instance.GetFilterConditions();
+ Assert.Equal(2, conditions.Filters.Count);
+
+ await cut.InvokeAsync(() => filter.Instance.SetFilterConditionsAsync(new FilterKeyValueAction()
+ {
+ FieldValue = true,
+ }));
+ conditions = filter.Instance.GetFilterConditions();
+ Assert.Empty(conditions.Filters);
+ }
+
+ [Fact]
+ public async Task OnFilterAsync_List()
+ {
+ var cut = Context.RenderComponent(pb =>
+ {
+ pb.Add(a => a.Table, new MockTable());
+ pb.Add(a => a.Column, new MockColumnList());
+ pb.Add(a => a.IsHeaderRow, false);
+ });
+ var item = cut.Find(".dropdown-menu .dropdown-item");
+ await cut.InvokeAsync(() => item.Click());
+
+ var filter = cut.FindComponent>>();
+ var conditions = filter.Instance.GetFilterConditions();
+
+ Assert.Single(conditions.Filters);
+
+ await cut.InvokeAsync(() => filter.Instance.Reset());
+ conditions = filter.Instance.GetFilterConditions();
+ Assert.Empty(conditions.Filters);
+ }
+
+ class MockTable : ITable
+ {
+ public Dictionary Filters { get; set; } = [];
+
+ public Func? OnFilterAsync { get; set; }
+
+ public List Columns => [];
+
+ public IEnumerable GetVisibleColumns() => Columns;
+ }
+
+ class MockColumn : TableColumn
+ {
+ public MockColumn()
+ {
+ PropertyType = typeof(string);
+ FieldName = "MultiSelect";
+
+ FilterTemplate = new RenderFragment(builder =>
+ {
+ builder.OpenComponent(0);
+ builder.AddAttribute(1, nameof(FilterProvider.ChildContent), new RenderFragment(builder =>
+ {
+ builder.OpenComponent>(2);
+ builder.AddAttribute(3, nameof(MultiSelectFilter.Items), new List
+ {
+ new("v1", "Test-1"),
+ new("v2", "Test-2")
+ });
+ builder.CloseComponent();
+ }));
+ builder.CloseComponent();
+ });
+ }
+ }
+
+ class MockColumnList : TableColumn>
+ {
+ public MockColumnList()
+ {
+ PropertyType = typeof(List);
+ FieldName = "MultiSelect";
+
+ FilterTemplate = new RenderFragment(builder =>
+ {
+ builder.OpenComponent(0);
+ builder.AddAttribute(1, nameof(FilterProvider.ChildContent), new RenderFragment(builder =>
+ {
+ builder.OpenComponent>>(2);
+ builder.AddAttribute(3, nameof(MultiSelectFilter>.Items), new List
+ {
+ new("v1", "Test-1"),
+ new("v2", "Test-2")
+ });
+ builder.CloseComponent();
+ }));
+ builder.CloseComponent();
+ });
+ }
+ }
+
+}