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
252 changes: 252 additions & 0 deletions src/BlazorWebFormsComponents.Test/AutoPostBackTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
using System;
using Bunit;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using Moq;
using Shouldly;
using Xunit;

using CheckBoxComponent = BlazorWebFormsComponents.CheckBox;
using TextBoxComponent = BlazorWebFormsComponents.TextBox;
using RadioButtonComponent = BlazorWebFormsComponents.RadioButton;

namespace BlazorWebFormsComponents.Test;

/// <summary>
/// Tests for AutoPostBack behavior — verifies that SSR mode with AutoPostBack=true
/// emits onchange="this.form.submit()" and that Interactive mode does not.
/// </summary>
public class AutoPostBackTests : IDisposable
{
private readonly BunitContext _ctx;

public AutoPostBackTests()
{
_ctx = new BunitContext();
_ctx.JSInterop.SetupVoid("bwfc.Page.OnAfterRender");
_ctx.Services.AddSingleton<LinkGenerator>(new Mock<LinkGenerator>().Object);
_ctx.Services.AddSingleton<IDataProtectionProvider>(new EphemeralDataProtectionProvider());
_ctx.Services.AddLogging();
}

public void Dispose() => _ctx.Dispose();

private void RegisterHttpContextWithMethod(string method)
{
var httpContext = new DefaultHttpContext();
httpContext.Request.Method = method;
var mock = new Mock<IHttpContextAccessor>();
mock.Setup(x => x.HttpContext).Returns(httpContext);
_ctx.Services.AddSingleton<IHttpContextAccessor>(mock.Object);
}

private void RegisterNoHttpContext()
{
var mock = new Mock<IHttpContextAccessor>();
HttpContext? noContext = null;
mock.Setup(x => x.HttpContext).Returns(noContext);
_ctx.Services.AddSingleton<IHttpContextAccessor>(mock.Object);
}

#region DropDownList

[Fact]
public void DropDownList_SSR_AutoPostBackTrue_EmitsOnchangeScript()
{
RegisterHttpContextWithMethod("GET");
var items = new ListItemCollection
{
new ListItem("One", "1"),
new ListItem("Two", "2")
};

var cut = _ctx.Render<DropDownList<object>>(parameters => parameters
.Add(p => p.StaticItems, items)
.Add(p => p.AutoPostBack, true));

cut.Find("select").GetAttribute("onchange").ShouldBe("this.form.submit()");
}

[Fact]
public void DropDownList_SSR_AutoPostBackFalse_NoOnchangeScript()
{
RegisterHttpContextWithMethod("GET");
var items = new ListItemCollection
{
new ListItem("One", "1"),
new ListItem("Two", "2")
};

var cut = _ctx.Render<DropDownList<object>>(parameters => parameters
.Add(p => p.StaticItems, items)
.Add(p => p.AutoPostBack, false));

cut.Find("select").GetAttribute("onchange").ShouldBeNull();
}

[Fact]
public void DropDownList_Interactive_AutoPostBackTrue_NoOnchangeScript()
{
RegisterNoHttpContext();
var items = new ListItemCollection
{
new ListItem("One", "1"),
new ListItem("Two", "2")
};

var cut = _ctx.Render<DropDownList<object>>(parameters => parameters
.Add(p => p.StaticItems, items)
.Add(p => p.AutoPostBack, true));

cut.Find("select").GetAttribute("onchange").ShouldBeNull();
}

#endregion

#region CheckBox

[Fact]
public void CheckBox_SSR_AutoPostBackTrue_EmitsOnchangeScript()
{
RegisterHttpContextWithMethod("GET");

var cut = _ctx.Render<CheckBoxComponent>(parameters => parameters
.Add(p => p.Text, "Accept")
.Add(p => p.AutoPostBack, true));

cut.Find("input[type='checkbox']").GetAttribute("onchange").ShouldBe("this.form.submit()");
}

[Fact]
public void CheckBox_SSR_AutoPostBackFalse_NoOnchangeScript()
{
RegisterHttpContextWithMethod("GET");

var cut = _ctx.Render<CheckBoxComponent>(parameters => parameters
.Add(p => p.Text, "Accept")
.Add(p => p.AutoPostBack, false));

cut.Find("input[type='checkbox']").GetAttribute("onchange").ShouldBeNull();
}

#endregion

#region TextBox

[Fact]
public void TextBox_SSR_AutoPostBackTrue_EmitsOnchangeScript()
{
RegisterHttpContextWithMethod("GET");

var cut = _ctx.Render<TextBoxComponent>(parameters => parameters
.Add(p => p.Text, "hello")
.Add(p => p.AutoPostBack, true));

cut.Find("input").GetAttribute("onchange").ShouldBe("this.form.submit()");
}

#endregion

#region RadioButton

[Fact]
public void RadioButton_SSR_AutoPostBackTrue_EmitsOnchangeScript()
{
RegisterHttpContextWithMethod("GET");

var cut = _ctx.Render<RadioButtonComponent>(parameters => parameters
.Add(p => p.Text, "Option A")
.Add(p => p.AutoPostBack, true));

cut.Find("input[type='radio']").GetAttribute("onchange").ShouldBe("this.form.submit()");
}

[Fact]
public void RadioButton_Interactive_AutoPostBackTrue_NoOnchangeScript()
{
RegisterNoHttpContext();

var cut = _ctx.Render<RadioButtonComponent>(parameters => parameters
.Add(p => p.Text, "Option A")
.Add(p => p.AutoPostBack, true));

cut.Find("input[type='radio']").GetAttribute("onchange").ShouldBeNull();
}

#endregion

#region ListBox

[Fact]
public void ListBox_SSR_AutoPostBackTrue_EmitsOnchangeScript()
{
RegisterHttpContextWithMethod("GET");
var items = new ListItemCollection
{
new ListItem("One", "1"),
new ListItem("Two", "2")
};

var cut = _ctx.Render<ListBox<object>>(parameters => parameters
.Add(p => p.StaticItems, items)
.Add(p => p.AutoPostBack, true));

cut.Find("select").GetAttribute("onchange").ShouldBe("this.form.submit()");
}

#endregion

#region CheckBoxList

[Fact]
public void CheckBoxList_SSR_AutoPostBackTrue_EmitsOnchangeScript()
{
RegisterHttpContextWithMethod("GET");
var items = new ListItemCollection
{
new ListItem("One", "1"),
new ListItem("Two", "2")
};

var cut = _ctx.Render<CheckBoxList<object>>(parameters => parameters
.Add(p => p.StaticItems, items)
.Add(p => p.AutoPostBack, true));

var checkboxes = cut.FindAll("input[type='checkbox']");
checkboxes.Count.ShouldBeGreaterThan(0);
foreach (var cb in checkboxes)
{
cb.GetAttribute("onchange").ShouldBe("this.form.submit()");
}
}

#endregion

#region RadioButtonList

[Fact]
public void RadioButtonList_SSR_AutoPostBackTrue_EmitsOnchangeScript()
{
RegisterHttpContextWithMethod("GET");
var items = new ListItemCollection
{
new ListItem("One", "1"),
new ListItem("Two", "2")
};

var cut = _ctx.Render<RadioButtonList<object>>(parameters => parameters
.Add(p => p.StaticItems, items)
.Add(p => p.AutoPostBack, true));

var radios = cut.FindAll("input[type='radio']");
radios.Count.ShouldBeGreaterThan(0);
foreach (var radio in radios)
{
radio.GetAttribute("onchange").ShouldBe("this.form.submit()");
}
}

#endregion
}
31 changes: 31 additions & 0 deletions src/BlazorWebFormsComponents/BaseWebFormsComponent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,37 @@
protected bool IsHttpContextAvailable
=> HttpContextAccessor?.HttpContext is not null;

