Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/BootstrapBlazor/BootstrapBlazor.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Razor">

<PropertyGroup>
<Version>10.1.5-beta04</Version>
<Version>10.1.5-beta05</Version>
</PropertyGroup>

<ItemGroup>
Expand Down
6 changes: 1 addition & 5 deletions src/BootstrapBlazor/Components/Upload/AvatarUpload.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -187,14 +187,10 @@ public override async Task ToggleMessage(IReadOnlyCollection<ValidationResult> r

ValidateModule ??= await LoadValidateModule();

var invalidItems = IsInValidOnAddItem
? [new { Id = AddId, _results.First().ErrorMessage }]
: _results.Select(i => new { Id = i.MemberNames.FirstOrDefault(), i.ErrorMessage }).ToList();

var items = IsInValidOnAddItem
? [AddId]
: Files.Select(i => i.ValidateId).ToList();

var invalidItems = _results.GetInvalidItems(IsInValidOnAddItem, AddId);
var addId = IsInValidOnAddItem ? null : AddId;
await ValidateModule.InvokeVoidAsync("executeUpload", items, invalidItems, addId);
}
Expand Down
6 changes: 1 addition & 5 deletions src/BootstrapBlazor/Components/Upload/CardUpload.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -176,14 +176,10 @@ public override async Task ToggleMessage(IReadOnlyCollection<ValidationResult> r

ValidateModule ??= await LoadValidateModule();

var invalidItems = IsInValidOnAddItem
? [new { Id = AddId, _results.First().ErrorMessage }]
: _results.Select(i => new { Id = i.MemberNames.FirstOrDefault(), i.ErrorMessage }).ToList();

var items = IsInValidOnAddItem
? [AddId]
: Files.Select(i => i.ValidateId).ToList();

var invalidItems = _results.GetInvalidItems(IsInValidOnAddItem, AddId);
var addId = IsInValidOnAddItem ? null : AddId;
await ValidateModule.InvokeVoidAsync("executeUpload", items, invalidItems, addId);
}
Expand Down
4 changes: 2 additions & 2 deletions src/BootstrapBlazor/Components/Upload/FileListUploadBase.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
// Licensed to the .NET Foundation under one or more agreements.
// 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;

/// <summary>
/// PreviewListUploadBase 基类
/// FileListUploadBase 基类
/// </summary>
/// <typeparam name="TValue"></typeparam>
public class FileListUploadBase<TValue> : UploadBase<TValue>
Expand Down
7 changes: 7 additions & 0 deletions src/BootstrapBlazor/Components/Upload/UploadBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,11 @@ protected async Task OnFileChange(InputFileChangeEventArgs args)
await OnAllFileUploaded(items);
}

UpdateValue(items);
}

