Repository: dotnetcore/BootstrapBlazor (2026-04-20T06:17:18Z)

Issue:
feat(DynamicElement): add OnTouchStart/End parameter
### Which class is this unit test associated with?

revert implement IDisposeAsync interface


Discussion:
@ArgoZhang Thank you for reporting. We will give triage later.



Patch:
diff --git a/src/BootstrapBlazor/BootstrapBlazor.csproj b/src/BootstrapBlazor/BootstrapBlazor.csproj
index 21fa52f951c..a5840be4ac0 100644
--- a/src/BootstrapBlazor/BootstrapBlazor.csproj
+++ b/src/BootstrapBlazor/BootstrapBlazor.csproj
@@ -1,7 +1,7 @@
 ﻿<Project Sdk="Microsoft.NET.Sdk.Razor">
 
   <PropertyGroup>
-    <Version>10.5.1-beta08</Version>
+    <Version>10.5.1-beta10</Version>
   </PropertyGroup>
 
   <ItemGroup>
diff --git a/src/BootstrapBlazor/Components/BaseComponents/DynamicElement.cs b/src/BootstrapBlazor/Components/BaseComponents/DynamicElement.cs
index 9418053363c..f727958de1d 100644
--- a/src/BootstrapBlazor/Components/BaseComponents/DynamicElement.cs
+++ b/src/BootstrapBlazor/Components/BaseComponents/DynamicElement.cs
@@ -12,7 +12,7 @@ namespace BootstrapBlazor.Components;
 /// <para lang="zh">动态元素组件</para>
 /// <para lang="en">Dynamic element component</para>
 /// </summary>
-public class DynamicElement : BootstrapComponentBase, IAsyncDisposable
+public class DynamicElement : BootstrapComponentBase
 {
     /// <summary>
     /// <para lang="zh">获得/设置 TagName 属性 默认为 div</para>
@@ -78,6 +78,34 @@ public class DynamicElement : BootstrapComponentBase, IAsyncDisposable
     [Parameter]
     public bool TriggerContextMenu { get; set; }
 
+    /// <summary>
+    /// <para lang="zh">获得/设置 OnTouchStart 回调委托</para>
+    /// <para lang="en">Gets or sets the OnTouchStart callback delegate</para>
+    /// </summary>
+    [Parameter]
+    public Func<TouchEventArgs, Task>? OnTouchStart { get; set; }
+
+    /// <summary>
+    /// <para lang="zh">获得/设置 OnTouchEnd 回调委托</para>
+    /// <para lang="en">Gets or sets the OnTouchEnd callback delegate</para>
+    /// </summary>
+    [Parameter]
+    public Func<TouchEventArgs, Task>? OnTouchEnd { get; set; }
+
+    /// <summary>
+    /// <para lang="zh">获得/设置 是否触发 OnTouchStart 事件 默认 false</para>
+    /// <para lang="en">Gets or sets whether to trigger OnTouchStart events. Default is false</para>
+    /// </summary>
+    [Parameter]
+    public bool TriggerTouchStart { get; set; }
+
+    /// <summary>
+    /// <para lang="zh">获得/设置 是否触发 OnTouchEnd 事件 默认 false</para>
+    /// <para lang="en">Gets or sets whether to trigger OnTouchEnd events. Default is false</para>
+    /// </summary>
+    [Parameter]
+    public bool TriggerTouchEnd { get; set; }
+
     /// <summary>
     /// <para lang="zh">获得/设置 内容组件</para>
     /// <para lang="en">Gets or sets the child content</para>
@@ -139,6 +167,16 @@ protected override void BuildRenderTree(RenderTreeBuilder builder)
             builder.AddEventPreventDefaultAttribute(9, "oncontextmenu", true);
         }
 
+        if (IsTriggerTouchStart())
+        {
+            builder.AddAttribute(11, "ontouchstart", EventCallback.Factory.Create<TouchEventArgs>(this, OnTriggerTouchStart));
+        }
+
+        if (IsTriggerTouchEnd())
+        {
+            builder.AddAttribute(12, "ontouchend", EventCallback.Factory.Create<TouchEventArgs>(this, OnTriggerTouchEnd));
+        }
+
         builder.AddContent(10, ChildContent);
 
         if (GenerateElement || IsTriggerClick() || IsTriggerDoubleClick())