/// <summary>
/// Returns the HTML onchange attribute value for AutoPostBack behavior.
/// In SSR mode with AutoPostBack=true, returns "this.form.submit()" to trigger a form POST.
/// In Interactive mode or when AutoPostBack=false, returns null (Blazor handles events natively).
/// </summary>
protected string? GetAutoPostBackScript(bool autoPostBack)

Check warning on line 199 in src/BlazorWebFormsComponents/BaseWebFormsComponent.cs

View workflow job for this annotation

GitHub Actions / build

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

Check warning on line 199 in src/BlazorWebFormsComponents/BaseWebFormsComponent.cs

View workflow job for this annotation

GitHub Actions / Analyze (csharp)

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

Check warning on line 199 in src/BlazorWebFormsComponents/BaseWebFormsComponent.cs

View workflow job for this annotation

GitHub Actions / Run Playwright Integration Tests

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
{
if (!autoPostBack) return null;
if (CurrentRenderMode == WebFormsRenderMode.StaticSSR)
return "this.form.submit()";
return null;
}

private static readonly IReadOnlyDictionary<string, object> _emptyAttributes =
new Dictionary<string, object>();

/// <summary>
/// Returns an attribute dictionary containing onchange="this.form.submit()" when
/// AutoPostBack is true in SSR mode. Returns an empty dictionary otherwise.
/// Used via @attributes splatting to avoid compile-time conflicts with @onchange.
/// </summary>
protected IReadOnlyDictionary<string, object> GetAutoPostBackAttributes(bool autoPostBack)
{
var script = GetAutoPostBackScript(autoPostBack);
if (script != null)
{
return new Dictionary<string, object> { ["onchange"] = script };
}
return _emptyAttributes;
}

/// <summary>
/// Optional data protection provider for ViewState encryption in SSR mode.
/// Resolved lazily from the service provider — null when not registered.
Expand Down
5 changes: 3 additions & 2 deletions src/BlazorWebFormsComponents/BulletedList.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,10 @@ public partial class BulletedList<ItemType> : BaseListControl<ItemType>
public EventCallback<EventArgs> TextChanged { get; set; }

/// <summary>
/// Gets or sets whether the control automatically posts back when selection changes. Migration stub.
/// Gets or sets whether the control automatically posts back when selection changes.
/// BulletedList is a display-only control that does not support AutoPostBack.
/// </summary>
[Parameter]
[Parameter, Obsolete("BulletedList does not support AutoPostBack. Use OnClick event instead.")]
public bool AutoPostBack { get; set; }

/// <summary>
Expand Down
6 changes: 3 additions & 3 deletions src/BlazorWebFormsComponents/CheckBox.razor
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,16 @@
@if (TextAlign == Enums.TextAlign.Left)
{
<label for="@_inputId">@Text</label>
<input id="@_inputId" type="checkbox" checked="@Checked" disabled="@(!Enabled)" class="@CssClass" style="@Style" title="@ToolTip" accesskey="@(string.IsNullOrEmpty(AccessKey) ? null : AccessKey)" @onchange="HandleChange" />
<input id="@_inputId" type="checkbox" checked="@Checked" disabled="@(!Enabled)" class="@CssClass" style="@Style" title="@ToolTip" accesskey="@(string.IsNullOrEmpty(AccessKey) ? null : AccessKey)" @onchange="HandleChange" @attributes="GetAutoPostBackAttributes(AutoPostBack)" />
}
else
{
<input id="@_inputId" type="checkbox" checked="@Checked" disabled="@(!Enabled)" class="@CssClass" style="@Style" title="@ToolTip" accesskey="@(string.IsNullOrEmpty(AccessKey) ? null : AccessKey)" @onchange="HandleChange" />
<input id="@_inputId" type="checkbox" checked="@Checked" disabled="@(!Enabled)" class="@CssClass" style="@Style" title="@ToolTip" accesskey="@(string.IsNullOrEmpty(AccessKey) ? null : AccessKey)" @onchange="HandleChange" @attributes="GetAutoPostBackAttributes(AutoPostBack)" />
<label for="@_inputId">@Text</label>
}
}
else
{
<input id="@_inputId" type="checkbox" checked="@Checked" disabled="@(!Enabled)" class="@CssClass" style="@Style" title="@ToolTip" accesskey="@(string.IsNullOrEmpty(AccessKey) ? null : AccessKey)" @onchange="HandleChange" />
<input id="@_inputId" type="checkbox" checked="@Checked" disabled="@(!Enabled)" class="@CssClass" style="@Style" title="@ToolTip" accesskey="@(string.IsNullOrEmpty(AccessKey) ? null : AccessKey)" @onchange="HandleChange" @attributes="GetAutoPostBackAttributes(AutoPostBack)" />
}
}
7 changes: 6 additions & 1 deletion src/BlazorWebFormsComponents/CheckBox.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,12 @@ public partial class CheckBox : BaseStyledComponent
[Parameter]
public EventCallback<ChangeEventArgs> OnCheckedChanged { get; set; }