private void UpdateValue(List<UploadFile> items)
{
if (ValueType.IsAssignableTo(typeof(IEnumerable<UploadFile>)))
{
CurrentValue = (TValue)(object)items;
Expand Down Expand Up @@ -248,6 +253,8 @@ protected virtual async Task<bool> OnFileDelete(UploadFile item)
UploadFiles.Remove(item);
DefaultFileList?.Remove(item);
_filesCache = null;

UpdateValue(Files);
}
StateHasChanged();
return ret;
Expand Down
8 changes: 8 additions & 0 deletions src/BootstrapBlazor/Components/Upload/UploadValidateItem.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// 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;

readonly record struct UploadValidateItem(string? Id, string? ErrorMessage);
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

using Microsoft.AspNetCore.Components.Forms;
using Microsoft.Extensions.Localization;
using System.Collections;
using System.Collections.Concurrent;
using System.Linq.Expressions;
using System.Reflection;
Expand Down Expand Up @@ -508,7 +509,14 @@ private async Task ValidateAsync(IValidateComponent validator, ValidationContext
else
{
// 未选择文件
propertyValue = null;
if (propertyValue is string)
{

Copy link

Copilot AI Dec 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This empty code block appears to be intentional but lacks documentation. When the propertyValue is a string type and no files are uploaded, this block does nothing, which means the string value is validated as-is. Consider adding a comment explaining why string values don't need special handling in this case, or if this is incomplete logic that needs to be implemented.

Suggested change
// 字符串类型在未选择文件时无需特殊处理,直接按原值进行数据注解验证

Copilot uses AI. Check for mistakes.
}
else if (propertyValue is IEnumerable)
Comment on lines +512 to +516
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): The empty string branch introduces differing behavior for string vs collection properties and may leave stale values on "no file selected".

Previously, propertyValue was always set to null when no file was selected; now only IEnumerable values are cleared, while string values are left unchanged because the string branch is a no-op. This can cause stale string values to be revalidated when no file is chosen. To keep behavior consistent, consider also setting propertyValue to null (or another clear default) for the string case, or refactor so both types are handled in the same way.

{
propertyValue = null;
}
ValidateDataAnnotations(propertyValue, context, messages, pi);
}

Expand Down
6 changes: 5 additions & 1 deletion src/BootstrapBlazor/Extensions/ValidateContextExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Licensed to the .NET Foundation under one or more agreements.
// 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
Expand Down Expand Up @@ -42,4 +42,8 @@ public static ValidationResult GetValidationResult(this ValidationContext contex
var memberNames = string.IsNullOrEmpty(context.MemberName) ? null : new string[] { context.MemberName };
return new ValidationResult(errorMessage, memberNames);
}

internal static List<UploadValidateItem> GetInvalidItems(this IReadOnlyCollection<ValidationResult> source, bool isInValidOnAddItem, string? newId) => isInValidOnAddItem
? [new UploadValidateItem() { Id = newId, ErrorMessage = source.First().ErrorMessage }]
: source.Select(i => new UploadValidateItem() { Id = i.MemberNames.FirstOrDefault(), ErrorMessage = i.ErrorMessage }).ToList();
Comment on lines +47 to +48
Copy link

Copilot AI Dec 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The object initializer syntax used here is inconsistent with the positional constructor defined in the record struct. The record struct UploadValidateItem(string? Id, string? ErrorMessage) uses positional parameters, so instances should be created using the constructor: new UploadValidateItem(newId, source.First().ErrorMessage) instead of using object initializer syntax. The same applies to the second case where it should be new UploadValidateItem(i.MemberNames.FirstOrDefault(), i.ErrorMessage).

Suggested change
? [new UploadValidateItem() { Id = newId, ErrorMessage = source.First().ErrorMessage }]
: source.Select(i => new UploadValidateItem() { Id = i.MemberNames.FirstOrDefault(), ErrorMessage = i.ErrorMessage }).ToList();
? [new UploadValidateItem(newId, source.First().ErrorMessage)]
: source.Select(i => new UploadValidateItem(i.MemberNames.FirstOrDefault(), i.ErrorMessage)).ToList();

Copilot uses AI. Check for mistakes.
}
20 changes: 20 additions & 0 deletions test/UnitTest/Components/UploadAvatarTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,26 @@ await cut.InvokeAsync(() => input.Instance.OnChange.InvokeAsync(new InputFileCha
await cut.InvokeAsync(() => items[1].Click());
}

[Fact]
public void UploadValidateItem_Ok()
{
var type = Type.GetType("BootstrapBlazor.Components.UploadValidateItem, BootstrapBlazor");
Assert.NotNull(type);

var instance = Activator.CreateInstance(type, ["addId", "mock_ErrorMessage"]);
var propertyInfo = type.GetProperty("Id");
Assert.NotNull(propertyInfo);

var v = propertyInfo.GetValue(instance, null);
Assert.Equal("addId", v);

propertyInfo = type.GetProperty("ErrorMessage");
Assert.NotNull(propertyInfo);

v = propertyInfo.GetValue(instance, null);
Assert.Equal("mock_ErrorMessage", v);
}

private class Person
{
[Required]
Expand Down
70 changes: 64 additions & 6 deletions test/UnitTest/Components/UploadCardTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
// Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone

using Microsoft.AspNetCore.Components.Forms;
using System.ComponentModel.DataAnnotations;

namespace UnitTest.Components;

Expand Down Expand Up @@ -150,20 +151,77 @@ public void ShowFileSize_Ok()
cut.DoesNotContain("upload-item-file-size");
}

private class Dummy
{
[Required]
public List<UploadFile>? Files { get; set; }
}

[Fact]
public void CardUpload_ValidateForm_Ok()
public async Task CardUpload_ValidateForm_Ok()
{
var foo = new Foo();
var invalid = false;
var foo = new Dummy();
var cut = Context.Render<ValidateForm>(pb =>
{
pb.Add(a => a.Model, foo);
pb.AddChildContent<CardUpload<string>>(pb =>
pb.AddChildContent<CardUpload<List<UploadFile>>>(pb =>
{
pb.Add(a => a.Accept, "Image");
pb.Add(a => a.Value, foo.Files);
pb.Add(a => a.ValueChanged, EventCallback.Factory.Create<List<UploadFile>?>(this, v => foo.Files = v));
pb.Add(a => a.ValueExpression, Utility.GenerateValueExpression(foo, "Files", typeof(List<UploadFile>)));
pb.Add(a => a.AllowExtensions, [".jpg"]);
pb.Add(a => a.ShowDeleteButton, true);
});
pb.Add(a => a.OnValidSubmit, context =>
{
invalid = false;
return Task.CompletedTask;
});
pb.Add(a => a.OnInvalidSubmit, context =>
{
pb.Add(a => a.Value, foo.Name);
pb.Add(a => a.ValueExpression, foo.GenerateValueExpression());
invalid = true;
return Task.CompletedTask;
});
});
cut.Contains("form-label");

// 提交表单
var form = cut.Find("form");
await cut.InvokeAsync(() => form.Submit());
Assert.True(invalid);

var input = cut.FindComponent<InputFile>();
await cut.InvokeAsync(async () =>
{
await input.Instance.OnChange.InvokeAsync(new InputFileChangeEventArgs(new List<MockBrowserFile>()
{
new()
}));
form.Submit();
});
Assert.False(invalid);

// 设置 Disabled 取消校验
var upload = cut.FindComponent<CardUpload<List<UploadFile>>>();
upload.Render(pb =>
{
pb.Add(a => a.IsDisabled, true);
});

Assert.DoesNotContain("is-invalid", upload.Markup);

upload.Render(pb =>
{
pb.Add(a => a.IsDisabled, false);
});

var items = cut.FindAll(".btn-outline-danger");
Assert.Single(items);
await cut.InvokeAsync(() => items[0].Click());

form = cut.Find("form");
await cut.InvokeAsync(() => form.Submit());
Copy link

Copilot AI Dec 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test case is missing an assertion after submitting the form at line 224. After deleting the file (line 221) and submitting the form (line 224), the test should verify that the form is now invalid again since the Files field is marked as Required and should be empty after deletion. Consider adding an assertion like Assert.True(invalid) after line 224 to verify the validation behavior.

Copilot uses AI. Check for mistakes.
}

[Fact]
Expand Down
Loading