From ecce9b3357a807f88ef974d411a26460e7ff2a8a Mon Sep 17 00:00:00 2001 From: "Jeffrey T. Fritz" Date: Mon, 2 Mar 2026 08:14:54 -0500 Subject: [PATCH 1/5] chore: bump version.json to 0.17 for next dev cycle Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.json b/version.json index aa5a2d167..b366e4428 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/main/src/NerdBank.GitVersioning/version.schema.json", - "version": "0.15", + "version": "0.17", "publicReleaseRefSpec": [ "^refs/heads/master$", "^refs/heads/main$", From 796e6119e365a92aa0de600cee5ab3bc54868d54 Mon Sep 17 00:00:00 2001 From: "Jeffrey T. Fritz" Date: Mon, 2 Mar 2026 23:30:55 -0500 Subject: [PATCH 2/5] feat: implement 16 ListView CRUD events (#356) 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> --- .../ListViewDeleteEventArgs.cs | 13 +++++++++++++ .../ListViewInsertEventArgs.cs | 7 +++++++ ...ListViewPagePropertiesChangingEventArgs.cs | 5 +++++ .../ListViewUpdateEventArgs.cs | 19 +++++++++++++++++++ 4 files changed, 44 insertions(+) diff --git a/src/BlazorWebFormsComponents/ListViewDeleteEventArgs.cs b/src/BlazorWebFormsComponents/ListViewDeleteEventArgs.cs index c2ecee066..5488aa36a 100644 --- a/src/BlazorWebFormsComponents/ListViewDeleteEventArgs.cs +++ b/src/BlazorWebFormsComponents/ListViewDeleteEventArgs.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Specialized; namespace BlazorWebFormsComponents { @@ -10,6 +11,8 @@ public class ListViewDeleteEventArgs : EventArgs public ListViewDeleteEventArgs(int itemIndex) { ItemIndex = itemIndex; + Keys = new OrderedDictionary(); + Values = new OrderedDictionary(); } /// @@ -21,5 +24,15 @@ public ListViewDeleteEventArgs(int itemIndex) /// Gets or sets a value indicating whether the event should be cancelled. /// public bool Cancel { get; set; } + + /// + /// Gets a dictionary of field name/value pairs that represent the key of the item to delete. + /// + public IOrderedDictionary Keys { get; } + + /// + /// Gets a dictionary of the non-key field name/value pairs in the item to delete. + /// + public IOrderedDictionary Values { get; } } } diff --git a/src/BlazorWebFormsComponents/ListViewInsertEventArgs.cs b/src/BlazorWebFormsComponents/ListViewInsertEventArgs.cs index 6255c3009..99ee49479 100644 --- a/src/BlazorWebFormsComponents/ListViewInsertEventArgs.cs +++ b/src/BlazorWebFormsComponents/ListViewInsertEventArgs.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Specialized; namespace BlazorWebFormsComponents { @@ -9,6 +10,7 @@ public class ListViewInsertEventArgs : EventArgs { public ListViewInsertEventArgs() { + Values = new OrderedDictionary(); } /// @@ -20,5 +22,10 @@ public ListViewInsertEventArgs() /// Gets the item to be inserted. /// public object Item { get; set; } + + /// + /// Gets the values for the record to insert. + /// + public IOrderedDictionary Values { get; } } } diff --git a/src/BlazorWebFormsComponents/ListViewPagePropertiesChangingEventArgs.cs b/src/BlazorWebFormsComponents/ListViewPagePropertiesChangingEventArgs.cs index 7ff269598..58069f908 100644 --- a/src/BlazorWebFormsComponents/ListViewPagePropertiesChangingEventArgs.cs +++ b/src/BlazorWebFormsComponents/ListViewPagePropertiesChangingEventArgs.cs @@ -22,5 +22,10 @@ public ListViewPagePropertiesChangingEventArgs(int startRowIndex, int maximumRow /// Gets the maximum number of items to display on each page. /// public int MaximumRows { get; } + + /// + /// Gets or sets the total number of rows in the underlying data source. + /// + public int TotalRowCount { get; set; } } } diff --git a/src/BlazorWebFormsComponents/ListViewUpdateEventArgs.cs b/src/BlazorWebFormsComponents/ListViewUpdateEventArgs.cs index 29aecbd49..b766e1841 100644 --- a/src/BlazorWebFormsComponents/ListViewUpdateEventArgs.cs +++ b/src/BlazorWebFormsComponents/ListViewUpdateEventArgs.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Specialized; namespace BlazorWebFormsComponents { @@ -10,6 +11,9 @@ public class ListViewUpdateEventArgs : EventArgs public ListViewUpdateEventArgs(int itemIndex) { ItemIndex = itemIndex; + Keys = new OrderedDictionary(); + OldValues = new OrderedDictionary(); + NewValues = new OrderedDictionary(); } /// @@ -21,5 +25,20 @@ public ListViewUpdateEventArgs(int itemIndex) /// Gets or sets a value indicating whether the event should be cancelled. /// public bool Cancel { get; set; } + + /// + /// Gets a dictionary of field name/value pairs that represent the key of the item to update. + /// + public IOrderedDictionary Keys { get; } + + /// + /// Gets a dictionary that contains the original values of the item to update. + /// + public IOrderedDictionary OldValues { get; } + + /// + /// Gets a dictionary that contains the revised values of the item to update. + /// + public IOrderedDictionary NewValues { get; } } } From 2d0f6378add59c4b0947459215f791bf899602f3 Mon Sep 17 00:00:00 2001 From: "Jeffrey T. Fritz" Date: Mon, 2 Mar 2026 23:32:03 -0500 Subject: [PATCH 3/5] docs: add ListView CRUD events documentation (#356) 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> --- docs/DataControls/ListView.md | 728 +++++++++++++++++++++++++++++++++- 1 file changed, 724 insertions(+), 4 deletions(-) diff --git a/docs/DataControls/ListView.md b/docs/DataControls/ListView.md index deed7966d..21124c72b 100644 --- a/docs/DataControls/ListView.md +++ b/docs/DataControls/ListView.md @@ -2,7 +2,7 @@ The ListView component is meant to emulate the asp:ListView control in markup and is defined in the [System.Web.UI.WebControls.ListView class](https://docs.microsoft.com/en-us/dotnet/api/system.web.ui.webcontrols.listview?view=netframework-4.8) -[Usage Notes](#usage-notes) | [Web Forms Syntax](#web-forms-declarative-syntax) | [Blazor Syntax](#blazor-syntax) +[Usage Notes](#usage-notes) | [Web Forms Syntax](#web-forms-declarative-syntax) | [Blazor Syntax](#blazor-syntax) | [CRUD Events](#crud-events) ## Features supported in Blazor - Alternating Item Templates @@ -22,6 +22,15 @@ The ListView component is meant to emulate the asp:ListView control in markup an - InsertItemTemplate - EditIndex - InsertItemPosition + - **CRUD Events** — 16 lifecycle events for data operations: + - **Insert:** ItemInserting / ItemInserted + - **Update:** ItemUpdating / ItemUpdated + - **Delete:** ItemDeleting / ItemDeleted + - **Edit / Cancel:** ItemEditing / ItemCanceling + - **Sort:** Sorting / Sorted + - **Paging:** PagePropertiesChanging / PagePropertiesChanged + - **Selection:** SelectedIndexChanging / SelectedIndexChanged + - **Lifecycle:** OnLayoutCreated / ItemCreated / OnItemDataBound (DataBound override) ##### [Back to top](#listview) @@ -147,15 +156,28 @@ void ItemDataBound(ListViewItemEventArgs e) ## Blazor Syntax ```razor - + ItemDeleted="OnItemDeleted" + ItemInserting="OnItemInserting" + ItemInserted="OnItemInserted" + Sorting="OnSorting" + Sorted="OnSorted" + PagePropertiesChanging="OnPagePropertiesChanging" + PagePropertiesChanged="OnPagePropertiesChanged" + OnLayoutCreated="OnLayoutCreated" + SelectedIndexChanging="OnSelectedIndexChanging" + SelectedIndexChanged="OnSelectedIndexChanged" + ItemCreated="OnItemCreated" + OnItemDataBound="OnItemDataBound"> @Item.Name @@ -273,4 +295,702 @@ The `InsertItemTemplate` renders an insert row when `InsertItemPosition` is set ``` -##### [Back to top](#listview) \ No newline at end of file +##### [Back to top](#listview) + +## CRUD Events + +The ListView fires 16 lifecycle events that mirror the original Web Forms control's event model. These events use `EventCallback` parameters and follow the familiar *-ing / *-ed pattern: the *-ing event fires before the operation (and can cancel it), while the *-ed event fires after the operation completes. + +!!! tip "Command Routing" + All CRUD events are triggered through the `HandleCommand` method on the ListView reference. Call `listView.HandleCommand("edit", null, index)` to trigger the edit flow, `"delete"` for delete, `"update"` for update, `"insert"` for insert, `"cancel"` for cancel, `"sort"` for sort, and `"select"` for selection. + +### Event Reference + +#### Insert Events + +| Event | EventArgs | Description | +|-------|-----------|-------------| +| `ItemInserting` | `ListViewInsertEventArgs` | Fires before an item is inserted. Set `Cancel = true` to prevent insertion. | +| `ItemInserted` | `ListViewInsertedEventArgs` | Fires after the insert operation completes. | + +**`ListViewInsertEventArgs` properties:** + +| Property | Type | Description | +|----------|------|-------------| +| `Cancel` | `bool` | Set to `true` to cancel the insert operation. | +| `Item` | `object` | Gets or sets the item to be inserted. | + +**`ListViewInsertedEventArgs` properties:** + +| Property | Type | Description | +|----------|------|-------------| +| `AffectedRows` | `int` | The number of rows affected by the insert. | +| `Exception` | `Exception` | The exception raised during insert, if any. | +| `ExceptionHandled` | `bool` | Set to `true` to indicate the exception has been handled. | +| `KeepInsertedValues` | `bool` | Set to `true` to retain the values in the insert form after insertion. | + +**Web Forms → Blazor comparison:** + +=== "Web Forms" + + ```html + + ``` + ```csharp + protected void lv_ItemInserting(object sender, ListViewInsertEventArgs e) + { + // Validate before insert + if (string.IsNullOrEmpty(txtName.Text)) + { + e.Cancel = true; + } + } + + protected void lv_ItemInserted(object sender, ListViewInsertedEventArgs e) + { + if (e.Exception != null) + { + e.ExceptionHandled = true; + lblError.Text = "Insert failed."; + } + } + ``` + +=== "Blazor" + + ```razor + + ``` + ```csharp + @code { + void OnItemInserting(ListViewInsertEventArgs e) + { + if (string.IsNullOrEmpty(newName)) + { + e.Cancel = true; + } + } + + void OnItemInserted(ListViewInsertedEventArgs e) + { + if (e.Exception != null) + { + e.ExceptionHandled = true; + errorMessage = "Insert failed."; + } + } + } + ``` + +##### [Back to top](#listview) + +#### Update Events + +| Event | EventArgs | Description | +|-------|-----------|-------------| +| `ItemUpdating` | `ListViewUpdateEventArgs` | Fires before an item is updated. Set `Cancel = true` to prevent the update. | +| `ItemUpdated` | `ListViewUpdatedEventArgs` | Fires after the update operation completes. | + +**`ListViewUpdateEventArgs` properties:** + +| Property | Type | Description | +|----------|------|-------------| +| `ItemIndex` | `int` | The index of the item being updated. | +| `Cancel` | `bool` | Set to `true` to cancel the update operation. | + +**`ListViewUpdatedEventArgs` properties:** + +| Property | Type | Description | +|----------|------|-------------| +| `AffectedRows` | `int` | The number of rows affected by the update. | +| `Exception` | `Exception` | The exception raised during update, if any. | +| `ExceptionHandled` | `bool` | Set to `true` to indicate the exception has been handled. | +| `KeepInEditMode` | `bool` | Set to `true` to keep the row in edit mode after the update. | + +**Web Forms → Blazor comparison:** + +=== "Web Forms" + + ```html + + ``` + ```csharp + protected void lv_ItemUpdating(object sender, ListViewUpdateEventArgs e) + { + // e.ItemIndex identifies which item is being updated + } + + protected void lv_ItemUpdated(object sender, ListViewUpdatedEventArgs e) + { + if (e.Exception != null) + { + e.ExceptionHandled = true; + } + } + ``` + +=== "Blazor" + + ```razor + + ``` + ```csharp + @code { + void OnItemUpdating(ListViewUpdateEventArgs e) + { + var product = products[e.ItemIndex]; + product.Name = editName; + product.Price = editPrice; + } + + void OnItemUpdated(ListViewUpdatedEventArgs e) + { + if (e.Exception != null) + { + e.ExceptionHandled = true; + errorMessage = "Update failed."; + } + // Row exits edit mode automatically unless KeepInEditMode = true + } + } + ``` + +##### [Back to top](#listview) + +#### Delete Events + +| Event | EventArgs | Description | +|-------|-----------|-------------| +| `ItemDeleting` | `ListViewDeleteEventArgs` | Fires before an item is deleted. Set `Cancel = true` to prevent deletion. | +| `ItemDeleted` | `ListViewDeletedEventArgs` | Fires after the delete operation completes. | + +**`ListViewDeleteEventArgs` properties:** + +| Property | Type | Description | +|----------|------|-------------| +| `ItemIndex` | `int` | The index of the item being deleted. | +| `Cancel` | `bool` | Set to `true` to cancel the delete operation. | + +**`ListViewDeletedEventArgs` properties:** + +| Property | Type | Description | +|----------|------|-------------| +| `AffectedRows` | `int` | The number of rows affected by the delete. | +| `Exception` | `Exception` | The exception raised during delete, if any. | +| `ExceptionHandled` | `bool` | Set to `true` to indicate the exception has been handled. | + +**Web Forms → Blazor comparison:** + +=== "Web Forms" + + ```html + + ``` + ```csharp + protected void lv_ItemDeleting(object sender, ListViewDeleteEventArgs e) + { + // Confirm deletion; set e.Cancel = true to abort + } + + protected void lv_ItemDeleted(object sender, ListViewDeletedEventArgs e) + { + if (e.AffectedRows == 1) + { + // Refresh data + } + } + ``` + +=== "Blazor" + + ```razor + + ``` + ```csharp + @code { + void OnItemDeleting(ListViewDeleteEventArgs e) + { + products.RemoveAt(e.ItemIndex); + } + + void OnItemDeleted(ListViewDeletedEventArgs e) + { + statusMessage = $"Deleted. {e.AffectedRows} row(s) affected."; + } + } + ``` + +##### [Back to top](#listview) + +#### Edit and Cancel Events + +| Event | EventArgs | Description | +|-------|-----------|-------------| +| `ItemEditing` | `ListViewEditEventArgs` | Fires when an Edit command is requested. Set `NewEditIndex` to control which item enters edit mode. | +| `ItemCanceling` | `ListViewCancelEventArgs` | Fires when a Cancel command is requested. `CancelMode` indicates whether canceling an edit or an insert. | + +**`ListViewEditEventArgs` properties:** + +| Property | Type | Description | +|----------|------|-------------| +| `NewEditIndex` | `int` | The index of the item entering edit mode. | +| `Cancel` | `bool` | Set to `true` to cancel the edit operation. | + +**`ListViewCancelEventArgs` properties:** + +| Property | Type | Description | +|----------|------|-------------| +| `ItemIndex` | `int` | The index of the item containing the Cancel button. | +| `CancelMode` | `ListViewCancelMode` | Indicates whether canceling an edit (`CancelingEdit`) or an insert (`CancelingInsert`). | +| `Cancel` | `bool` | Set to `true` to prevent the cancel operation. | + +**Web Forms → Blazor comparison:** + +=== "Web Forms" + + ```html + + ``` + ```csharp + protected void lv_ItemEditing(object sender, ListViewEditEventArgs e) + { + lv.EditIndex = e.NewEditIndex; + BindData(); + } + + protected void lv_ItemCanceling(object sender, ListViewCancelEventArgs e) + { + lv.EditIndex = -1; + BindData(); + } + ``` + +=== "Blazor" + + ```razor + + ``` + ```csharp + @code { + private int editIndex = -1; + + void OnItemEditing(ListViewEditEventArgs e) + { + editIndex = e.NewEditIndex; + } + + void OnItemCanceling(ListViewCancelEventArgs e) + { + editIndex = -1; + // e.CancelMode tells you if canceling an edit or insert + } + } + ``` + +!!! note "Automatic EditIndex Management" + The ListView automatically sets `EditIndex` to the `NewEditIndex` from `ListViewEditEventArgs` after `ItemEditing` fires (unless cancelled), and resets `EditIndex` to `-1` after `ItemCanceling` fires. You can still set `EditIndex` manually in your handler for custom behavior. + +##### [Back to top](#listview) + +#### Sorting Events + +| Event | EventArgs | Description | +|-------|-----------|-------------| +| `Sorting` | `ListViewSortEventArgs` | Fires before a sort operation. Set `Cancel = true` to prevent sorting. | +| `Sorted` | `ListViewSortEventArgs` | Fires after the sort operation completes. | + +**`ListViewSortEventArgs` properties:** + +| Property | Type | Description | +|----------|------|-------------| +| `SortExpression` | `string` | The expression (typically a property name) to sort by. | +| `SortDirection` | `SortDirection` | The sort direction: `Ascending` or `Descending`. Automatically toggles. | +| `Cancel` | `bool` | Set to `true` to cancel the sort operation. | + +**Web Forms → Blazor comparison:** + +=== "Web Forms" + + ```html + + ``` + ```csharp + protected void lv_Sorting(object sender, ListViewSortEventArgs e) + { + // e.SortExpression, e.SortDirection available + } + ``` + +=== "Blazor" + + ```razor + + ``` + ```csharp + @code { + void OnSorting(ListViewSortEventArgs e) + { + // Sort direction toggles automatically between Ascending/Descending + // Sort your data source using e.SortExpression and e.SortDirection + } + + void OnSorted(ListViewSortEventArgs e) + { + statusMessage = $"Sorted by {e.SortExpression} ({e.SortDirection})"; + } + } + ``` + +!!! tip "Sort Direction Toggle" + The ListView automatically toggles `SortDirection` between `Ascending` and `Descending` when the same `SortExpression` is used consecutively, matching Web Forms behavior. + +##### [Back to top](#listview) + +#### Paging Events + +| Event | EventArgs | Description | +|-------|-----------|-------------| +| `PagePropertiesChanging` | `ListViewPagePropertiesChangingEventArgs` | Fires when page properties are about to change. | +| `PagePropertiesChanged` | `EventArgs` | Fires after the page properties have changed. | + +**`ListViewPagePropertiesChangingEventArgs` properties:** + +| Property | Type | Description | +|----------|------|-------------| +| `StartRowIndex` | `int` | The index of the first item on the new page. | +| `MaximumRows` | `int` | The maximum number of items to display per page. | + +!!! note "Paging via SetPageProperties" + Unlike the CRUD events which are triggered by `HandleCommand`, paging events fire when you call `listView.SetPageProperties(startRowIndex, maximumRows)`. This mirrors Web Forms' `DataPager` interaction pattern. + +**Web Forms → Blazor comparison:** + +=== "Web Forms" + + ```html + + ``` + ```csharp + protected void lv_PagePropertiesChanging(object sender, + PagePropertiesChangingEventArgs e) + { + // e.StartRowIndex, e.MaximumRows available + } + ``` + +=== "Blazor" + + ```razor + + ``` + ```csharp + @code { + void OnPagePropertiesChanging(ListViewPagePropertiesChangingEventArgs e) + { + // e.StartRowIndex and e.MaximumRows reflect the new page + } + + void OnPagePropertiesChanged() + { + // Page properties have been applied — refresh UI if needed + } + } + ``` + +##### [Back to top](#listview) + +#### Selection Events + +| Event | EventArgs | Description | +|-------|-----------|-------------| +| `SelectedIndexChanging` | `ListViewSelectEventArgs` | Fires before the selected index changes. Set `Cancel = true` to prevent the change. | +| `SelectedIndexChanged` | `EventArgs` | Fires after the selected index has changed. | + +**`ListViewSelectEventArgs` properties:** + +| Property | Type | Description | +|----------|------|-------------| +| `NewSelectedIndex` | `int` | The index of the newly selected item. | +| `Cancel` | `bool` | Set to `true` to cancel the selection change. | + +**Web Forms → Blazor comparison:** + +=== "Web Forms" + + ```html + + ``` + ```csharp + protected void lv_SelectedIndexChanging(object sender, ListViewSelectEventArgs e) + { + // e.NewSelectedIndex identifies the item being selected + } + ``` + +=== "Blazor" + + ```razor + + ``` + ```csharp + @code { + private int selectedIndex; + + void OnSelectedIndexChanging(ListViewSelectEventArgs e) + { + // Validate selection; set e.Cancel = true to prevent + } + + void OnSelectedIndexChanged() + { + statusMessage = $"Selected item index: {selectedIndex}"; + } + } + ``` + +##### [Back to top](#listview) + +#### Lifecycle Events + +| Event | EventArgs | Description | +|-------|-----------|-------------| +| `OnLayoutCreated` | `EventArgs` | Fires after the layout template has been created. | +| `ItemCreated` | (none) | Fires when an item template is instantiated (after first render). | +| `OnItemDataBound` | `ListViewItemEventArgs` | Fires when an item is data-bound (existing event, listed here for completeness). | + +These lifecycle events fire during the component's rendering lifecycle rather than in response to user commands. + +**Web Forms → Blazor comparison:** + +=== "Web Forms" + + ```html + + ``` + +=== "Blazor" + + ```razor + + ``` + ```csharp + @code { + void HandleLayoutCreated() + { + // Layout template has been rendered + } + + void HandleItemCreated() + { + // Item template instantiated — runs once after first render + } + + void HandleItemDataBound(ListViewItemEventArgs e) + { + if (e.Item.ItemType == ListViewItemType.DataItem) + { + var dataItem = (ListViewDataItem)e.Item; + // Access dataItem.DataItem, dataItem.DisplayIndex, etc. + } + } + } + ``` + +##### [Back to top](#listview) + +### Complete CRUD Example + +This example demonstrates a full insert/update/delete workflow with all relevant events wired up: + +```razor + + + + + + + + + + + @context +
NamePriceActions
+
+ + + @Item.Name + @Item.Price.ToString("C") + + + + + + + + + + + + + + + + + + + + + + + + + + +