[Parameter, Obsolete("AutoPostBack is not supported in Blazor. Use OnCheckedChanged event instead.")]
/// <summary>
/// Gets or sets whether the control automatically posts back when the value changes.
/// In SSR mode, emits <c>onchange="this.form.submit()"</c> on the HTML element.
/// In Interactive mode, Blazor's native event binding handles change events automatically.
/// </summary>
[Parameter]
public bool AutoPostBack { get; set; }

private async Task HandleChange(ChangeEventArgs e)
Expand Down
4 changes: 2 additions & 2 deletions src/BlazorWebFormsComponents/CheckBoxList.razor
Original file line number Diff line number Diff line change
Expand Up @@ -98,11 +98,11 @@

if (TextAlign == Enums.TextAlign.Left)
{
<label for="@inputId">@item.Text</label><input id="@inputId" type="checkbox" name="@($"{_baseId}${index}")" value="@item.Value" checked="@isChecked" disabled="@(!Enabled || !item.Enabled)" @onchange="e => HandleChange(item, e)" />
<label for="@inputId">@item.Text</label><input id="@inputId" type="checkbox" name="@($"{_baseId}${index}")" value="@item.Value" checked="@isChecked" disabled="@(!Enabled || !item.Enabled)" @onchange="e => HandleChange(item, e)" @attributes="GetAutoPostBackAttributes(AutoPostBack)" />
}
else
{
<input id="@inputId" type="checkbox" name="@($"{_baseId}${index}")" value="@item.Value" checked="@isChecked" disabled="@(!Enabled || !item.Enabled)" @onchange="e => HandleChange(item, e)" /><label for="@inputId">@item.Text</label>
<input id="@inputId" type="checkbox" name="@($"{_baseId}${index}")" value="@item.Value" checked="@isChecked" disabled="@(!Enabled || !item.Enabled)" @onchange="e => HandleChange(item, e)" @attributes="GetAutoPostBackAttributes(AutoPostBack)" /><label for="@inputId">@item.Text</label>
}
};
}
7 changes: 4 additions & 3 deletions src/BlazorWebFormsComponents/CheckBoxList.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -85,10 +85,11 @@ public partial class CheckBoxList<ItemType> : BaseListControl<ItemType>
public EventCallback<ChangeEventArgs> OnSelectedIndexChanged { get; set; }

/// <summary>
/// Gets or sets a value indicating whether the control automatically posts back to the server when the selection changes.
/// This property is obsolete in Blazor and is included for compatibility only.
/// Gets or sets whether the control automatically posts back when the value changes.
/// In SSR mode, emits <c>onchange="this.form.submit()"</c> on the HTML element.
/// In Interactive mode, Blazor's native event binding handles change events automatically.
/// </summary>
[Parameter, Obsolete("AutoPostBack is not supported in Blazor. Use OnSelectedIndexChanged event instead.")]
[Parameter]
public bool AutoPostBack { get; set; }

/// <summary>
Expand Down
3 changes: 2 additions & 1 deletion src/BlazorWebFormsComponents/DropDownList.razor
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
@if (Visible)
{
<select id="@ClientID" class="@CssClass" style="@Style" title="@ToolTip"
disabled="@(!Enabled)" @onchange="HandleChange">
disabled="@(!Enabled)" @onchange="HandleChange"
@attributes="GetAutoPostBackAttributes(AutoPostBack)">
@foreach (var item in GetItems())
{
<option value="@item.Value" selected="@(item.Value == SelectedValue)">
Expand Down
7 changes: 4 additions & 3 deletions src/BlazorWebFormsComponents/DropDownList.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,11 @@ public partial class DropDownList<ItemType> : BaseListControl<ItemType>
public EventCallback<ChangeEventArgs> OnSelectedIndexChanged { get; set; }

/// <summary>
/// Gets or sets a value indicating whether the control automatically posts back to the server when the selection changes.
/// This property is obsolete in Blazor and is included for compatibility only.
/// Gets or sets whether the control automatically posts back when the value changes.
/// In SSR mode, emits <c>onchange="this.form.submit()"</c> on the HTML element.
/// In Interactive mode, Blazor's native event binding handles change events automatically.
/// </summary>
[Parameter, Obsolete("AutoPostBack is not supported in Blazor. Use OnSelectedIndexChanged event instead.")]
[Parameter]
public bool AutoPostBack { get; set; }

/// <summary>
Expand Down
Loading
Loading