@@ -153,6 +191,10 @@ protected override void BuildRenderTree(RenderTreeBuilder builder)
 
     private bool IsTriggerContextMenu() => TriggerContextMenu && OnContextMenu != null;
 
+    private bool IsTriggerTouchStart() => TriggerTouchStart && OnTouchStart != null;
+
+    private bool IsTriggerTouchEnd() => TriggerTouchEnd && OnTouchEnd != null;
+
     private async Task OnTriggerClick()
     {
         if (OnClick != null)
@@ -177,28 +219,19 @@ private async Task OnTriggerContextMenu(MouseEventArgs e)
         }
     }
 
-    /// <summary>
-    /// <para lang="zh">异步释放资源</para>
-    /// <para lang="en">Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources asynchronously</para>
-    /// </summary>
-    /// <param name="disposing"></param>
-    protected virtual async ValueTask DisposeAsync(bool disposing)
+    private async Task OnTriggerTouchStart(TouchEventArgs e)
     {
-        if (disposing)
+        if (OnTouchStart != null)
         {
-            OnClick = null;
-            OnDoubleClick = null;
-            OnContextMenu = null;
-            ChildContent = null;
+            await OnTouchStart(e);
         }
     }
 
-    /// <summary>
-    /// <inheritdoc/>
-    /// </summary>
-    public async ValueTask DisposeAsync()
+    private async Task OnTriggerTouchEnd(TouchEventArgs e)
     {
-        await DisposeAsync(true);
-        GC.SuppressFinalize(this);
+        if (OnTouchEnd != null)
+        {
+            await OnTouchEnd(e);
+        }
     }
 }
diff --git a/src/BootstrapBlazor/Components/Button/ButtonBase.cs b/src/BootstrapBlazor/Components/Button/ButtonBase.cs
index 3aaadb0ae91..edbdaa36d1a 100644
--- a/src/BootstrapBlazor/Components/Button/ButtonBase.cs
+++ b/src/BootstrapBlazor/Components/Button/ButtonBase.cs
@@ -334,16 +334,6 @@ protected override async ValueTask DisposeAsync(bool disposing)
     {
         if (disposing)
         {
-            if (OnClick.HasDelegate)
-            {
-                OnClick = EventCallback<MouseEventArgs>.Empty;
-            }
-
-            if (OnClickWithoutRender != null)
-            {
-                OnClickWithoutRender = null;
-            }
-
             if (IsAsync && ValidateForm != null)
             {
                 ValidateForm.UnregisterAsyncSubmitButton(this);
diff --git a/src/BootstrapBlazor/Components/Select/Select.razor b/src/BootstrapBlazor/Components/Select/Select.razor
index ff5d5ec8c4b..47a5358528d 100644
--- a/src/BootstrapBlazor/Components/Select/Select.razor
+++ b/src/BootstrapBlazor/Components/Select/Select.razor
@@ -1,4 +1,4 @@
-@namespace BootstrapBlazor.Components
+﻿@namespace BootstrapBlazor.Components
 @using Microsoft.AspNetCore.Components.Web.Virtualization
 @typeparam TValue
 @inherits SimpleSelectBase<TValue>
@@ -22,8 +22,7 @@
             }
             else
             {
-                <input type="text" id="@InputId" disabled="@Disabled" placeholder="@PlaceHolder" class="@InputClassString"
-                       value="@SelectedRow?.Text" @onchange="@_onChangeEventCallback" readonly="@ReadonlyString" />
+                <input type="text" id="@InputId" disabled="@Disabled" placeholder="@PlaceHolder" class="@InputClassString" value="@SelectedRow?.Text" @onchange="OnChange" readonly="@ReadonlyString" />
             }
             <span class="@AppendClassString"><i class="@DropdownIcon"></i></span>
         </div>
@@ -93,7 +92,7 @@
 
 @code {
     RenderFragment<SelectedItem> RenderRow => item =>
-    @<DynamicElement class="@ActiveItem(item)" OnClick="() => OnClickItem(item)">
+    @<div class="@ActiveItem(item)" @onclick="() => OnClickItem(item)">
         @if (ItemTemplate != null)
         {
             @ItemTemplate(item)
@@ -106,7 +105,7 @@
         {
             @item.Text
         }
-    </DynamicElement>;
+    </div>;
 
     RenderFragment<PlaceholderContext> RenderPlaceHolderRow => context =>
     @<div class="dropdown-item">
diff --git a/src/BootstrapBlazor/Components/Select/Select.razor.cs b/src/BootstrapBlazor/Components/Select/Select.razor.cs
index 9461ff45095..a9707b4f88b 100644
--- a/src/BootstrapBlazor/Components/Select/Select.razor.cs
+++ b/src/BootstrapBlazor/Components/Select/Select.razor.cs
@@ -202,8 +202,6 @@ public bool IsUseActiveWhenValueIsNull
 
     private string _defaultVirtualizedItemText = "";
 
-    private EventCallback<ChangeEventArgs> _onChangeEventCallback = EventCallback<ChangeEventArgs>.Empty;
-
     private SelectedItem? SelectedItem { get; set; }
 
     private SelectedItem? SelectedRow
@@ -236,10 +234,6 @@ protected override void OnParametersSet()
         NoSearchDataText ??= Localizer[nameof(NoSearchDataText)];
         DropdownIcon ??= IconTheme.GetIconByKey(ComponentIcons.SelectDropdownIcon);
         ClearIcon ??= IconTheme.GetIconByKey(ComponentIcons.SelectClearIcon);
-
-        _onChangeEventCallback = IsEditable
-            ? EventCallback.Factory.Create<ChangeEventArgs>(this, OnChange)
-            : EventCallback<ChangeEventArgs>.Empty;
     }
 
     /// <summary>
@@ -395,8 +389,6 @@ private async Task OnClickItem(SelectedItem item)
             _defaultVirtualizedItemText = item.Text;
             await SelectedItemChanged(item);
         }
-
-        StateHasChanged();
     }
 
     private async Task SelectedItemChanged(SelectedItem item)
@@ -509,19 +501,4 @@ private async Task OnChange(ChangeEventArgs args)
             ?? allItems.Find(i => !i.IsDisabled);
         return item;
     }
-
-    /// <summary>
-    /// <inheritdoc/>
-    /// </summary>
-    /// <param name="disposing"></param>
-    /// <returns></returns>
-    protected override ValueTask DisposeAsync(bool disposing)
-    {
-        if (disposing)
-        {
-            _onChangeEventCallback = EventCallback<ChangeEventArgs>.Empty;
-        }
-
-        return base.DisposeAsync(disposing);
-    }
 }
diff --git a/src/BootstrapBlazor/Components/Table/Table.razor b/src/BootstrapBlazor/Components/Table/Table.razor
index 04ba21e6809..3858bf4b40a 100644
--- a/src/BootstrapBlazor/Components/Table/Table.razor
+++ b/src/BootstrapBlazor/Components/Table/Table.razor
@@ -270,8 +270,8 @@
                                             class="@GetRowClassString(item, "table-row")" style="@GetRowStyleString(item)"
                                             TriggerContextMenu="ContextMenuZone != null"
                                             OnContextMenu="e => OnContextMenu(e, item)"
-                                            @ontouchstart="e => OnTouchStart(e, item)"
-                                            @ontouchend="OnTouchEnd"
+                                            TriggerTouchStart="true" OnTouchStart="e => OnTouchStart(e, item)"
+                                            TriggerTouchEnd="true" OnTouchEnd="OnTouchEnd"
                                             TriggerClick="@(ClickToSelect || OnClickRowCallback != null)"
                                             OnClick="() => ClickRow(item)">
                                 @if (IsMultipleSelect)