No products found. Use the insert row to add one.

+
+
+ +@if (!string.IsNullOrEmpty(statusMessage)) +{ +
@statusMessage
+} + +@code { + private ListView listView; + private List products = new(); + private int editIndex = -1; + private string editName, newName, statusMessage; + private decimal editPrice, newPrice; + + void OnItemEditing(ListViewEditEventArgs e) + { + var product = products.First(p => p.Id == e.NewEditIndex); + editName = product.Name; + editPrice = product.Price; + editIndex = e.NewEditIndex; + } + + void OnItemUpdating(ListViewUpdateEventArgs e) + { + var product = products.First(p => p.Id == e.ItemIndex); + product.Name = editName; + product.Price = editPrice; + } + + void OnItemUpdated(ListViewUpdatedEventArgs e) + { + statusMessage = "Product updated successfully."; + } + + void OnItemCanceling(ListViewCancelEventArgs e) + { + editIndex = -1; + } + + void OnItemDeleting(ListViewDeleteEventArgs e) + { + products.RemoveAll(p => p.Id == e.ItemIndex); + } + + void OnItemDeleted(ListViewDeletedEventArgs e) + { + statusMessage = "Product deleted."; + } + + void OnItemInserting(ListViewInsertEventArgs e) + { + if (string.IsNullOrWhiteSpace(newName)) + { + e.Cancel = true; + statusMessage = "Name is required."; + return; + } + products.Add(new Product + { + Id = products.Any() ? products.Max(p => p.Id) + 1 : 1, + Name = newName, + Price = newPrice + }); + newName = ""; + newPrice = 0; + } + + void OnItemInserted(ListViewInsertedEventArgs e) + { + statusMessage = "Product inserted."; + } + + public class Product + { + public int Id { get; set; } + public string Name { get; set; } = ""; + public decimal Price { get; set; } + } +} +``` + +## See Also + +- [FormView](FormView.md) — Similar CRUD event model for single-record views +- [DetailsView](DetailsView.md) — Single-record display with edit support +- [GridView](GridView.md) — Tabular data display +- [DataPager](DataPager.md) — Paging companion for ListView +- [Microsoft Docs: ListView Class](https://docs.microsoft.com/en-us/dotnet/api/system.web.ui.webcontrols.listview?view=netframework-4.8) \ No newline at end of file From 41ef79c2716b77d153d509d03d2b621d7773d369 Mon Sep 17 00:00:00 2001 From: "Jeffrey T. Fritz" Date: Mon, 2 Mar 2026 23:32:37 -0500 Subject: [PATCH 4/5] chore: update Beast history with ListView CRUD events docs Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .ai-team/agents/beast/history.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/.ai-team/agents/beast/history.md b/.ai-team/agents/beast/history.md index b0ade346b..f414b06e8 100644 --- a/.ai-team/agents/beast/history.md +++ b/.ai-team/agents/beast/history.md @@ -94,3 +94,18 @@ - Structured for non-technical readers: bottom-line callout, business value lead, tables for data, minimal jargon - Sections: What is BWFC, Migration Scope, Component Coverage, Three-Layer Pipeline, Time & Cost Impact, Layer 1 Results, Page Readiness, Risk Reduction, What's Next +- **Issue #356 — ListView CRUD Events documentation:** + - Updated `docs/DataControls/ListView.md` with comprehensive CRUD events section covering all 16 lifecycle events. + - Events organized into 7 sub-sections: Insert, Update, Delete, Edit/Cancel, Sorting, Paging, Selection, and Lifecycle. + - Each event sub-section includes: EventArgs property reference table, Web Forms → Blazor tabbed comparison (using `pymdownx.tabbed`), and practical code examples. + - EventArgs types documented from source: `ListViewInsertEventArgs`, `ListViewInsertedEventArgs`, `ListViewUpdateEventArgs`, `ListViewUpdatedEventArgs`, `ListViewDeleteEventArgs`, `ListViewDeletedEventArgs`, `ListViewEditEventArgs`, `ListViewCancelEventArgs`, `ListViewSortEventArgs`, `ListViewPagePropertiesChangingEventArgs`, `ListViewSelectEventArgs`, `ListViewItemEventArgs`. + - Added `ListViewCancelMode` enum documentation (`CancelingEdit`, `CancelingInsert`). + - Updated Features Supported section with full event listing grouped by category. + - Updated Blazor Syntax block to show all 16 event parameters. + - Added complete CRUD example demonstrating insert/update/delete workflow with all events wired up. + - Added See Also section with cross-references to FormView, DetailsView, GridView, DataPager, and Microsoft Docs. + - Used admonitions (`!!! tip`, `!!! note`) for command routing, automatic EditIndex management, sort direction toggle, and paging via SetPageProperties. + - **Key file:** `docs/DataControls/ListView.md` (grew from ~285 lines to ~840 lines). + - **Pattern used:** Tabbed Web Forms → Blazor comparison (pymdownx.tabbed), property reference tables, grouped event sub-sections. Follows FormView CRUD events documentation pattern. + - **Branch:** `squad/356-listview-crud-events` + From 9115cd3909e4734e91f1b65729db3197d8c1ff83 Mon Sep 17 00:00:00 2001 From: "Jeffrey T. Fritz" Date: Mon, 2 Mar 2026 23:33:07 -0500 Subject: [PATCH 5/5] test: add bUnit tests for 16 ListView CRUD events (#356) 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> --- .../ListView/ListViewCrudEventTests.razor | 953 ++++++++++++++++++ 1 file changed, 953 insertions(+) create mode 100644 src/BlazorWebFormsComponents.Test/ListView/ListViewCrudEventTests.razor diff --git a/src/BlazorWebFormsComponents.Test/ListView/ListViewCrudEventTests.razor b/src/BlazorWebFormsComponents.Test/ListView/ListViewCrudEventTests.razor new file mode 100644 index 000000000..1a1b2b335 --- /dev/null +++ b/src/BlazorWebFormsComponents.Test/ListView/ListViewCrudEventTests.razor @@ -0,0 +1,953 @@ +@using BlazorWebFormsComponents.Enums +@using SortDirection = BlazorWebFormsComponents.Enums.SortDirection + +@* Comprehensive tests for all 16 ListView CRUD events (#356). + Covers: event firing, EventArgs data, Cancel property, event ordering, + HandleCommand routing, and edge cases. *@ + +@code { + + ListView theListView; + + // ── Event tracking ── + List _eventOrder = new(); + ListViewEditEventArgs _editingArgs; + ListViewCancelEventArgs _cancelingArgs; + ListViewDeleteEventArgs _deletingArgs; + ListViewDeletedEventArgs _deletedArgs; + ListViewInsertEventArgs _insertingArgs; + ListViewInsertedEventArgs _insertedArgs; + ListViewUpdateEventArgs _updatingArgs; + ListViewUpdatedEventArgs _updatedArgs; + ListViewCommandEventArgs _commandArgs; + ListViewSortEventArgs _sortingArgs; + ListViewSortEventArgs _sortedArgs; + ListViewSelectEventArgs _selectingArgs; + ListViewPagePropertiesChangingEventArgs _pageChangingArgs; + bool _pageChangedFired; + bool _selectedChangedFired; + bool _layoutCreatedFired; + bool _itemCreatedFired; + int _dataBoundCount; + int _itemDataBoundCount; + + // ═════════════════════════════════════════════════════════════════ + // INSERT: ItemInserting / ItemInserted + // ═════════════════════════════════════════════════════════════════ + + [Fact] + public void HandleCommand_Insert_FiresItemInsertingBeforeItemInserted() + { + var cut = Render(@ + @Item.Name + ); + + cut.InvokeAsync(() => theListView.HandleCommand("Insert", null, 0)); + + _eventOrder.Count.ShouldBe(2); + _eventOrder[0].ShouldBe("ItemInserting"); + _eventOrder[1].ShouldBe("ItemInserted"); + } + + [Fact] + public void HandleCommand_Insert_Cancellation_PreventsItemInserted() + { + var cut = Render(@ + @Item.Name + ); + + cut.InvokeAsync(() => theListView.HandleCommand("Insert", null, 0)); + + _insertingArgs.ShouldNotBeNull(); + _insertingArgs.Cancel.ShouldBeTrue(); + _insertedArgs.ShouldBeNull(); + } + + [Fact] + public void HandleCommand_Insert_InsertedArgs_HasAffectedRows() + { + var cut = Render(@ + @Item.Name + ); + + cut.InvokeAsync(() => theListView.HandleCommand("Insert", null, 0)); + + _insertedArgs.ShouldNotBeNull(); + _insertedArgs.AffectedRows.ShouldBe(1); + _insertedArgs.Exception.ShouldBeNull(); + _insertedArgs.ExceptionHandled.ShouldBeFalse(); + _insertedArgs.KeepInsertedValues.ShouldBeFalse(); + } + + // ═════════════════════════════════════════════════════════════════ + // UPDATE: ItemUpdating / ItemUpdated + // ═════════════════════════════════════════════════════════════════ + + [Fact] + public void HandleCommand_Update_FiresItemUpdatingBeforeItemUpdated() + { + var cut = Render(@ + @Item.Name + ); + + cut.InvokeAsync(() => theListView.HandleCommand("Update", null, 1)); + + _eventOrder.Count.ShouldBe(2); + _eventOrder[0].ShouldBe("ItemUpdating"); + _eventOrder[1].ShouldBe("ItemUpdated"); + } + + [Fact] + public void HandleCommand_Update_Cancellation_PreventsItemUpdated() + { + var cut = Render(@ + @Item.Name + ); + + cut.InvokeAsync(() => theListView.HandleCommand("Update", null, 1)); + + _updatingArgs.ShouldNotBeNull(); + _updatingArgs.Cancel.ShouldBeTrue(); + _updatedArgs.ShouldBeNull(); + } + + [Fact] + public void HandleCommand_Update_UpdatedArgs_HasAffectedRows() + { + var cut = Render(@ + @Item.Name + ); + + cut.InvokeAsync(() => theListView.HandleCommand("Update", null, 0)); + + _updatedArgs.ShouldNotBeNull(); + _updatedArgs.AffectedRows.ShouldBe(1); + _updatedArgs.Exception.ShouldBeNull(); + _updatedArgs.ExceptionHandled.ShouldBeFalse(); + } + + [Fact] + public void HandleCommand_Update_KeepInEditMode_PreservesEditIndex() + { + int editIdx = 2; + var cut = Render(@ + @Item.Name + + ); + + cut.InvokeAsync(() => theListView.HandleCommand("Update", null, 2)); + + _updatedArgs.ShouldNotBeNull(); + _updatedArgs.KeepInEditMode.ShouldBeTrue(); + // EditIndex should remain at 2 since KeepInEditMode was set + theListView.EditIndex.ShouldNotBe(-1); + } + + [Fact] + public void HandleCommand_Update_DefaultKeepInEditMode_ClearsEditIndex() + { + int editIdx = 1; + var cut = Render(@ + @Item.Name + + ); + + cut.InvokeAsync(() => theListView.HandleCommand("Update", null, 1)); + + theListView.EditIndex.ShouldBe(-1); + } + + [Fact] + public void HandleCommand_Update_EventArgsContainItemIndex() + { + var cut = Render(@ + @Item.Name + ); + + cut.InvokeAsync(() => theListView.HandleCommand("Update", null, 3)); + + _updatingArgs.ShouldNotBeNull(); + _updatingArgs.ItemIndex.ShouldBe(3); + } + + // ═════════════════════════════════════════════════════════════════ + // DELETE: ItemDeleting / ItemDeleted + // ═════════════════════════════════════════════════════════════════ + + [Fact] + public void HandleCommand_Delete_FiresItemDeletingBeforeItemDeleted() + { + var cut = Render(@ + @Item.Name + ); + + cut.InvokeAsync(() => theListView.HandleCommand("Delete", null, 0)); + + _eventOrder.Count.ShouldBe(2); + _eventOrder[0].ShouldBe("ItemDeleting"); + _eventOrder[1].ShouldBe("ItemDeleted"); + } + + [Fact] + public void HandleCommand_Delete_Cancellation_PreventsItemDeleted() + { + var cut = Render(@ + @Item.Name + ); + + cut.InvokeAsync(() => theListView.HandleCommand("Delete", null, 0)); + + _deletingArgs.ShouldNotBeNull(); + _deletingArgs.Cancel.ShouldBeTrue(); + _deletedArgs.ShouldBeNull(); + } + + [Fact] + public void HandleCommand_Delete_EventArgsContainItemIndex() + { + var cut = Render(@ + @Item.Name + ); + + cut.InvokeAsync(() => theListView.HandleCommand("Delete", null, 4)); + + _deletingArgs.ShouldNotBeNull(); + _deletingArgs.ItemIndex.ShouldBe(4); + } + + [Fact] + public void HandleCommand_Delete_DeletedArgs_HasAffectedRowsAndNoException() + { + var cut = Render(@ + @Item.Name + ); + + cut.InvokeAsync(() => theListView.HandleCommand("Delete", null, 0)); + + _deletedArgs.ShouldNotBeNull(); + _deletedArgs.AffectedRows.ShouldBe(1); + _deletedArgs.Exception.ShouldBeNull(); + _deletedArgs.ExceptionHandled.ShouldBeFalse(); + } + + // ═════════════════════════════════════════════════════════════════ + // EDIT: ItemEditing + // ═════════════════════════════════════════════════════════════════ + + [Fact] + public void HandleCommand_Edit_EventArgsContainNewEditIndex() + { + var cut = Render(@ + @Item.Name + ); + + cut.InvokeAsync(() => theListView.HandleCommand("Edit", null, 3)); + + _editingArgs.ShouldNotBeNull(); + _editingArgs.NewEditIndex.ShouldBe(3); + _editingArgs.Cancel.ShouldBeFalse(); + theListView.EditIndex.ShouldBe(3); + } + + [Fact] + public void HandleCommand_Edit_ModifiedNewEditIndex_UsesModifiedValue() + { + var cut = Render(@ + @Item.Name + + ); + + cut.InvokeAsync(() => theListView.HandleCommand("Edit", null, 1)); + + // Handler modified NewEditIndex to 4 + theListView.EditIndex.ShouldBe(4); + } + + // ═════════════════════════════════════════════════════════════════ + // CANCEL: ItemCanceling + // ═════════════════════════════════════════════════════════════════ + + [Fact] + public void HandleCommand_Cancel_InEditMode_SetsCancelModeToEdit() + { + int editIdx = 1; + var cut = Render(@ + @Item.Name + + ); + + cut.InvokeAsync(() => theListView.HandleCommand("Cancel", null, 1)); + + _cancelingArgs.ShouldNotBeNull(); + _cancelingArgs.CancelMode.ShouldBe(ListViewCancelMode.CancelingEdit); + _cancelingArgs.ItemIndex.ShouldBe(1); + theListView.EditIndex.ShouldBe(-1); + } + + [Fact] + public void HandleCommand_Cancel_InInsertMode_SetsCancelModeToInsert() + { + InsertItemPosition pos = InsertItemPosition.FirstItem; + var cut = Render(@ + @Item.Name +
Insert
+
); + + cut.InvokeAsync(() => theListView.HandleCommand("Cancel", null, 0)); + + _cancelingArgs.ShouldNotBeNull(); + _cancelingArgs.CancelMode.ShouldBe(ListViewCancelMode.CancelingInsert); + } + + [Fact] + public void HandleCommand_Cancel_Cancellation_PreventsEditIndexReset() + { + int editIdx = 2; + var cut = Render(@ + @Item.Name + + ); + + cut.InvokeAsync(() => theListView.HandleCommand("Cancel", null, 2)); + + _cancelingArgs.ShouldNotBeNull(); + _cancelingArgs.Cancel.ShouldBeTrue(); + // EditIndex should remain since the cancel was vetoed + theListView.EditIndex.ShouldNotBe(-1); + } + + // ═════════════════════════════════════════════════════════════════ + // SORTING: Sorting / Sorted + // ═════════════════════════════════════════════════════════════════ + + [Fact] + public void HandleCommand_Sort_FiresSortingBeforeSorted() + { + var cut = Render(@ + @Item.Name + ); + + cut.InvokeAsync(() => theListView.HandleCommand("Sort", "Name", 0)); + + _eventOrder.Count.ShouldBe(2); + _eventOrder[0].ShouldBe("Sorting"); + _eventOrder[1].ShouldBe("Sorted"); + } + + [Fact] + public void HandleCommand_Sort_Cancellation_PreventsSortedAndPropertyUpdate() + { + var cut = Render(@ + @Item.Name + ); + + cut.InvokeAsync(() => theListView.HandleCommand("Sort", "Name", 0)); + + _sortingArgs.ShouldNotBeNull(); + _sortingArgs.Cancel.ShouldBeTrue(); + _sortedArgs.ShouldBeNull(); + theListView.SortExpression.ShouldBeNull(); + } + + [Fact] + public void HandleCommand_Sort_NewExpression_DefaultsToAscending() + { + SortDirection dir = SortDirection.Ascending; + var cut = Render(@ + @Item.Name + ); + + // Sort by a DIFFERENT expression + cut.InvokeAsync(() => theListView.HandleCommand("Sort", "Price", 0)); + + _sortingArgs.ShouldNotBeNull(); + _sortingArgs.SortExpression.ShouldBe("Price"); + _sortingArgs.SortDirection.ShouldBe(SortDirection.Ascending); + theListView.SortExpression.ShouldBe("Price"); + } + + [Fact] + public void HandleCommand_Sort_SameExpression_TogglesDirection() + { + SortDirection dir = SortDirection.Ascending; + var cut = Render(@ + @Item.Name + ); + + cut.InvokeAsync(() => theListView.HandleCommand("Sort", "Name", 0)); + + _sortingArgs.SortDirection.ShouldBe(SortDirection.Descending); + theListView.SortDirection.ShouldBe(SortDirection.Descending); + } + + [Fact] + public void HandleCommand_Sort_SortedArgs_MatchSortingArgs() + { + var cut = Render(@ + @Item.Name + ); + + cut.InvokeAsync(() => theListView.HandleCommand("Sort", "Name", 0)); + + // Both events receive the same args instance + _sortingArgs.ShouldNotBeNull(); + _sortedArgs.ShouldNotBeNull(); + _sortedArgs.SortExpression.ShouldBe(_sortingArgs.SortExpression); + _sortedArgs.SortDirection.ShouldBe(_sortingArgs.SortDirection); + } + + // ═════════════════════════════════════════════════════════════════ + // PAGING: PagePropertiesChanging / PagePropertiesChanged + // ═════════════════════════════════════════════════════════════════ + + [Fact] + public void SetPageProperties_FiresChangingBeforeChanged() + { + var cut = Render(@ + @Item.Name + ); + + cut.InvokeAsync(() => theListView.SetPageProperties(10, 5)); + + _eventOrder.Count.ShouldBe(2); + _eventOrder[0].ShouldBe("PagePropertiesChanging"); + _eventOrder[1].ShouldBe("PagePropertiesChanged"); + } + + [Fact] + public void SetPageProperties_EventArgsContainCorrectValues() + { + var cut = Render(@ + @Item.Name + ); + + cut.InvokeAsync(() => theListView.SetPageProperties(20, 10)); + + _pageChangingArgs.ShouldNotBeNull(); + _pageChangingArgs.StartRowIndex.ShouldBe(20); + _pageChangingArgs.MaximumRows.ShouldBe(10); + theListView.StartRowIndex.ShouldBe(20); + theListView.MaximumRows.ShouldBe(10); + } + + [Fact] + public void SetPageProperties_ZeroValues_Accepted() + { + var cut = Render(@ + @Item.Name + ); + + cut.InvokeAsync(() => theListView.SetPageProperties(0, 0)); + + _pageChangingArgs.ShouldNotBeNull(); + _pageChangingArgs.StartRowIndex.ShouldBe(0); + _pageChangingArgs.MaximumRows.ShouldBe(0); + _pageChangedFired.ShouldBeTrue(); + } + + // ═════════════════════════════════════════════════════════════════ + // SELECTION: SelectedIndexChanging / SelectedIndexChanged + // ═════════════════════════════════════════════════════════════════ + + [Fact] + public void HandleCommand_Select_FiresChangingBeforeChanged() + { + var cut = Render(@ + @Item.Name + ); + + cut.InvokeAsync(() => theListView.HandleCommand("Select", null, 1)); + + _eventOrder.Count.ShouldBe(2); + _eventOrder[0].ShouldBe("SelectedIndexChanging"); + _eventOrder[1].ShouldBe("SelectedIndexChanged"); + } + + [Fact] + public void HandleCommand_Select_Cancellation_PreventsSelectedIndexChanged() + { + var cut = Render(@ + @Item.Name + ); + + cut.InvokeAsync(() => theListView.HandleCommand("Select", null, 2)); + + _selectingArgs.ShouldNotBeNull(); + _selectingArgs.Cancel.ShouldBeTrue(); + _selectedChangedFired.ShouldBeFalse(); + theListView.SelectedIndex.ShouldBe(0); + } + + [Fact] + public void HandleCommand_Select_ModifiedIndex_UsesModifiedNewSelectedIndex() + { + var cut = Render(@ + @Item.Name + ); + + cut.InvokeAsync(() => theListView.HandleCommand("Select", null, 1)); + + // Handler set NewSelectedIndex to 99 + theListView.SelectedIndex.ShouldBe(99); + _selectedChangedFired.ShouldBeTrue(); + } + + [Fact] + public void HandleCommand_Select_EventArgsContainRequestedIndex() + { + var cut = Render(@ + @Item.Name + ); + + cut.InvokeAsync(() => theListView.HandleCommand("Select", null, 3)); + + _selectingArgs.ShouldNotBeNull(); + _selectingArgs.NewSelectedIndex.ShouldBe(3); + } + + // ═════════════════════════════════════════════════════════════════ + // LAYOUT CREATED + // ═════════════════════════════════════════════════════════════════ + + [Fact] + public void LayoutCreated_FiresWhenItemsExist() + { + var cut = Render(@ + @Item.Name + ); + + _layoutCreatedFired.ShouldBeTrue(); + } + + [Fact] + public void LayoutCreated_DoesNotFireWhenItemsEmpty() + { + Widget[] emptyItems = Array.Empty(); + var cut = Render(@ + @Item.Name +
No data
+
); + + _layoutCreatedFired.ShouldBeFalse(); + } + + [Fact] + public void LayoutCreated_FiresWithCustomLayoutTemplate() + { + var cut = Render(@ + @Item.Name + +
@itemPlaceholder
+
+
); + + _layoutCreatedFired.ShouldBeTrue(); + cut.FindAll("div.custom-layout").Count.ShouldBe(1); + } + + // ═════════════════════════════════════════════════════════════════ + // ITEM CREATED + // ═════════════════════════════════════════════════════════════════ + + [Fact] + public async Task ItemCreated_FiresOnFirstRender() + { + var cut = Render(@ + @Item.Name + ); + + // 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(@ + @Item.Name + ); + + _dataBoundCount.ShouldBeGreaterThan(0); + _itemDataBoundCount.ShouldBeGreaterThanOrEqualTo(Widget.SimpleWidgetList.Length); + } + + [Fact] + public void DataBound_ItemDataBound_FiresPerItem() + { + var cut = Render(@ + @Item.Name + ); + + // 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(@ + @Item.Name + ); + + 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(@ + @Item.Name + ); + + cut.InvokeAsync(() => theListView.HandleCommand("EDIT", null, 0)); + + _editingArgs.ShouldNotBeNull(); + _editingArgs.NewEditIndex.ShouldBe(0); + } + + [Fact] + public void HandleCommand_Sort_PassesCommandArgumentAsSortExpression() + { + var cut = Render(@ + @Item.Name + ); + + cut.InvokeAsync(() => theListView.HandleCommand("Sort", "Price", 0)); + + _sortingArgs.ShouldNotBeNull(); + _sortingArgs.SortExpression.ShouldBe("Price"); + } + + [Fact] + public void HandleCommand_Sort_NullArgument_UsesEmptyString() + { + var cut = Render(@ + @Item.Name + ); + + cut.InvokeAsync(() => theListView.HandleCommand("Sort", null, 0)); + + _sortingArgs.ShouldNotBeNull(); + _sortingArgs.SortExpression.ShouldBe(string.Empty); + } + + // ═════════════════════════════════════════════════════════════════ + // EDGE CASES + // ═════════════════════════════════════════════════════════════════ + + [Fact] + public void HandleCommand_Delete_ThenInsert_BothEventPairsFire() + { + var cut = Render(@ + @Item.Name + ); + + cut.InvokeAsync(() => theListView.HandleCommand("Delete", null, 0)); + _deletingArgs.ShouldNotBeNull(); + _deletedArgs.ShouldNotBeNull(); + + cut.InvokeAsync(() => theListView.HandleCommand("Insert", null, 0)); + _insertingArgs.ShouldNotBeNull(); + _insertedArgs.ShouldNotBeNull(); + } + + [Fact] + public void HandleCommand_Edit_ThenUpdate_FullCrudCycle() + { + var cut = Render(@ + @Item.Name + + ); + + // Edit item 1 + cut.InvokeAsync(() => theListView.HandleCommand("Edit", null, 1)); + theListView.EditIndex.ShouldBe(1); + + // Update item 1 + cut.InvokeAsync(() => theListView.HandleCommand("Update", null, 1)); + theListView.EditIndex.ShouldBe(-1); + _updatedArgs.ShouldNotBeNull(); + } + + [Fact] + public void HandleCommand_Edit_ThenCancel_FullCrudCycle() + { + var cut = Render(@ + @Item.Name + + ); + + // Edit item 2 + cut.InvokeAsync(() => theListView.HandleCommand("Edit", null, 2)); + theListView.EditIndex.ShouldBe(2); + + // Cancel + cut.InvokeAsync(() => theListView.HandleCommand("Cancel", null, 2)); + theListView.EditIndex.ShouldBe(-1); + _cancelingArgs.CancelMode.ShouldBe(ListViewCancelMode.CancelingEdit); + } + + // ═════════════════════════════════════════════════════════════════ + // Event Handlers + // ═════════════════════════════════════════════════════════════════ + + // Order-tracking handlers + void OnInsertingTrackOrder(ListViewInsertEventArgs args) { _insertingArgs = args; _eventOrder.Add("ItemInserting"); } + void OnInsertedTrackOrder(ListViewInsertedEventArgs args) { _insertedArgs = args; _eventOrder.Add("ItemInserted"); } + void OnUpdatingTrackOrder(ListViewUpdateEventArgs args) { _updatingArgs = args; _eventOrder.Add("ItemUpdating"); } + void OnUpdatedTrackOrder(ListViewUpdatedEventArgs args) { _updatedArgs = args; _eventOrder.Add("ItemUpdated"); } + void OnDeletingTrackOrder(ListViewDeleteEventArgs args) { _deletingArgs = args; _eventOrder.Add("ItemDeleting"); } + void OnDeletedTrackOrder(ListViewDeletedEventArgs args) { _deletedArgs = args; _eventOrder.Add("ItemDeleted"); } + void OnSortingTrackOrder(ListViewSortEventArgs args) { _sortingArgs = args; _eventOrder.Add("Sorting"); } + void OnSortedTrackOrder(ListViewSortEventArgs args) { _sortedArgs = args; _eventOrder.Add("Sorted"); } + void OnSelectingTrackOrder(ListViewSelectEventArgs args) { _selectingArgs = args; _eventOrder.Add("SelectedIndexChanging"); } + void OnSelectedTrackOrder(EventArgs args) { _selectedChangedFired = true; _eventOrder.Add("SelectedIndexChanged"); } + void OnPageChangingTrackOrder(ListViewPagePropertiesChangingEventArgs args) { _pageChangingArgs = args; _eventOrder.Add("PagePropertiesChanging"); } + void OnPageChangedTrackOrder(EventArgs args) { _pageChangedFired = true; _eventOrder.Add("PagePropertiesChanged"); } + + // Cancellation handlers + void OnInsertingCancel(ListViewInsertEventArgs args) { _insertingArgs = args; args.Cancel = true; } + void OnUpdatingCancel(ListViewUpdateEventArgs args) { _updatingArgs = args; args.Cancel = true; } + void OnDeletingCancel(ListViewDeleteEventArgs args) { _deletingArgs = args; args.Cancel = true; } + void OnCancelingCancel(ListViewCancelEventArgs args) { _cancelingArgs = args; args.Cancel = true; } + void OnSortingCancel(ListViewSortEventArgs args) { _sortingArgs = args; args.Cancel = true; } + void OnSelectingCancel(ListViewSelectEventArgs args) { _selectingArgs = args; args.Cancel = true; } + + // Simple capture handlers + void OnEditing(ListViewEditEventArgs args) => _editingArgs = args; + void OnCanceling(ListViewCancelEventArgs args) => _cancelingArgs = args; + void OnDeleting(ListViewDeleteEventArgs args) => _deletingArgs = args; + void OnDeleted(ListViewDeletedEventArgs args) => _deletedArgs = args; + void OnInserting(ListViewInsertEventArgs args) => _insertingArgs = args; + void OnInserted(ListViewInsertedEventArgs args) => _insertedArgs = args; + void OnUpdating(ListViewUpdateEventArgs args) => _updatingArgs = args; + void OnUpdated(ListViewUpdatedEventArgs args) => _updatedArgs = args; + void OnCommand(ListViewCommandEventArgs args) => _commandArgs = args; + void OnSorting(ListViewSortEventArgs args) => _sortingArgs = args; + void OnSorted(ListViewSortEventArgs args) => _sortedArgs = args; + void OnSelecting(ListViewSelectEventArgs args) => _selectingArgs = args; + void OnSelectedChanged(EventArgs args) => _selectedChangedFired = true; + void OnPageChanging(ListViewPagePropertiesChangingEventArgs args) => _pageChangingArgs = args; + void OnPageChanged(EventArgs args) => _pageChangedFired = true; + void OnLayoutCreated(EventArgs args) => _layoutCreatedFired = true; + void OnItemCreated() => _itemCreatedFired = true; + void OnDataBound(EventArgs args) => _dataBoundCount++; + void OnItemDataBound(ListViewItemEventArgs args) => _itemDataBoundCount++; + + // Modifier handlers + void OnEditingModifyIndex(ListViewEditEventArgs args) { _editingArgs = args; args.NewEditIndex = 4; } + void OnUpdatedKeepEditMode(ListViewUpdatedEventArgs args) { _updatedArgs = args; args.KeepInEditMode = true; } + void OnSelectingModifyIndex(ListViewSelectEventArgs args) { _selectingArgs = args; args.NewSelectedIndex = 99; } +}