From 3f7feb1447502135ac24726fba70d736a2aabda9 Mon Sep 17 00:00:00 2001 From: Argo-AsicoTech Date: Wed, 18 Dec 2024 16:19:54 +0800 Subject: [PATCH 1/7] =?UTF-8?q?refactor:=20=E5=A2=9E=E5=8A=A0=20Key=20?= =?UTF-8?q?=E5=85=B3=E9=94=AE=E5=AD=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/BootstrapBlazor/Components/Checkbox/CheckboxList.razor | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/BootstrapBlazor/Components/Checkbox/CheckboxList.razor b/src/BootstrapBlazor/Components/Checkbox/CheckboxList.razor index b2a6ae0df1b..da2432aefb7 100644 --- a/src/BootstrapBlazor/Components/Checkbox/CheckboxList.razor +++ b/src/BootstrapBlazor/Components/Checkbox/CheckboxList.razor @@ -25,7 +25,7 @@ else
@foreach (var item in Items) { -
+
Date: Wed, 18 Dec 2024 16:20:08 +0800 Subject: [PATCH 2/7] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=20CheckboxGeneri?= =?UTF-8?q?c=20=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Checkbox/CheckboxListGeneric.razor | 36 +++ .../Checkbox/CheckboxListGeneric.razor.cs | 220 ++++++++++++++++++ 2 files changed, 256 insertions(+) create mode 100644 src/BootstrapBlazor/Components/Checkbox/CheckboxListGeneric.razor create mode 100644 src/BootstrapBlazor/Components/Checkbox/CheckboxListGeneric.razor.cs diff --git a/src/BootstrapBlazor/Components/Checkbox/CheckboxListGeneric.razor b/src/BootstrapBlazor/Components/Checkbox/CheckboxListGeneric.razor new file mode 100644 index 00000000000..da2432aefb7 --- /dev/null +++ b/src/BootstrapBlazor/Components/Checkbox/CheckboxListGeneric.razor @@ -0,0 +1,36 @@ +@namespace BootstrapBlazor.Components +@typeparam TValue +@inherits ValidateBase + +@if (IsShowLabel) +{ + +} + +@if (IsButton) +{ +
+
+ @foreach (var item in Items) + { + + @item.Text + + } +
+
+} +else +{ +
+ @foreach (var item in Items) + { +
+ +
+ } +
+} diff --git a/src/BootstrapBlazor/Components/Checkbox/CheckboxListGeneric.razor.cs b/src/BootstrapBlazor/Components/Checkbox/CheckboxListGeneric.razor.cs new file mode 100644 index 00000000000..73f215922bf --- /dev/null +++ b/src/BootstrapBlazor/Components/Checkbox/CheckboxListGeneric.razor.cs @@ -0,0 +1,220 @@ +// 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 + +using Microsoft.Extensions.Localization; +using System.Collections; +using System.Reflection; + +namespace BootstrapBlazor.Components; + +/// +/// CheckboxList 组件基类 +/// +public partial class CheckboxListGeneric : ValidateBase +{ + /// + /// 获得 组件样式 + /// + private string? ClassString => CssBuilder.Default("checkbox-list form-control") + .AddClass("no-border", !ShowBorder && ValidCss != "is-invalid") + .AddClass("is-vertical", IsVertical) + .AddClass(CssClass).AddClass(ValidCss) + .Build(); + + /// + /// 获得 组件内部 Checkbox 项目样式 + /// + protected string? CheckboxItemClassString => CssBuilder.Default("checkbox-item") + .AddClass(CheckboxItemClass) + .Build(); + + private string? ButtonClassString => CssBuilder.Default("checkbox-list is-button") + .AddClassFromAttributes(AdditionalAttributes) + .Build(); + + private string? ButtonGroupClassString => CssBuilder.Default("btn-group") + .AddClass("disabled", IsDisabled) + .AddClass("btn-group-vertical", IsVertical) + .Build(); + + private string? GetButtonItemClassString(SelectedItem item) => CssBuilder.Default("btn") + .AddClass($"active bg-{Color.ToDescriptionString()}", item.Value != null && (Value?.Contains(item.Value) ?? false)) + .Build(); + + /// + /// 获得/设置 数据源 + /// + [Parameter] + [NotNull] + public IEnumerable>? Items { get; set; } + + /// + /// 获得/设置 组件值 + /// + [Parameter] + public new List? Value { get; set; } + + /// + /// 获得/设置 是否为按钮样式 默认 false + /// + [Parameter] + public bool IsButton { get; set; } + + /// + /// 获得/设置 Checkbox 组件布局样式 + /// + [Parameter] + public string? CheckboxItemClass { get; set; } + + /// + /// 获得/设置 是否显示边框 默认为 true + /// + [Parameter] + public bool ShowBorder { get; set; } = true; + + /// + /// 获得/设置 是否为竖向排列 默认为 false + /// + [Parameter] + public bool IsVertical { get; set; } + + /// + /// 获得/设置 按钮颜色 默认为 None 未设置 + /// + [Parameter] + public Color Color { get; set; } + + /// + /// 获得/设置 SelectedItemChanged 方法 + /// + [Parameter] + public Func>, List, Task>? OnSelectedChanged { get; set; } + + /// + /// 获得/设置 最多选中数量 + /// + [Parameter] + public int MaxSelectedCount { get; set; } + + /// + /// 获得/设置 超过最大选中数量时回调委托 + /// + [Parameter] + public Func? OnMaxSelectedCountExceed { get; set; } + + [Inject] + [NotNull] + private IStringLocalizerFactory? LocalizerFactory { get; set; } + + /// + /// 获得 当前选项是否被禁用 + /// + /// + /// + protected bool GetDisabledState(SelectedItem item) => IsDisabled || item.IsDisabled; + + private Func>? _onBeforeStateChangedCallback; + + /// + /// OnInitialized 方法 + /// + protected override void OnInitialized() + { + base.OnInitialized(); + + // 处理 Required 标签 + if (EditContext != null && FieldIdentifier != null) + { + var pi = FieldIdentifier.Value.Model.GetType().GetPropertyByName(FieldIdentifier.Value.FieldName); + if (pi != null) + { + var required = pi.GetCustomAttribute(true); + if (required != null) + { + Rules.Add(new RequiredValidator() + { + LocalizerFactory = LocalizerFactory, + ErrorMessage = required.ErrorMessage, + AllowEmptyString = required.AllowEmptyStrings + }); + } + } + } + } + + /// + /// OnParametersSet 方法 + /// + protected override void OnParametersSet() + { + base.OnParametersSet(); + + if (IsButton && Color == Color.None) + { + Color = Color.Primary; + } + + if (Items == null) + { + var t = typeof(TValue); + var innerType = t.GetGenericArguments().FirstOrDefault(); + if (innerType != null) + { + Items = innerType.ToSelectList(); + } + Items ??= []; + } + + _onBeforeStateChangedCallback = MaxSelectedCount > 0 ? new Func>(OnBeforeStateChanged) : null; + } + private async Task OnBeforeStateChanged(CheckboxState state) + { + var ret = true; + if (state == CheckboxState.Checked) + { + var items = Items.Where(i => i.Active).ToList(); + ret = items.Count < MaxSelectedCount; + } + + if (!ret && OnMaxSelectedCountExceed != null) + { + await OnMaxSelectedCountExceed(); + } + return ret; + } + + /// + /// Checkbox 组件选项状态改变时触发此方法 + /// + /// + /// + private async Task OnStateChanged(SelectedItem item, bool v) + { + if (item.Value == null) + { + return; + } + + Value ??= []; + if (v) + { + Value.Add(item.Value); + } + else + { + Value.Remove(item.Value); + } + + if (OnSelectedChanged != null) + { + await OnSelectedChanged(Items, Value); + } + } + + /// + /// 点击选择框方法 + /// + private Task OnClick(SelectedItem item) => OnStateChanged(item, !item.Active); +} From 90b5376720406cbbb4dad60ca69f3cc76aedab4c Mon Sep 17 00:00:00 2001 From: Argo-AsicoTech Date: Wed, 18 Dec 2024 20:14:54 +0800 Subject: [PATCH 3/7] =?UTF-8?q?refactor:=20=E6=9B=B4=E6=96=B0=E6=B3=9B?= =?UTF-8?q?=E5=9E=8B=20CheckboxList=20=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Checkbox/CheckboxListGeneric.razor | 2 +- .../Checkbox/CheckboxListGeneric.razor.cs | 31 ++++++++++++------- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/src/BootstrapBlazor/Components/Checkbox/CheckboxListGeneric.razor b/src/BootstrapBlazor/Components/Checkbox/CheckboxListGeneric.razor index da2432aefb7..ca60e3fffdf 100644 --- a/src/BootstrapBlazor/Components/Checkbox/CheckboxListGeneric.razor +++ b/src/BootstrapBlazor/Components/Checkbox/CheckboxListGeneric.razor @@ -1,6 +1,6 @@ @namespace BootstrapBlazor.Components @typeparam TValue -@inherits ValidateBase +@inherits ValidateBase> @if (IsShowLabel) { diff --git a/src/BootstrapBlazor/Components/Checkbox/CheckboxListGeneric.razor.cs b/src/BootstrapBlazor/Components/Checkbox/CheckboxListGeneric.razor.cs index 73f215922bf..e1590fc3585 100644 --- a/src/BootstrapBlazor/Components/Checkbox/CheckboxListGeneric.razor.cs +++ b/src/BootstrapBlazor/Components/Checkbox/CheckboxListGeneric.razor.cs @@ -12,7 +12,7 @@ namespace BootstrapBlazor.Components; /// /// CheckboxList 组件基类 /// -public partial class CheckboxListGeneric : ValidateBase +public partial class CheckboxListGeneric { /// /// 获得 组件样式 @@ -50,12 +50,6 @@ public partial class CheckboxListGeneric : ValidateBase [NotNull] public IEnumerable>? Items { get; set; } - /// - /// 获得/设置 组件值 - /// - [Parameter] - public new List? Value { get; set; } - /// /// 获得/设置 是否为按钮样式 默认 false /// @@ -168,6 +162,15 @@ protected override void OnParametersSet() } _onBeforeStateChangedCallback = MaxSelectedCount > 0 ? new Func>(OnBeforeStateChanged) : null; + + // set item active + if (Value != null) + { + foreach (var item in Items) + { + item.Active = Value.Contains(item.Value); + } + } } private async Task OnBeforeStateChanged(CheckboxState state) { @@ -197,19 +200,25 @@ private async Task OnStateChanged(SelectedItem item, bool v) return; } - Value ??= []; + var vals = new List(); + if (Value != null) + { + vals.AddRange(Value); + } if (v) { - Value.Add(item.Value); + vals.Add(item.Value); } else { - Value.Remove(item.Value); + vals.Remove(item.Value); } + CurrentValue = vals; + if (OnSelectedChanged != null) { - await OnSelectedChanged(Items, Value); + await OnSelectedChanged(Items, CurrentValue); } } From 7141422dad2803657fe1e69a35dc22b70bb9e27a Mon Sep 17 00:00:00 2001 From: Argo-AsicoTech Date: Wed, 18 Dec 2024 20:16:38 +0800 Subject: [PATCH 4/7] =?UTF-8?q?doc:=20=E6=9B=B4=E6=96=B0=E7=A4=BA=E4=BE=8B?= =?UTF-8?q?=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Components/Samples/CheckboxLists.razor | 13 ++++++++++++ .../Components/Samples/CheckboxLists.razor.cs | 21 +++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/src/BootstrapBlazor.Server/Components/Samples/CheckboxLists.razor b/src/BootstrapBlazor.Server/Components/Samples/CheckboxLists.razor index 418f028cccd..c054cccbc5a 100644 --- a/src/BootstrapBlazor.Server/Components/Samples/CheckboxLists.razor +++ b/src/BootstrapBlazor.Server/Components/Samples/CheckboxLists.razor @@ -120,6 +120,19 @@ + + +
+ @if (_selectedFoos != null) + { + foreach (var item in _selectedFoos) + { +
@item.Name
+ } + } +
+
+ diff --git a/src/BootstrapBlazor.Server/Components/Samples/CheckboxLists.razor.cs b/src/BootstrapBlazor.Server/Components/Samples/CheckboxLists.razor.cs index e7f7d113eb2..c3d38486a91 100644 --- a/src/BootstrapBlazor.Server/Components/Samples/CheckboxLists.razor.cs +++ b/src/BootstrapBlazor.Server/Components/Samples/CheckboxLists.razor.cs @@ -37,6 +37,11 @@ public partial class CheckboxLists [NotNull] private IEnumerable? Items5 { get; set; } + [NotNull] + private IEnumerable>? GenericItems { get; set; } + + private List? _selectedFoos; + /// /// OnInitialized method /// @@ -89,6 +94,22 @@ protected override void OnInitialized() FooItems = Foo.GenerateHobbies(LocalizerFoo); } + /// + /// + /// + /// + protected override async Task OnInitializedAsync() + { + await base.OnInitializedAsync(); + + GenericItems = new List>() + { + new() { Text = Localizer["item1"], Value = new Foo() { Name = LocalizerFoo["Foo.Name", "001"] } }, + new() { Text = Localizer["item2"], Value = new Foo() { Name = LocalizerFoo["Foo.Name", "002"] } }, + new() { Text = Localizer["item3"], Value = new Foo() { Name = LocalizerFoo["Foo.Name", "003"] } }, + }; + } + [NotNull] private Foo? Model { get; set; } From 8ad21d1428d9e42ca036f7feb926e613ced5898e Mon Sep 17 00:00:00 2001 From: Argo-AsicoTech Date: Fri, 20 Dec 2024 14:52:41 +0800 Subject: [PATCH 5/7] =?UTF-8?q?test:=20=E5=A2=9E=E5=8A=A0=E5=8D=95?= =?UTF-8?q?=E5=85=83=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Components/CheckboxListGenericTest.cs | 342 ++++++++++++++++++ 1 file changed, 342 insertions(+) create mode 100644 test/UnitTest/Components/CheckboxListGenericTest.cs diff --git a/test/UnitTest/Components/CheckboxListGenericTest.cs b/test/UnitTest/Components/CheckboxListGenericTest.cs new file mode 100644 index 00000000000..ac7cce8a88f --- /dev/null +++ b/test/UnitTest/Components/CheckboxListGenericTest.cs @@ -0,0 +1,342 @@ +// 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 + +using Microsoft.Extensions.Localization; + +namespace UnitTest.Components; + +public class CheckboxListGenericTest : BootstrapBlazorTestBase +{ + private IStringLocalizer Localizer { get; } + + public CheckboxListGenericTest() + { + Localizer = Context.Services.GetRequiredService>(); + } + + [Fact] + public void EditorForm_Ok() + { + var dummy = new Dummy(); + var cut = Context.RenderComponent(builder => + { + builder.Add(a => a.Model, dummy); + builder.AddChildContent>>(pb => + { + pb.Add(a => a.Items, Foo.GenerateHobbies(Localizer)); + pb.Add(a => a.Value, dummy.Data); + pb.Add(a => a.ValueExpression, Utility.GenerateValueExpression(dummy, nameof(dummy.Data), typeof(List))); + }); + }); + // 断言生成 CheckboxList + Assert.Contains("form-check is-label", cut.Markup); + cut.Contains("是/否"); + + // 提交表单触发客户端验证 + var form = cut.Find("form"); + form.Submit(); + Assert.Contains("is-invalid", cut.Markup); + } + + [Fact] + public void ShowBorder_Ok() + { + var foo = Foo.Generate(Localizer); + var cut = Context.RenderComponent>>(pb => + { + pb.Add(a => a.Items, Foo.GenerateHobbies(Localizer)); + pb.Add(a => a.Value, foo.Hobby); + }); + Assert.DoesNotContain("no-border", cut.Markup); + + cut.SetParametersAndRender(pb => + { + pb.Add(a => a.ShowBorder, false); + }); + Assert.Contains("no-border", cut.Markup); + } + + [Fact] + public void IsVertical_Ok() + { + var cut = Context.RenderComponent>>(); + Assert.DoesNotContain("is-vertical", cut.Markup); + + cut.SetParametersAndRender(pb => + { + pb.Add(a => a.IsVertical, true); + }); + Assert.Contains("is-vertical", cut.Markup); + } + + [Fact] + public void IsDisabled_Ok() + { + var cut = Context.RenderComponent>>(pb => + { + pb.Add(a => a.Items, new List() + { + new() { Text = "Item 1", Value = "1" }, + new() { Text = "Item 2", Value = "2" , IsDisabled = true }, + new() { Text = "Item 3", Value = "3" }, + }); + }); + cut.Contains("form-check is-label disabled"); + + cut.SetParametersAndRender(pb => + { + pb.Add(a => a.Items, new List() + { + new() { Text = "Item 1", Value = "1" }, + new() { Text = "Item 2", Value = "2" } + }); + pb.Add(a => a.IsDisabled, true); + }); + cut.Contains("form-check is-label disabled"); + } + + [Fact] + public void CheckboxItemClass_Ok() + { + var cut = Context.RenderComponent>(builder => + { + builder.Add(a => a.CheckboxItemClass, "test-item"); + }); + Assert.DoesNotContain("test-item", cut.Markup); + + cut.SetParametersAndRender(pb => + { + pb.Add(a => a.Items, Foo.GenerateHobbies(Localizer)); + }); + Assert.Contains("test-item", cut.Markup); + } + + [Fact] + public async Task StringValue_Ok() + { + var cut = Context.RenderComponent>(builder => + { + builder.Add(a => a.Value, "1,2"); + }); + Assert.Contains("checkbox-list", cut.Markup); + + cut.SetParametersAndRender(pb => + { + pb.Add(a => a.Items, new List() + { + new("1", "Test 1"), + new("2", "Test 2") + }); + }); + Assert.Contains("checkbox-list", cut.Markup); + + var selected = false; + cut.SetParametersAndRender(pb => + { + pb.Add(a => a.OnSelectedChanged, (v1, v2) => + { + selected = true; + return Task.CompletedTask; + }); + }); + // 字符串值选中事件 + var item = cut.FindComponent>(); + await cut.InvokeAsync(item.Instance.OnToggleClick); + Assert.True(selected); + } + + [Fact] + public async Task OnSelectedChanged_Ok() + { + var selected = false; + var foo = Foo.Generate(Localizer); + var cut = Context.RenderComponent>>(pb => + { + pb.Add(a => a.Items, Foo.GenerateHobbies(Localizer)); + pb.Add(a => a.Value, foo.Hobby); + pb.Add(a => a.OnSelectedChanged, (v1, v2) => + { + selected = true; + return Task.CompletedTask; + }); + }); + + var item = cut.FindComponent>(); + await cut.InvokeAsync(item.Instance.OnToggleClick); + Assert.True(selected); + } + + [Fact] + public void EnumValue_Ok() + { + var selectedEnumValues = new List { EnumEducation.Middle, EnumEducation.Primary }; + var cut = Context.RenderComponent>>(pb => + { + pb.Add(a => a.Value, selectedEnumValues); + }); + Assert.Contains("form-check-input", cut.Markup); + } + + [Fact] + public async Task IntValue_Ok() + { + var ret = new List(); + var selectedIntValues = new List { 1, 2 }; + var cut = Context.RenderComponent>>(pb => + { + pb.Add(a => a.Value, selectedIntValues); + pb.Add(a => a.Items, new List() + { + new("1", "Test 1"), + new("2", "Test 2") + }); + pb.Add(a => a.OnSelectedChanged, (v1, v2) => + { + ret.AddRange(v2); + return Task.CompletedTask; + }); + }); + var item = cut.FindComponent>(); + await cut.InvokeAsync(item.Instance.OnToggleClick); + + // 选中 2 + Assert.Equal(2, ret.First()); + } + + [Fact] + public void NotSupportedException_Error() + { + Assert.Throws(() => Context.RenderComponent>>()); + Assert.Throws(() => Context.RenderComponent>()); + } + + [Fact] + public void FormatValue_Ok() + { + var cut = Context.RenderComponent(); + cut.InvokeAsync(() => + { + Assert.Null(cut.Instance.NullValueTest()); + Assert.NotNull(cut.Instance.NotNullValueTest()); + }); + } + + [Fact] + public void FormatGenericValue_Ok() + { + var cut = Context.RenderComponent(); + cut.InvokeAsync(() => + { + Assert.Equal(string.Empty, cut.Instance.NullValueTest()); + Assert.Equal("test", cut.Instance.NotNullValueTest()); + }); + } + + [Fact] + public void IsButton_Ok() + { + var cut = Context.RenderComponent>>(pb => + { + pb.Add(a => a.IsButton, true); + pb.Add(a => a.Color, Color.Danger); + pb.Add(a => a.Items, new List() + { + new("1", "Test 1"), + new("2", "Test 2") + }); + }); + cut.InvokeAsync(() => + { + var item = cut.Find(".btn"); + item.Click(); + cut.Contains("btn active bg-danger"); + }); + } + + [Fact] + public async Task OnMaxSelectedCountExceed_Ok() + { + bool max = false; + var items = new List() + { + new("1", "Test 1"), + new("2", "Test 2"), + new("3", "Test 3") + }; + var cut = Context.RenderComponent>(pb => + { + pb.Add(a => a.MaxSelectedCount, 2); + pb.Add(a => a.Items, items); + pb.Add(a => a.OnMaxSelectedCountExceed, () => + { + max = true; + return Task.CompletedTask; + }); + }); + var checkboxes = cut.FindComponents>(); + Assert.Equal(3, checkboxes.Count); + + await cut.InvokeAsync(async () => + { + await checkboxes[0].Instance.OnToggleClick(); + }); + Assert.Equal(CheckboxState.Checked, checkboxes[0].Instance.State); + + await cut.InvokeAsync(async () => + { + await checkboxes[1].Instance.OnToggleClick(); + }); + Assert.Equal(CheckboxState.Checked, checkboxes[1].Instance.State); + + // 选中第三个由于限制无法选中 + await cut.InvokeAsync(async () => + { + await checkboxes[2].Instance.OnToggleClick(); + }); + Assert.Equal(CheckboxState.Checked, checkboxes[0].Instance.State); + Assert.Equal(CheckboxState.Checked, checkboxes[1].Instance.State); + Assert.Equal(CheckboxState.UnChecked, checkboxes[2].Instance.State); + Assert.True(max); + + // 取消选择第一个 + max = false; + await cut.InvokeAsync(async () => + { + await checkboxes[0].Instance.OnToggleClick(); + }); + Assert.Equal(CheckboxState.UnChecked, checkboxes[0].Instance.State); + Assert.Equal(CheckboxState.Checked, checkboxes[1].Instance.State); + Assert.Equal(CheckboxState.UnChecked, checkboxes[2].Instance.State); + Assert.False(max); + } + + private class CheckboxListGenericMock + { + + } + + private class FormatValueTestCheckboxList : CheckboxList + { + public string? NullValueTest() => base.FormatValueAsString(null); + + public string? NotNullValueTest() => base.FormatValueAsString("test"); + } + + private class FormatValueTestGenericCheckboxList : CheckboxList?> + { + public string? NullValueTest() => base.FormatValueAsString(null); + + public string? NotNullValueTest() + { + Items = new List() { new("test", "test") { Active = true } }; + return base.FormatValueAsString(new List() { "test" }); + } + } + + private class Dummy + { + public List? Data { get; set; } + } +} From 6d8878e6c47c9b85a16844a8b30b7e906968142a Mon Sep 17 00:00:00 2001 From: Argo-AsicoTech Date: Sat, 21 Dec 2024 10:34:40 +0800 Subject: [PATCH 6/7] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=20CheckboxListGe?= =?UTF-8?q?neric=20=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Checkbox/CheckboxListGeneric.razor.cs | 61 ++++++++++++------- 1 file changed, 38 insertions(+), 23 deletions(-) diff --git a/src/BootstrapBlazor/Components/Checkbox/CheckboxListGeneric.razor.cs b/src/BootstrapBlazor/Components/Checkbox/CheckboxListGeneric.razor.cs index e1590fc3585..6719599f0c5 100644 --- a/src/BootstrapBlazor/Components/Checkbox/CheckboxListGeneric.razor.cs +++ b/src/BootstrapBlazor/Components/Checkbox/CheckboxListGeneric.razor.cs @@ -4,7 +4,6 @@ // Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone using Microsoft.Extensions.Localization; -using System.Collections; using System.Reflection; namespace BootstrapBlazor.Components; @@ -12,7 +11,7 @@ namespace BootstrapBlazor.Components; /// /// CheckboxList 组件基类 /// -public partial class CheckboxListGeneric +public partial class CheckboxListGeneric : IModelEqualityComparer { /// /// 获得 组件样式 @@ -40,9 +39,23 @@ public partial class CheckboxListGeneric .Build(); private string? GetButtonItemClassString(SelectedItem item) => CssBuilder.Default("btn") - .AddClass($"active bg-{Color.ToDescriptionString()}", item.Value != null && (Value?.Contains(item.Value) ?? false)) + .AddClass($"active bg-{Color.ToDescriptionString()}", IsEquals(item.Value)) .Build(); + /// + /// 获得/设置 数据主键标识标签 默认为
用于判断数据主键标签,如果模型未设置主键时可使用 参数自定义判断
数据模型支持联合主键 + ///
+ [Parameter] + [NotNull] + public Type? CustomKeyAttribute { get; set; } = typeof(KeyAttribute); + + /// + /// 获得/设置 比较数据是否相同回调方法 默认为 null + /// 提供此回调方法时忽略 属性 + /// + [Parameter] + public Func? ModelEqualityComparer { get; set; } + /// /// 获得/设置 数据源 /// @@ -150,28 +163,23 @@ protected override void OnParametersSet() Color = Color.Primary; } - if (Items == null) - { - var t = typeof(TValue); - var innerType = t.GetGenericArguments().FirstOrDefault(); - if (innerType != null) - { - Items = innerType.ToSelectList(); - } - Items ??= []; - } + Items ??= []; _onBeforeStateChangedCallback = MaxSelectedCount > 0 ? new Func>(OnBeforeStateChanged) : null; // set item active if (Value != null) { - foreach (var item in Items) + var item = Items.FirstOrDefault(i => IsEquals(i.Value)); + if (item != null) { - item.Active = Value.Contains(item.Value); + item.Active = true; } } } + + private bool IsEquals(TValue? val) => Value != null && Value.Find(v => Equals(v, val)) != null; + private async Task OnBeforeStateChanged(CheckboxState state) { var ret = true; @@ -195,23 +203,21 @@ private async Task OnBeforeStateChanged(CheckboxState state) /// private async Task OnStateChanged(SelectedItem item, bool v) { - if (item.Value == null) - { - return; - } - - var vals = new List(); + item.Active = v; + var vals = new List(); if (Value != null) { vals.AddRange(Value); } - if (v) + + var val = vals.Find(i => IsEquals(item.Value)); + if (v && val == null) { vals.Add(item.Value); } else { - vals.Remove(item.Value); + vals.Remove(val); } CurrentValue = vals; @@ -220,10 +226,19 @@ private async Task OnStateChanged(SelectedItem item, bool v) { await OnSelectedChanged(Items, CurrentValue); } + else + { + StateHasChanged(); + } } /// /// 点击选择框方法 /// private Task OnClick(SelectedItem item) => OnStateChanged(item, !item.Active); + + /// + /// + /// + public bool Equals(TValue? x, TValue? y) => this.Equals(x, y); } From e8c76204a34eaf2b2fb1544f51fd114ad070b3f5 Mon Sep 17 00:00:00 2001 From: Argo-AsicoTech Date: Sat, 21 Dec 2024 10:34:47 +0800 Subject: [PATCH 7/7] =?UTF-8?q?test:=20=E5=A2=9E=E5=8A=A0=E5=8D=95?= =?UTF-8?q?=E5=85=83=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Components/CheckboxListGenericTest.cs | 248 ++++++------------ 1 file changed, 83 insertions(+), 165 deletions(-) diff --git a/test/UnitTest/Components/CheckboxListGenericTest.cs b/test/UnitTest/Components/CheckboxListGenericTest.cs index ac7cce8a88f..bf3cc5b70da 100644 --- a/test/UnitTest/Components/CheckboxListGenericTest.cs +++ b/test/UnitTest/Components/CheckboxListGenericTest.cs @@ -3,51 +3,53 @@ // See the LICENSE file in the project root for more information. // Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone +using Microsoft.EntityFrameworkCore.Metadata.Internal; using Microsoft.Extensions.Localization; +using System.ComponentModel.DataAnnotations; namespace UnitTest.Components; public class CheckboxListGenericTest : BootstrapBlazorTestBase { - private IStringLocalizer Localizer { get; } - - public CheckboxListGenericTest() - { - Localizer = Context.Services.GetRequiredService>(); - } - [Fact] public void EditorForm_Ok() { - var dummy = new Dummy(); + var dummy = new Dummy() { Data = [new() { Id = 2, Name = "Test2" }] }; + var items = new List>() + { + new(new Foo() { Id = 1, Name = "Test1" }, "Test 1"), + new(new Foo() { Id = 2, Name = "Test2" }, "Test 2") + }; var cut = Context.RenderComponent(builder => { builder.Add(a => a.Model, dummy); - builder.AddChildContent>>(pb => + builder.AddChildContent>(pb => { - pb.Add(a => a.Items, Foo.GenerateHobbies(Localizer)); + pb.Add(a => a.Items, items); pb.Add(a => a.Value, dummy.Data); pb.Add(a => a.ValueExpression, Utility.GenerateValueExpression(dummy, nameof(dummy.Data), typeof(List))); }); }); // 断言生成 CheckboxList Assert.Contains("form-check is-label", cut.Markup); - cut.Contains("是/否"); // 提交表单触发客户端验证 var form = cut.Find("form"); form.Submit(); - Assert.Contains("is-invalid", cut.Markup); + Assert.Contains("is-valid", cut.Markup); } [Fact] public void ShowBorder_Ok() { - var foo = Foo.Generate(Localizer); - var cut = Context.RenderComponent>>(pb => + var items = new List>() + { + new(new Foo() { Id = 1, Name = "Test1" }, "Test 1"), + new(new Foo() { Id = 2, Name = "Test2" }, "Test 2") + }; + var cut = Context.RenderComponent>(pb => { - pb.Add(a => a.Items, Foo.GenerateHobbies(Localizer)); - pb.Add(a => a.Value, foo.Hobby); + pb.Add(a => a.Items, items); }); Assert.DoesNotContain("no-border", cut.Markup); @@ -61,36 +63,60 @@ public void ShowBorder_Ok() [Fact] public void IsVertical_Ok() { - var cut = Context.RenderComponent>>(); + var cut = Context.RenderComponent>(); Assert.DoesNotContain("is-vertical", cut.Markup); cut.SetParametersAndRender(pb => { pb.Add(a => a.IsVertical, true); + pb.Add(a => a.CustomKeyAttribute, typeof(KeyAttribute)); + pb.Add(a => a.ModelEqualityComparer, new Func((x, y) => x.Id == y.Id)); }); Assert.Contains("is-vertical", cut.Markup); } + [Fact] + public async Task NullItem_Ok() + { + var items = new List>() + { + new(null, "Select ..."), + new(new Foo() { Id = 2, Name = "Test2" }, "Test 2") + }; + var cut = Context.RenderComponent>(pb => + { + pb.Add(a => a.Items, items); + }); + cut.Contains("Select ..."); + + var checkboxes = cut.FindComponents>(); + await cut.InvokeAsync(async () => + { + await checkboxes[0].Instance.OnToggleClick(); + }); + Assert.Null(cut.Instance.Value[0]); + } + [Fact] public void IsDisabled_Ok() { - var cut = Context.RenderComponent>>(pb => + var items = new List>() { - pb.Add(a => a.Items, new List() - { - new() { Text = "Item 1", Value = "1" }, - new() { Text = "Item 2", Value = "2" , IsDisabled = true }, - new() { Text = "Item 3", Value = "3" }, - }); + new(new Foo() { Id = 1, Name = "Test1" }, "Test 1"), + new(new Foo() { Id = 2, Name = "Test2" }, "Test 2") { IsDisabled = true } + }; + var cut = Context.RenderComponent>(pb => + { + pb.Add(a => a.Items, items); }); cut.Contains("form-check is-label disabled"); cut.SetParametersAndRender(pb => { - pb.Add(a => a.Items, new List() + pb.Add(a => a.Items, new List>() { - new() { Text = "Item 1", Value = "1" }, - new() { Text = "Item 2", Value = "2" } + new(new Foo() { Id = 1, Name = "Test1" }, "Test 1"), + new(new Foo() { Id = 2, Name = "Test2" }, "Test 2") }); pb.Add(a => a.IsDisabled, true); }); @@ -100,15 +126,20 @@ public void IsDisabled_Ok() [Fact] public void CheckboxItemClass_Ok() { - var cut = Context.RenderComponent>(builder => + var cut = Context.RenderComponent>(builder => { builder.Add(a => a.CheckboxItemClass, "test-item"); }); Assert.DoesNotContain("test-item", cut.Markup); + var items = new List>() + { + new(new Foo() { Id = 1, Name = "Test1" }, "Test 1"), + new(new Foo() { Id = 2, Name = "Test2" }, "Test 2") + }; cut.SetParametersAndRender(pb => { - pb.Add(a => a.Items, Foo.GenerateHobbies(Localizer)); + pb.Add(a => a.Items, items); }); Assert.Contains("test-item", cut.Markup); } @@ -116,19 +147,14 @@ public void CheckboxItemClass_Ok() [Fact] public async Task StringValue_Ok() { - var cut = Context.RenderComponent>(builder => + var items = new List>() { - builder.Add(a => a.Value, "1,2"); - }); - Assert.Contains("checkbox-list", cut.Markup); - - cut.SetParametersAndRender(pb => + new(new Foo() { Id = 1, Name = "Test1" }, "Test 1"), + new(new Foo() { Id = 2, Name = "Test2" }, "Test 2") + }; + var cut = Context.RenderComponent>(pb => { - pb.Add(a => a.Items, new List() - { - new("1", "Test 1"), - new("2", "Test 2") - }); + pb.Add(a => a.Items, items); }); Assert.Contains("checkbox-list", cut.Markup); @@ -148,124 +174,38 @@ public async Task StringValue_Ok() } [Fact] - public async Task OnSelectedChanged_Ok() + public async Task IsButton_Ok() { - var selected = false; - var foo = Foo.Generate(Localizer); - var cut = Context.RenderComponent>>(pb => + var items = new List>() { - pb.Add(a => a.Items, Foo.GenerateHobbies(Localizer)); - pb.Add(a => a.Value, foo.Hobby); - pb.Add(a => a.OnSelectedChanged, (v1, v2) => - { - selected = true; - return Task.CompletedTask; - }); - }); - - var item = cut.FindComponent>(); - await cut.InvokeAsync(item.Instance.OnToggleClick); - Assert.True(selected); - } - - [Fact] - public void EnumValue_Ok() - { - var selectedEnumValues = new List { EnumEducation.Middle, EnumEducation.Primary }; - var cut = Context.RenderComponent>>(pb => - { - pb.Add(a => a.Value, selectedEnumValues); - }); - Assert.Contains("form-check-input", cut.Markup); - } - - [Fact] - public async Task IntValue_Ok() - { - var ret = new List(); - var selectedIntValues = new List { 1, 2 }; - var cut = Context.RenderComponent>>(pb => - { - pb.Add(a => a.Value, selectedIntValues); - pb.Add(a => a.Items, new List() - { - new("1", "Test 1"), - new("2", "Test 2") - }); - pb.Add(a => a.OnSelectedChanged, (v1, v2) => - { - ret.AddRange(v2); - return Task.CompletedTask; - }); - }); - var item = cut.FindComponent>(); - await cut.InvokeAsync(item.Instance.OnToggleClick); - - // 选中 2 - Assert.Equal(2, ret.First()); - } - - [Fact] - public void NotSupportedException_Error() - { - Assert.Throws(() => Context.RenderComponent>>()); - Assert.Throws(() => Context.RenderComponent>()); - } - - [Fact] - public void FormatValue_Ok() - { - var cut = Context.RenderComponent(); - cut.InvokeAsync(() => - { - Assert.Null(cut.Instance.NullValueTest()); - Assert.NotNull(cut.Instance.NotNullValueTest()); - }); - } - - [Fact] - public void FormatGenericValue_Ok() - { - var cut = Context.RenderComponent(); - cut.InvokeAsync(() => - { - Assert.Equal(string.Empty, cut.Instance.NullValueTest()); - Assert.Equal("test", cut.Instance.NotNullValueTest()); - }); - } - - [Fact] - public void IsButton_Ok() - { - var cut = Context.RenderComponent>>(pb => + new(new Foo() { Id = 1, Name = "Test1" }, "Test 1"), + new(new Foo() { Id = 2, Name = "Test2" }, "Test 2") + }; + var cut = Context.RenderComponent>(pb => { pb.Add(a => a.IsButton, true); - pb.Add(a => a.Color, Color.Danger); - pb.Add(a => a.Items, new List() - { - new("1", "Test 1"), - new("2", "Test 2") - }); + pb.Add(a => a.Color, Color.None); + pb.Add(a => a.Items, items); }); - cut.InvokeAsync(() => + var item = cut.Find(".btn"); + await cut.InvokeAsync(() => { - var item = cut.Find(".btn"); item.Click(); - cut.Contains("btn active bg-danger"); }); + cut.Contains("btn active bg-primary"); } [Fact] public async Task OnMaxSelectedCountExceed_Ok() { bool max = false; - var items = new List() + var items = new List>() { - new("1", "Test 1"), - new("2", "Test 2"), - new("3", "Test 3") + new(new Foo() { Id = 1, Name = "Test1" }, "Test 1"), + new(new Foo() { Id = 2, Name = "Test2" }, "Test 2"), + new(new Foo() { Id = 3, Name = "Test3" }, "Test 3") }; - var cut = Context.RenderComponent>(pb => + var cut = Context.RenderComponent>(pb => { pb.Add(a => a.MaxSelectedCount, 2); pb.Add(a => a.Items, items); @@ -312,31 +252,9 @@ await cut.InvokeAsync(async () => Assert.False(max); } - private class CheckboxListGenericMock - { - - } - - private class FormatValueTestCheckboxList : CheckboxList - { - public string? NullValueTest() => base.FormatValueAsString(null); - - public string? NotNullValueTest() => base.FormatValueAsString("test"); - } - - private class FormatValueTestGenericCheckboxList : CheckboxList?> - { - public string? NullValueTest() => base.FormatValueAsString(null); - - public string? NotNullValueTest() - { - Items = new List() { new("test", "test") { Active = true } }; - return base.FormatValueAsString(new List() { "test" }); - } - } - private class Dummy { + [Required] public List? Data { get; set; } } }