@@ -734,7 +734,8 @@
         @<DynamicElement @key="GetKeyByITem(item)" TagName="tr"
                          class="@GetRowClassString(item)" style="@GetRowStyleString(item)" 
                          TriggerContextMenu="ContextMenuZone != null" OnContextMenu="e => OnContextMenu(e, item)"
-                         @ontouchstart="e => OnTouchStart(e, item)" @ontouchend="OnTouchEnd"
+                         TriggerTouchStart="true" OnTouchStart="e => OnTouchStart(e, item)"
+                         TriggerTouchEnd="true" OnTouchEnd="OnTouchEnd"
                          TriggerClick="@(ClickToSelect || OnClickRowCallback != null)" OnClick="() => ClickRow(item)"
                          TriggerDoubleClick="@(DoubleClickToEdit || OnDoubleClickRowCallback != null)"
                          OnDoubleClick="() => DoubleClickRow(item)">
diff --git a/src/BootstrapBlazor/Components/Table/Table.razor.cs b/src/BootstrapBlazor/Components/Table/Table.razor.cs
index 4f3d0eeb227..78105425c3c 100644
--- a/src/BootstrapBlazor/Components/Table/Table.razor.cs
+++ b/src/BootstrapBlazor/Components/Table/Table.razor.cs
@@ -2000,9 +2000,10 @@ private async Task OnTouchStart(TouchEventArgs e, TItem item)
         }
     }
 
-    private void OnTouchEnd()
+    private Task OnTouchEnd(TouchEventArgs e)
     {
         TouchStart = false;
+        return Task.CompletedTask;
     }
 
     private object? GetKeyByITem(TItem item) => SortableList != null ? item : null; //OnGetRowKey?.Invoke(item);


Test patch:
diff --git a/test/UnitTest/Components/DynamicElementTest.cs b/test/UnitTest/Components/DynamicElementTest.cs
index e088fd62888..556105bd90a 100644
--- a/test/UnitTest/Components/DynamicElementTest.cs
+++ b/test/UnitTest/Components/DynamicElementTest.cs
@@ -3,6 +3,8 @@
 // See the LICENSE file in the project root for more information.
 // Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone
 
+using Microsoft.AspNetCore.Components.Web;
+
 namespace UnitTest.Components;
 
 public class DynamicElementTest
@@ -18,4 +20,45 @@ public void Key_OK()
 
         Assert.Equal("<div></div>", cut.Markup);
     }
+
+    [Fact]
+    public void TouchEvents_Ok()
+    {
+        var context = new BunitContext();
+        TouchEventArgs? touchStartArgs = null;
+        TouchEventArgs? touchEndArgs = null;
+        var touchStartEventArgs = new TouchEventArgs
+        {
+            Touches = [new TouchPoint { ClientX = 10, ClientY = 20, ScreenX = 30, ScreenY = 40 }]
+        };
+        var touchEndEventArgs = new TouchEventArgs
+        {
+            ChangedTouches = [new TouchPoint { ClientX = 11, ClientY = 21, ScreenX = 31, ScreenY = 41 }]
+        };
+
+        var cut = context.Render<DynamicElement>(pb =>
+        {
+            pb.Add(a => a.TagName, "span");
+            pb.Add(a => a.TriggerTouchStart, true);
+            pb.Add(a => a.OnTouchStart, e =>
+            {
+                touchStartArgs = e;
+                return Task.CompletedTask;
+            });
+            pb.Add(a => a.TriggerTouchEnd, true);
+            pb.Add(a => a.OnTouchEnd, e =>
+            {
+                touchEndArgs = e;
+                return Task.CompletedTask;
+            });
+            pb.AddChildContent("Touch");
+        });
+
+        var element = cut.Find("span");
+        element.TriggerEvent("ontouchstart", touchStartEventArgs);
+        element.TriggerEvent("ontouchend", touchEndEventArgs);
+
+        Assert.Same(touchStartEventArgs, touchStartArgs);
+        Assert.Same(touchEndEventArgs, touchEndArgs);
+    }
 }