Conversation
Add missing Web Forms-parity properties to ListView EventArgs types: - ListViewInsertEventArgs: add Values (IOrderedDictionary) - ListViewUpdateEventArgs: add Keys, OldValues, NewValues (IOrderedDictionary) - ListViewDeleteEventArgs: add Keys, Values (IOrderedDictionary) - ListViewPagePropertiesChangingEventArgs: add TotalRowCount All 16 events were already wired with [Parameter] EventCallbacks and HandleCommand routing. This commit completes EventArgs property parity with System.Web.UI.WebControls originals. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Document all 16 ListView lifecycle events covering insert, update, delete, edit/cancel, sorting, paging, selection, and lifecycle categories. Each event includes EventArgs property tables, Web Forms to Blazor syntax comparison tabs, and practical usage examples. Updated Features section and Blazor syntax block to reflect the complete event surface. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
43 comprehensive tests covering all ListView CRUD event pairs: - Insert: ItemInserting/ItemInserted (firing, cancellation, order, AffectedRows) - Update: ItemUpdating/ItemUpdated (firing, cancellation, order, KeepInEditMode) - Delete: ItemDeleting/ItemDeleted (firing, cancellation, order, AffectedRows) - Edit: ItemEditing (EventArgs, modified NewEditIndex, cancellation) - Cancel: ItemCanceling (CancelMode edit vs insert, cancellation vetoes) - Sort: Sorting/Sorted (order, cancellation, toggle direction, new expression) - Paging: PagePropertiesChanging/PagePropertiesChanged (order, args, zero values) - Selection: SelectedIndexChanging/SelectedIndexChanged (order, cancellation, modified index) - LayoutCreated (items present, empty list, custom layout) - ItemCreated (OnAfterRenderAsync first render) - DataBound/ItemDataBound (per-item firing) - HandleCommand routing (case-insensitive, unknown commands, null sort arg) - Full CRUD cycles (Edit->Update, Edit->Cancel, Delete->Insert) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
| var cut = Render(@<ListView Items="Widget.SimpleWidgetList" | ||
| ItemType="Widget" | ||
| OnLayoutCreated="OnLayoutCreated" | ||
| Context="Item"> | ||
| <ItemTemplate><span>@Item.Name</span></ItemTemplate> | ||
| </ListView>); | ||
|
|
||
| _layoutCreatedFired.ShouldBeTrue(); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void LayoutCreated_DoesNotFireWhenItemsEmpty() | ||
| { | ||
| Widget[] emptyItems = Array.Empty<Widget>(); | ||
| var cut = Render(@<ListView Items="emptyItems" | ||
| ItemType="Widget" | ||
| OnLayoutCreated="OnLayoutCreated" | ||
| Context="Item"> | ||
| <ItemTemplate><span>@Item.Name</span></ItemTemplate> | ||
| <EmptyItemTemplate><div>No data</div></EmptyItemTemplate> | ||
| </ListView>); | ||
|
|
||
| _layoutCreatedFired.ShouldBeFalse(); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void LayoutCreated_FiresWithCustomLayoutTemplate() | ||
| { | ||
| var cut = Render(@<ListView Items="Widget.SimpleWidgetList" | ||
| ItemType="Widget" | ||
| OnLayoutCreated="OnLayoutCreated" | ||
| Context="Item"> | ||
| <ItemTemplate><span>@Item.Name</span></ItemTemplate> | ||
| <LayoutTemplate Context="itemPlaceholder"> | ||
| <div class="custom-layout">@itemPlaceholder</div> | ||
| </LayoutTemplate> | ||
| </ListView>); | ||
|
|
||
| _layoutCreatedFired.ShouldBeTrue(); | ||
| cut.FindAll("div.custom-layout").Count.ShouldBe(1); | ||
| } | ||
|
|
||
| // ═════════════════════════════════════════════════════════════════ | ||
| // ITEM CREATED | ||
| // ═════════════════════════════════════════════════════════════════ | ||
|
|
||
| [Fact] | ||
| public async Task ItemCreated_FiresOnFirstRender() | ||
| { | ||
| var cut = Render(@<ListView Items="Widget.SimpleWidgetList" | ||
| ItemType="Widget" | ||
| ItemCreated="OnItemCreated" | ||
| Context="Item" | ||
| @ref="theListView"> | ||
| <ItemTemplate><span>@Item.Name</span></ItemTemplate> | ||
| </ListView>); | ||
|
|
||
| // ItemCreated fires in OnAfterRenderAsync(firstRender: true) | ||
| // Trigger a render cycle to ensure the event fires | ||
| await cut.InvokeAsync(() => { }); | ||
|
|
||
| _itemCreatedFired.ShouldBeTrue(); | ||
| } | ||
|
|
||
| // ═════════════════════════════════════════════════════════════════ | ||
| // DATA BOUND (override) | ||
| // ═════════════════════════════════════════════════════════════════ | ||
|
|
Check warning
Code scanning / CodeQL
Useless assignment to local variable Warning test
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI about 7 hours ago
In general, to fix a "useless assignment to local variable" you either remove the unused variable (if its value is not needed) or start using it meaningfully. Here, the test LayoutCreated_FiresWhenItemsExist never uses cut, and only needs the side effect of rendering the ListView to trigger OnLayoutCreated. The simplest, behavior‑preserving change is to call Render(...) without assigning its result to cut.
Concretely, in src/BlazorWebFormsComponents.Test/ListView/ListViewCrudEventTests.razor, within the LayoutCreated_FiresWhenItemsExist method (around line 658), replace var cut = Render(@<ListView ... with a bare Render(@<ListView ... call. No additional methods, imports, or definitions are required.
| @@ -655,7 +655,7 @@ | ||
| [Fact] | ||
| public void LayoutCreated_FiresWhenItemsExist() | ||
| { | ||
| var cut = Render(@<ListView Items="Widget.SimpleWidgetList" | ||
| Render(@<ListView Items="Widget.SimpleWidgetList" | ||
| ItemType="Widget" | ||
| OnLayoutCreated="OnLayoutCreated" | ||
| Context="Item"> |
| var cut = Render(@<ListView Items="emptyItems" | ||
| ItemType="Widget" | ||
| OnLayoutCreated="OnLayoutCreated" | ||
| Context="Item"> | ||
| <ItemTemplate><span>@Item.Name</span></ItemTemplate> | ||
| <EmptyItemTemplate><div>No data</div></EmptyItemTemplate> | ||
| </ListView>); | ||
|
|
||
| _layoutCreatedFired.ShouldBeFalse(); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void LayoutCreated_FiresWithCustomLayoutTemplate() | ||
| { | ||
| var cut = Render(@<ListView Items="Widget.SimpleWidgetList" | ||
| ItemType="Widget" | ||
| OnLayoutCreated="OnLayoutCreated" | ||
| Context="Item"> | ||
| <ItemTemplate><span>@Item.Name</span></ItemTemplate> | ||
| <LayoutTemplate Context="itemPlaceholder"> | ||
| <div class="custom-layout">@itemPlaceholder</div> | ||
| </LayoutTemplate> | ||
| </ListView>); | ||
|
|
||
| _layoutCreatedFired.ShouldBeTrue(); | ||
| cut.FindAll("div.custom-layout").Count.ShouldBe(1); | ||
| } | ||
|
|
||
| // ═════════════════════════════════════════════════════════════════ | ||
| // ITEM CREATED | ||
| // ═════════════════════════════════════════════════════════════════ | ||
|
|
||
| [Fact] | ||
| public async Task ItemCreated_FiresOnFirstRender() | ||
| { | ||
| var cut = Render(@<ListView Items="Widget.SimpleWidgetList" | ||
| ItemType="Widget" | ||
| ItemCreated="OnItemCreated" | ||
| Context="Item" | ||
| @ref="theListView"> | ||
| <ItemTemplate><span>@Item.Name</span></ItemTemplate> | ||
| </ListView>); | ||
|
|
||
| // ItemCreated fires in OnAfterRenderAsync(firstRender: true) | ||
| // Trigger a render cycle to ensure the event fires | ||
| await cut.InvokeAsync(() => { }); | ||
|
|
||
| _itemCreatedFired.ShouldBeTrue(); | ||
| } | ||
|
|
||
| // ═════════════════════════════════════════════════════════════════ | ||
| // DATA BOUND (override) | ||
| // ═════════════════════════════════════════════════════════════════ | ||
|
|
||
| [Fact] | ||
| public void DataBound_FiresAfterAllItemsRendered() | ||
| { | ||
| var cut = Render(@<ListView Items="Widget.SimpleWidgetList" | ||
| ItemType="Widget" | ||
| OnDataBound="OnDataBound" | ||
| OnItemDataBound="OnItemDataBound" | ||
| Context="Item"> | ||
| <ItemTemplate><span>@Item.Name</span></ItemTemplate> | ||
| </ListView>); | ||
|
|
||
| _dataBoundCount.ShouldBeGreaterThan(0); | ||
| _itemDataBoundCount.ShouldBeGreaterThanOrEqualTo(Widget.SimpleWidgetList.Length); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void DataBound_ItemDataBound_FiresPerItem() | ||
| { |
Check warning
Code scanning / CodeQL
Useless assignment to local variable Warning test
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI about 7 hours ago
General fix: when a local variable is assigned a value that is never read, either remove the variable (and just execute the expression for its side effects) or, if the value is actually needed, add the missing read/use. Here, the expression Render(@<ListView ...>) is required for its side effects (rendering the component and triggering events), but the returned cut is not used, so we should remove the variable and keep just the call.
Best fix in this file: in LayoutCreated_DoesNotFireWhenItemsEmpty, change the line var cut = Render(...); to just Render(...);. This keeps the render side effect, preserves test behavior, and removes the unused local variable. No additional imports, methods, or definitions are needed; it's a single-line change within src/BlazorWebFormsComponents.Test/ListView/ListViewCrudEventTests.razor.
Concretely:
- In
LayoutCreated_DoesNotFireWhenItemsEmpty(around line 672), replace the declaration/assignmentvar cut = Render(@<ListView ...with a standalone callRender(@<ListView .... - The rest of the method and file remain unchanged.
| @@ -669,7 +669,7 @@ | ||
| public void LayoutCreated_DoesNotFireWhenItemsEmpty() | ||
| { | ||
| Widget[] emptyItems = Array.Empty<Widget>(); | ||
| var cut = Render(@<ListView Items="emptyItems" | ||
| Render(@<ListView Items="emptyItems" | ||
| ItemType="Widget" | ||
| OnLayoutCreated="OnLayoutCreated" | ||
| Context="Item"> |
| var cut = Render(@<ListView Items="Widget.SimpleWidgetList" | ||
| ItemType="Widget" | ||
| OnDataBound="OnDataBound" | ||
| OnItemDataBound="OnItemDataBound" | ||
| Context="Item"> | ||
| <ItemTemplate><span>@Item.Name</span></ItemTemplate> | ||
| </ListView>); | ||
|
|
||
| _dataBoundCount.ShouldBeGreaterThan(0); | ||
| _itemDataBoundCount.ShouldBeGreaterThanOrEqualTo(Widget.SimpleWidgetList.Length); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void DataBound_ItemDataBound_FiresPerItem() | ||
| { | ||
| var cut = Render(@<ListView Items="Widget.SimpleWidgetList" | ||
| ItemType="Widget" | ||
| OnItemDataBound="OnItemDataBound" | ||
| Context="Item"> | ||
| <ItemTemplate><span>@Item.Name</span></ItemTemplate> | ||
| </ListView>); | ||
|
|
||
| // bUnit may trigger multiple render cycles; ensure at least one fire per item | ||
| _itemDataBoundCount.ShouldBeGreaterThanOrEqualTo(Widget.SimpleWidgetList.Length); | ||
| } | ||
|
|
||
| // ═════════════════════════════════════════════════════════════════ | ||
| // COMMAND ROUTING: HandleCommand dispatches to correct event | ||
| // ═════════════════════════════════════════════════════════════════ | ||
|
|
||
| [Fact] | ||
| public void HandleCommand_UnknownCommand_FiresItemCommandWithArgs() | ||
| { | ||
| var cut = Render(@<ListView Items="Widget.SimpleWidgetList" | ||
| ItemType="Widget" | ||
| ItemCommand="OnCommand" | ||
| Context="Item" | ||
| @ref="theListView"> | ||
| <ItemTemplate><span>@Item.Name</span></ItemTemplate> | ||
| </ListView>); | ||
|
|
||
| cut.InvokeAsync(() => theListView.HandleCommand("Approve", "request-42", 0)); | ||
|
|
||
| _commandArgs.ShouldNotBeNull(); | ||
| _commandArgs.CommandName.ShouldBe("Approve"); | ||
| _commandArgs.CommandArgument.ShouldBe("request-42"); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void HandleCommand_CaseInsensitive_RoutesCorrectly() | ||
| { | ||
| var cut = Render(@<ListView Items="Widget.SimpleWidgetList" | ||
| ItemType="Widget" | ||
| ItemEditing="OnEditing" | ||
| Context="Item" | ||
| @ref="theListView"> | ||
| <ItemTemplate><span>@Item.Name</span></ItemTemplate> | ||
| </ListView>); | ||
|
|
||
| cut.InvokeAsync(() => theListView.HandleCommand("EDIT", null, 0)); | ||
|
|
||
| _editingArgs.ShouldNotBeNull(); | ||
| _editingArgs.NewEditIndex.ShouldBe(0); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void HandleCommand_Sort_PassesCommandArgumentAsSortExpression() | ||
| { | ||
| var cut = Render(@<ListView Items="Widget.SimpleWidgetList" | ||
| ItemType="Widget" | ||
| Sorting="OnSorting" | ||
| Context="Item" | ||
| @ref="theListView"> | ||
| <ItemTemplate><span>@Item.Name</span></ItemTemplate> | ||
| </ListView>); | ||
|
|
||
| cut.InvokeAsync(() => theListView.HandleCommand("Sort", "Price", 0)); | ||
|
|
||
| _sortingArgs.ShouldNotBeNull(); | ||
| _sortingArgs.SortExpression.ShouldBe("Price"); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void HandleCommand_Sort_NullArgument_UsesEmptyString() | ||
| { |
Check warning
Code scanning / CodeQL
Useless assignment to local variable Warning test
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI about 7 hours ago
In general, to fix a "useless assignment to local variable" you either (1) remove the variable and just evaluate the expression for its side effects, if you don't need the result, or (2) start using the variable meaningfully if the result is actually needed. Here, the Render(...) call is required for its side effects (rendering the ListView and triggering data-bound events), but the cut variable itself is never used in DataBound_FiresAfterAllItemsRendered.
The best minimal fix without changing existing functionality is to remove the var cut = part and simply call Render(...) as a standalone statement. This preserves the render and event wiring but eliminates the unused local variable. Concretely, in src/BlazorWebFormsComponents.Test/ListView/ListViewCrudEventTests.razor, in the DataBound_FiresAfterAllItemsRendered method (starting at line 727), replace the line var cut = Render(@<ListView ... with Render(@<ListView ... and keep the rest of the method unchanged. No new imports, methods, or definitions are needed.
| @@ -726,7 +726,7 @@ | ||
| [Fact] | ||
| public void DataBound_FiresAfterAllItemsRendered() | ||
| { | ||
| var cut = Render(@<ListView Items="Widget.SimpleWidgetList" | ||
| Render(@<ListView Items="Widget.SimpleWidgetList" | ||
| ItemType="Widget" | ||
| OnDataBound="OnDataBound" | ||
| OnItemDataBound="OnItemDataBound" |
| var cut = Render(@<ListView Items="Widget.SimpleWidgetList" | ||
| ItemType="Widget" | ||
| OnItemDataBound="OnItemDataBound" | ||
| Context="Item"> | ||
| <ItemTemplate><span>@Item.Name</span></ItemTemplate> | ||
| </ListView>); | ||
|
|
||
| // bUnit may trigger multiple render cycles; ensure at least one fire per item | ||
| _itemDataBoundCount.ShouldBeGreaterThanOrEqualTo(Widget.SimpleWidgetList.Length); | ||
| } | ||
|
|
||
| // ═════════════════════════════════════════════════════════════════ | ||
| // COMMAND ROUTING: HandleCommand dispatches to correct event | ||
| // ═════════════════════════════════════════════════════════════════ | ||
|
|
||
| [Fact] | ||
| public void HandleCommand_UnknownCommand_FiresItemCommandWithArgs() | ||
| { | ||
| var cut = Render(@<ListView Items="Widget.SimpleWidgetList" | ||
| ItemType="Widget" | ||
| ItemCommand="OnCommand" | ||
| Context="Item" | ||
| @ref="theListView"> | ||
| <ItemTemplate><span>@Item.Name</span></ItemTemplate> | ||
| </ListView>); | ||
|
|
||
| cut.InvokeAsync(() => theListView.HandleCommand("Approve", "request-42", 0)); | ||
|
|
||
| _commandArgs.ShouldNotBeNull(); | ||
| _commandArgs.CommandName.ShouldBe("Approve"); | ||
| _commandArgs.CommandArgument.ShouldBe("request-42"); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void HandleCommand_CaseInsensitive_RoutesCorrectly() | ||
| { | ||
| var cut = Render(@<ListView Items="Widget.SimpleWidgetList" | ||
| ItemType="Widget" | ||
| ItemEditing="OnEditing" | ||
| Context="Item" | ||
| @ref="theListView"> | ||
| <ItemTemplate><span>@Item.Name</span></ItemTemplate> | ||
| </ListView>); | ||
|
|
||
| cut.InvokeAsync(() => theListView.HandleCommand("EDIT", null, 0)); | ||
|
|
||
| _editingArgs.ShouldNotBeNull(); | ||
| _editingArgs.NewEditIndex.ShouldBe(0); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void HandleCommand_Sort_PassesCommandArgumentAsSortExpression() | ||
| { | ||
| var cut = Render(@<ListView Items="Widget.SimpleWidgetList" | ||
| ItemType="Widget" | ||
| Sorting="OnSorting" | ||
| Context="Item" | ||
| @ref="theListView"> | ||
| <ItemTemplate><span>@Item.Name</span></ItemTemplate> | ||
| </ListView>); | ||
|
|
||
| cut.InvokeAsync(() => theListView.HandleCommand("Sort", "Price", 0)); | ||
|
|
||
| _sortingArgs.ShouldNotBeNull(); | ||
| _sortingArgs.SortExpression.ShouldBe("Price"); | ||
| } | ||
|
|
||
| [Fact] |
Check warning
Code scanning / CodeQL
Useless assignment to local variable Warning test
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI about 7 hours ago
To fix this type of issue, you remove the unnecessary local variable or actually use it meaningfully. Here, the Render(...) call is needed (to perform the render and trigger data bound events), but the cut variable itself is not, since it is never read. The most direct fix that preserves all behavior is to keep the Render(...) call but drop the var cut = part.
Concretely, in src/BlazorWebFormsComponents.Test/ListView/ListViewCrudEventTests.razor, inside the DataBound_ItemDataBound_FiresPerItem test method, replace the line starting with var cut = Render(@<ListView ... with a call to Render(@<ListView ... without assigning to any variable. The rest of the method, including the subsequent assertion on _itemDataBoundCount, remains unchanged. No new imports, methods, or definitions are needed.
| @@ -741,7 +741,7 @@ | ||
| [Fact] | ||
| public void DataBound_ItemDataBound_FiresPerItem() | ||
| { | ||
| var cut = Render(@<ListView Items="Widget.SimpleWidgetList" | ||
| Render(@<ListView Items="Widget.SimpleWidgetList" | ||
| ItemType="Widget" | ||
| OnItemDataBound="OnItemDataBound" | ||
| Context="Item"> |
| ListView<Widget> theListView; | ||
|
|
||
| // ── Event tracking ── | ||
| List<string> _eventOrder = new(); |
Check notice
Code scanning / CodeQL
Missed 'readonly' opportunity Note test
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI about 7 hours ago
To fix the problem, the field _eventOrder should be declared as readonly so that its variable reference cannot be reassigned after initialization, while still allowing modifications to the list contents. This aligns with CodeQL’s recommendation and guards against accidental reassignment in future changes.
Concretely, in src/BlazorWebFormsComponents.Test/ListView/ListViewCrudEventTests.razor, within the @code block, update the declaration on line 13 from List<string> _eventOrder = new(); to readonly List<string> _eventOrder = new();. No other fields are flagged, and no additional methods, imports, or definitions are required, because readonly is a standard C# modifier and collection mutation (e.g., _eventOrder.Add(...)) remains fully supported.
| @@ -10,7 +10,7 @@ | ||
| ListView<Widget> theListView; | ||
|
|
||
| // ── Event tracking ── | ||
| List<string> _eventOrder = new(); | ||
| readonly List<string> _eventOrder = new(); | ||
| ListViewEditEventArgs _editingArgs; | ||
| ListViewCancelEventArgs _cancelingArgs; | ||
| ListViewDeleteEventArgs _deletingArgs; |
Closes FritzAndFriends#356
Completes EventArgs property parity for ListView CRUD events to match the System.Web.UI.WebControls originals.
All 16 events already wired with [Parameter] EventCallback declarations and HandleCommand routing. This PR adds missing dictionary properties to EventArgs types.
Changes: