diff --git a/.ai-team/agents/beast/history.md b/.ai-team/agents/beast/history.md index 25f8aa49a..e3f7e3596 100644 --- a/.ai-team/agents/beast/history.md +++ b/.ai-team/agents/beast/history.md @@ -50,3 +50,9 @@ Team update (2026-02-23): AccessKey/ToolTip must be added to BaseStyledComponent decided by Beast, Cyclops Team update (2026-02-23): Chart implementation architecture consolidated (10 decisions) decided by Cyclops, Forge Team update (2026-02-23): DetailsView/PasswordRecovery branch (sprint3) must be merged forward decided by Forge + + Team update (2026-02-23): BaseListControl introduced docs should reflect shared base for list controls decided by Cyclops + Team update (2026-02-23): Label AssociatedControlID switches rendered element document accessibility benefit decided by Cyclops + Team update (2026-02-23): Login controls now inherit BaseStyledComponent update docs for outer style support decided by Rogue, Cyclops + Team update (2026-02-23): Milestone 6 Work Plan ratified 54 WIs, Beast assigned branding (UI-11) and docs (UI-12) decided by Forge + Team update (2026-02-23): Menu Orientation requires Razor local variable workaround document this pattern decided by Jubilee diff --git a/.ai-team/agents/colossus/history.md b/.ai-team/agents/colossus/history.md index 87f826546..bf4203f17 100644 --- a/.ai-team/agents/colossus/history.md +++ b/.ai-team/agents/colossus/history.md @@ -1,144 +1,18 @@ # Colossus — History -## 2026-02-10: Initial integration test audit + -- Audited all 74 sample page routes against existing smoke tests -- Added 32 missing smoke test `[InlineData]` entries in `ControlSampleTests.cs` -- Added 4 interaction tests for Sprint 2 components (MultiView, ChangePassword, CreateUserWizard, Localize) -- Fixed Calendar sample page CS1503 errors (bare enum values → fully qualified `CalendarSelectionMode.X`) +## Summary: Milestones 1–3 Integration Tests (2026-02-10 through 2026-02-12) -## 2026-02-10: Sprint 3 — DetailsView and PasswordRecovery tests +Audited 74 sample routes, added 32 missing smoke tests. Added interaction tests for Sprint 2 (MultiView, ChangePassword, CreateUserWizard, Localize) and Sprint 3 (DetailsView paging/edit, PasswordRecovery 3-step flow). Fixed 7 pre-existing failures: missing `@using BlazorWebFormsComponents.LoginControls` on ChangePassword/CreateUserWizard, external placeholder URLs → local SVGs, duplicate ImageMap InlineData, Calendar console error filter, TreeView broken image path. 116 integration tests passing. -- Added smoke test `[InlineData("/ControlSamples/DetailsView")]` under Data Controls in `ControlSampleTests.cs` -- Added smoke test `[InlineData("/ControlSamples/PasswordRecovery")]` under Login Controls in `ControlSampleTests.cs` -- Added 3 interaction tests for DetailsView in `InteractiveComponentTests.cs`: - - `DetailsView_RendersTable_WithAutoGeneratedRows` — verifies table renders with field rows - - `DetailsView_Paging_ChangesRecord` — verifies pager links navigate between records - - `DetailsView_EditButton_SwitchesMode` — verifies Edit link switches to edit mode with Update/Cancel links -- Added 2 interaction tests for PasswordRecovery in `InteractiveComponentTests.cs`: - - `PasswordRecovery_Step1Form_RendersUsernameInput` — verifies Step 1 username input and submit button render - - `PasswordRecovery_UsernameSubmit_TransitionsToQuestionStep` — verifies username submission fires handler and transitions -- Build verified: 0 errors, 0 warnings -- Key learnings: - - DetailsView sample has 3 sections: auto-generated rows, paging (with `PageIndexChanged` counter), and edit mode (with `ModeChanging`/`ItemUpdating` status message) - - PasswordRecovery sample has 3 instances: default (with all 3 handlers), custom text, and help link. First instance has status message feedback via `_statusMessage` - - DetailsView renders as `` with `` rows per field — consistent with Web Forms output - - PasswordRecovery Step 1 uses `input[type='text']` for username, button for submit +## Summary: Milestone 4 Chart + Utility Tests (2026-02-12) -📌 Team update (2026-02-12): Sprint 3 gate review — DetailsView and PasswordRecovery APPROVED. 50/53 components (94%). Library effectively feature-complete. — decided by Forge +Chart: 8 smoke tests + 11 canvas tests + 19 enhanced visual tests (dimensions, Chart.js initialization, multi-series datasets, canvas context). Used `WaitUntilState.DOMContentLoaded` for Chart tests. DataBinder + ViewState: 4 utility feature tests (Eval rendering, ViewState counter increment). Enhanced Chart tests use `BoundingBoxAsync()`, `page.EvaluateAsync` for Chart.js internals, ±10px tolerance for dimensions. Total: 120 integration tests. - Team update (2026-02-12): Milestone 4 planned Chart component with Chart.js via JS interop. 8 work items, design review required before implementation. decided by Forge + Squad - -## Learnings - -### 2026-02-12: Milestone 4 — Chart integration tests (WI-7) - -- Added 8 Chart smoke tests as a dedicated `ChartControl_Loads_AndRendersContent` Theory in `ControlSampleTests.cs` - - Follows the Menu pattern: separate Theory with its own verify method (`VerifyChartPageLoads`) that tolerates JS interop console errors but checks for page errors - - Routes: `/ControlSamples/Chart`, `Chart/Line`, `Chart/Bar`, `Chart/Pie`, `Chart/Area`, `Chart/Doughnut`, `Chart/Scatter`, `Chart/StackedColumn` -- Added 4 interactive tests in `InteractiveComponentTests.cs`: - - `Chart_DefaultPage_RendersCanvas` — verifies `` on Column (default) page - - `Chart_LinePage_RendersCanvas` — verifies `` on Line page - - `Chart_PiePage_RendersCanvas` — verifies `` on Pie page - - `Chart_AllTypes_RenderCanvas` — Theory test covering all 8 routes for `` element -- All 19 Chart tests pass (8 smoke + 3 individual canvas + 8 theory canvas) -- Used `WaitUntilState.DOMContentLoaded` instead of `NetworkIdle` for Chart tests — Chart.js JS interop can keep network busy -- Key learnings: - - Chart component renders `
` wrapping a `` element (in `Chart.razor`), so `` is always in the DOM even before Chart.js initializes - - Chart pages use JS interop (`ChartJsInterop.cs`) — console errors are expected if Chart.js CDN/bundle isn't fully loaded; page errors are not - - Pre-existing test suite has 97 failures on non-Chart tests due to ASP.NET structured log console errors (`[timestamp] Error:`) being caught by `Assert.Empty(consoleErrors)` — these are unrelated to Chart work - - - Team update (2026-02-23): DetailsView/PasswordRecovery branch (sprint3) must be merged forward decided by Forge - Team update (2026-02-23): AccessKey/ToolTip must be added to BaseStyledComponent decided by Beast, Cyclops -## 2026-02-12: Boy Scout rule — fixed 7 pre-existing integration test failures - -Fixed all 7 failing integration tests. 111/111 passing after fixes. - -### Failure 1 & 2: ChangePassword + CreateUserWizard form fields not found -- **Root cause:** The sample pages at `ChangePassword/Index.razor` and `CreateUserWizard/Index.razor` were MISSING `@using BlazorWebFormsComponents.LoginControls`. The components rendered as raw HTML custom elements (``) instead of Blazor components. PasswordRecovery worked because it had the import. -- **Fix:** Added `@using BlazorWebFormsComponents.LoginControls` to both sample pages. Also updated test selectors from `input[type='password']` / `input[type='text']` to ID-based selectors (`input[id$='_CurrentPassword']`, etc.) with `WaitForAsync` for circuit establishment timing. - -### Failure 3 & 4 & 7: Image, ImageMap external placeholder URLs unreachable -- **Root cause:** Sample pages referenced `https://via.placeholder.com/...` URLs which are unreachable in the test environment. -- **Fix:** Created 8 local SVG placeholder images in `wwwroot/img/` (placeholder-150x100.svg, placeholder-80x80.svg, etc.) and replaced all external URLs in both `Image/Index.razor` and `ImageMap/Index.razor`. - -### Failure 4 (additional): ImageMap duplicate InlineData -- **Root cause:** ImageMap had entries in BOTH `EditorControl_Loads_WithoutErrors` and `NavigationControl_Loads_WithoutErrors`. Per team decisions, ImageMap is a Navigation Control. -- **Fix:** Removed `[InlineData("/ControlSamples/ImageMap")]` from EditorControl test theory. - -### Failure 5: Calendar console errors -- **Root cause:** ASP.NET Core structured log messages (timestamps like `[2026-02-12T16:00:34.529...]`) forwarded to browser console as "error" level. Calendar component and sample page have NO bugs — these are benign framework messages from Blazor's SignalR circuit. -- **Fix:** Added regex filter in `VerifyPageLoadsWithoutErrors` to exclude messages matching `^\[\d{4}-\d{2}-\d{2}T` pattern. - -### Failure 6: TreeView/Images broken image path -- **Root cause:** `ImageUrl="/img/C#.png"` but actual file is `CSharp.png`. -- **Fix:** Changed to `ImageUrl="/img/CSharp.png"`. - -## Learnings - -- **Missing @using is silent:** When a Blazor component can't be resolved, it renders as a raw HTML custom element with no error. This is extremely hard to catch without integration tests that verify actual DOM content. -- **LoginControls namespace:** Components in `BlazorWebFormsComponents.LoginControls` require an explicit `@using` — the root `@using BlazorWebFormsComponents` in `_Imports.razor` doesn't cover sub-namespaces. PasswordRecovery had it; ChangePassword and CreateUserWizard didn't. -- **ASP.NET Core log messages in browser console:** Blazor Server forwards structured log output to the browser console. These appear as "error" type messages starting with ISO 8601 timestamps. Tests must filter these to avoid false positives. -- **SVG placeholders:** Simple inline SVG files are ideal test-safe replacements for external placeholder image services. They're just XML text, always available, and don't require network access. - -📌 Team update (2026-02-12): Boy scout fixes logged — 7 pre-existing integration test failures fixed, 111/111 integration tests + 797/797 bUnit tests all green. Commit a4d17f5 on sprint3/detailsview-passwordrecovery. — logged by Scribe - -## 2026-02-12: DetailsView edit mode input textbox verification test - -- Added `DetailsView_EditMode_RendersInputTextboxes` integration test in `InteractiveComponentTests.cs` -- Test verifies the full edit mode lifecycle: - 1. Navigates to `/ControlSamples/DetailsView` and clicks the Edit link - 2. Waits for "Mode changing" status message (Blazor Server DOM update) - 3. Asserts at least 3 `` elements appear (CustomerID, FirstName, LastName, CompanyName fields) - 4. Asserts Update and Cancel links are present via `GetByRole(AriaRole.Link, ...)` - 5. Clicks Cancel and verifies return to ReadOnly mode — no text inputs remain -- This test catches the known bug where edit mode shows command row changes (Edit→Update/Cancel) but leaves field values as plain text instead of rendering `` textboxes -- Cyclops is fixing the component in parallel — this test will pass once the fix lands -- Key selector: `input[type='text']` works because the fix uses raw HTML `` not Blazor's `` (which omits `type="text"` in .NET 10) - -📌 Team update (2026-02-12): DetailsView auto-generated fields must render in Edit/Insert mode — decided by Cyclops - -## 2026-02-12: Sprint 3 missing integration tests — full interactive coverage - -- Added 4 new integration tests in `InteractiveComponentTests.cs` for Sprint 3 components: - - `DetailsView_EmptyData_ShowsMessage` — verifies `EmptyDataText="No customers found."` renders when data source is empty. Uses `GetByRole(AriaRole.Cell)` to avoid matching code sample `
` blocks.
-  - `PasswordRecovery_AnswerSubmit_TransitionsToSuccessStep` — full 3-step flow test: username → question → success. Uses ID-specific selectors (`#PasswordRecovery1_UserName`, `#PasswordRecovery1_Answer`, `#PasswordRecovery1_SubmitButton`) to target the first PasswordRecovery instance. Uses `PressSequentiallyAsync` + Tab for Blazor Server `InputText` binding on re-rendered DOM. Verifies "Recovery email sent successfully" status (the final status after both `OnVerifyingAnswer` and `OnSendingMail` handlers fire).
-  - `PasswordRecovery_HelpLink_Renders` — verifies the 3rd PasswordRecovery renders a help link `` with text "Need more help?" and correct href.
-  - `PasswordRecovery_CustomText_Applies` — verifies the 2nd PasswordRecovery renders custom `UserNameTitleText="Password Reset"` in a table cell and custom `UserNameLabelText="Email:"` in the label element.
-- All 116 integration tests passing (112 existing + 4 new), 0 failures.
-- Key learnings:
-  - Pages with code sample `
` blocks cause strict mode violations when using `text=` locators — the same text appears in both the rendered component and the code sample. Use role-based or ID-based selectors instead.
-  - Pages with multiple PasswordRecovery instances require ID-specific selectors (`#PasswordRecovery1_SubmitButton`) not suffix selectors (`input[id$='_SubmitButton']`) to avoid strict mode violations.
-  - After a multi-step Blazor Server form flow, the final `_statusMessage` reflects the LAST handler that sets it. For PasswordRecovery step 2→3, `OnVerifyingAnswer` sets one message, then `OnSendingMail` overwrites it — test must assert on the final value.
-  - `PressSequentiallyAsync` + Tab blur works reliably for Blazor Server `InputText` binding on dynamically re-rendered DOM elements.
-
-## 2026-02-12: DataBinder and ViewState utility feature integration tests
-
-- Added smoke tests in `ControlSampleTests.cs`:
-  - New "Utility Features" theory section with `[InlineData("/ControlSamples/DataBinder")]` and `[InlineData("/ControlSamples/ViewState")]`
-- Added 2 interaction tests in `InteractiveComponentTests.cs`:
-  - `DataBinder_Eval_RendersProductData` — verifies the DataBinder sample page renders product data ("Laptop Stand", "USB-C Hub", "Mechanical Keyboard") via Repeater with DataBinder.Eval(). Asserts at least 3 `
` rows present. - - `ViewState_Counter_IncrementsOnClick` — verifies the ViewState sample page's "Click Me (ViewState)" button increments a counter stored in ViewState. Clicks twice and verifies counter reaches 1 then 2. -- Build: 0 errors. All 120 integration tests passing (116 existing + 4 new), 0 failures. -- Key learnings: - - DataBinder sample uses `OnAfterRender(firstRender)` to call `DataBind()` on 4 Repeater instances — data only appears after first render, but NetworkIdle wait handles this. - - ViewState sample button text "Click Me (ViewState)" distinguishes it from the "Click Me (Property)" button in section 3. Used `GetByRole(AriaRole.Button, new() { Name = "Click Me (ViewState)" })` for precise targeting. - - Both pages include `
` blocks with sample code — assertions use `page.ContentAsync()` for text presence rather than strict locators to avoid matching code samples vs rendered content where appropriate.
-### 2026-02-12: Enhanced Chart visual appearance tests
-
-- Added 5 stronger Chart tests in `InteractiveComponentTests.cs` to verify chart appearance:
-  - `Chart_RendersCanvas_WithDimensions` — verifies canvas has non-zero width/height via BoundingBox
-  - `Chart_AllTypes_HaveExpectedContainerDimensions` — Theory test verifying all 8 chart types have container dimensions matching ChartWidth/ChartHeight parameters (600x400 for most, 500x400 for Pie/Doughnut)
-  - `Chart_ChartJsLibrary_IsInitialized` — verifies Chart.js global is loaded and has at least one chart instance via `Chart.instances`
-  - `Chart_Line_MultipleSeries_RenderMultipleDatasets` — verifies Line chart's 2 series (NY/LA temps) produce 2 datasets via `Chart.instances[0].data.datasets.length`
-  - `Chart_AllTypes_CanvasHasRenderingContext` — Theory test verifying all 8 chart types have a 2D rendering context
-- Total Chart tests: 38 (8 smoke + 11 basic canvas + 19 enhanced visual)
-- Build: 0 errors, 0 warnings
-- Test patterns:
-  - Use `LocatorWaitForOptions { State = WaitForSelectorState.Visible }` instead of `Expect()` (class doesn't inherit from `PageTest`)
-  - Use `page.EvaluateAsync` to query Chart.js internals (`Chart.instances`, dataset counts, etc.)
-  - Use `BoundingBoxAsync()` to verify element dimensions
-  - Allow ±10px tolerance on dimension checks for border/padding differences
+**Key patterns:** `LocatorWaitForOptions` instead of `Expect()` (no PageTest inheritance). `PressSequentiallyAsync` + Tab for Blazor Server InputText binding. ID-specific selectors for multi-instance pages. Filter ISO 8601 timestamps from console errors.
 
+📌 Team update (2026-02-12): LoginControls sample pages MUST include `@using BlazorWebFormsComponents.LoginControls`. Never use external image URLs. — Colossus
 
+ Team update (2026-02-23): Milestone 6 Work Plan ratified  54 WIs across P0/P1/P2 tiers  decided by Forge
+ Team update (2026-02-23): UI overhaul requested  Colossus assigned integration tests (UI-9)  decided by Jeffrey T. Fritz
diff --git a/.ai-team/agents/cyclops/history.md b/.ai-team/agents/cyclops/history.md
index 1a8f37e20..9dc585010 100644
--- a/.ai-team/agents/cyclops/history.md
+++ b/.ai-team/agents/cyclops/history.md
@@ -8,73 +8,35 @@
 ## Learnings
 
 
+
 
-- **Enum pattern:** Every Web Forms enum gets a file in `src/BlazorWebFormsComponents/Enums/`. Use the namespace `BlazorWebFormsComponents.Enums`. Enum values should match the original .NET Framework values and include explicit integer assignments. Older enums use `namespace { }` block syntax; newer ones use file-scoped `namespace;` syntax — either is accepted.
-- **Calendar component:** Lives at `src/BlazorWebFormsComponents/Calendar.razor` and `Calendar.razor.cs`. Inherits from `BaseStyledComponent`. Event arg classes (`CalendarDayRenderArgs`, `CalendarMonthChangedArgs`) are defined inline in the `.razor.cs` file.
-- **TableCaptionAlign enum already exists** at `src/BlazorWebFormsComponents/Enums/TableCaptionAlign.cs` — reusable across any table-based component (Calendar, Table, GridView, etc.).
-- **Blazor EventCallback and sync rendering:** Never use `.GetAwaiter().GetResult()` on `EventCallback.InvokeAsync()` during render — it can deadlock. Use fire-and-forget `_ = callback.InvokeAsync(args)` for render-time event hooks like `OnDayRender`.
-- **Pre-existing test infrastructure issue:** The test project on `dev` has a broken `AddXUnit` reference in `BlazorWebFormsTestContext.cs` — this is not caused by component changes.
-- **FileUpload must use InputFile internally:** Raw `` with `@onchange` receives `ChangeEventArgs` (no file data). Must use Blazor's `InputFile` component which provides `InputFileChangeEventArgs` with `IBrowserFile` objects. The `@using Microsoft.AspNetCore.Components.Forms` directive is needed in the `.razor` file since `_Imports.razor` only imports `Microsoft.AspNetCore.Components.Web`.
-- **Path security in file save operations:** `Path.Combine` silently drops earlier arguments if a later argument is rooted (e.g., `Path.Combine("uploads", "/etc/passwd")` returns `/etc/passwd`). Always use `Path.GetFileName()` to sanitize filenames and validate resolved paths with `Path.GetFullPath()` + `StartsWith()` check.
-- **PageService event handler catch pattern:** In `Page.razor.cs`, async event handlers that call `InvokeAsync(StateHasChanged)` should catch `ObjectDisposedException` (not generic `Exception`) — the component may be disposed during navigation while an event is still in flight. This is the standard Blazor pattern for disposed-component safety.
-- **Test dead code:** Code scanning flags unused variable assignments in test files. Use `_ = expr` discard for side-effect-only calls, and remove `var` assignments where the result is never asserted.
-- **ImageMap base class fix:** ImageMap inherits `BaseStyledComponent` (not `BaseWebFormsComponent`), matching the Web Forms `ImageMap → Image → WebControl` hierarchy. This gives it CssClass, Style, Font, BackColor, etc. The `@inherits` directive in `.razor` must match the code-behind.
-- **Instance-based IDs for generated HTML IDs:** Never use `static` counters for internal element IDs (like map names) — they leak across test runs and create non-deterministic output. Use `Guid.NewGuid()` as a field initializer instead.
-- **ImageAlign rendering:** `.ToString().ToLower()` on `ImageAlign` enum values produces the correct Web Forms output (`absbottom`, `absmiddle`, `texttop`). No custom mapping needed.
-- **Enabled propagation pattern:** When `Enabled=false` on a styled component, interactive child elements (like `` in ImageMap) should render as inactive (nohref, no onclick). Check `Enabled` from `BaseWebFormsComponent` — it defaults to `true`.
-- **PasswordRecovery component:** Lives at `src/BlazorWebFormsComponents/LoginControls/PasswordRecovery.razor` and `.razor.cs`. Inherits `BaseWebFormsComponent` (matching ChangePassword/CreateUserWizard pattern, not BaseStyledComponent). Uses 3-step int tracking: 0=UserName, 1=Question, 2=Success. Each step has its own `EditForm` wrapping. Created `SuccessTextStyle` sub-component, `MailMessageEventArgs`, and `SendMailErrorEventArgs` event args classes.
-- **Login controls inherit BaseWebFormsComponent, not BaseStyledComponent:** Despite the Web Forms hierarchy (CompositeControl → WebControl), all existing login controls (Login, ChangePassword, CreateUserWizard) inherit `BaseWebFormsComponent` and manage styles via CascadingParameters. New login controls should follow this established pattern.
-- **SubmitButtonStyle maps to LoginButtonStyle cascading name:** PasswordRecovery uses `SubmitButtonStyle` as its internal property name but cascades via `Name="LoginButtonStyle"` to reuse the existing `LoginButtonStyle` sub-component. This is the correct approach when the Web Forms property name differs from the existing cascading name.
-- **EditForm per step for multi-step login controls:** PasswordRecovery wraps each step in its own `EditForm` (unlike ChangePassword which wraps everything in one). This is necessary because each step has different submit handlers and different model fields being validated.
-- **DetailsView component:** Lives at `src/BlazorWebFormsComponents/DetailsView.razor` and `DetailsView.razor.cs`. Inherits `DataBoundComponent` (same as GridView/FormView). Renders a single record as `
` with one `` per field. Auto-generates rows via reflection when `AutoGenerateRows=true`. Supports paging across items, mode switching (ReadOnly/Edit/Insert), and command row with Edit/Delete/New/Update/Cancel buttons. -- **DetailsViewMode enum:** Separate from `FormViewMode` — Web Forms has both as distinct enums with identical values (ReadOnly=0, Edit=1, Insert=2). Created at `src/BlazorWebFormsComponents/Enums/DetailsViewMode.cs` using file-scoped namespace. -- **DetailsView event args:** All event arg classes live in `src/BlazorWebFormsComponents/DetailsViewEventArgs.cs`. Includes `DetailsViewCommandEventArgs`, `DetailsViewDeleteEventArgs`, `DetailsViewDeletedEventArgs`, `DetailsViewInsertEventArgs`, `DetailsViewInsertedEventArgs`, `DetailsViewUpdateEventArgs`, `DetailsViewUpdatedEventArgs`, `DetailsViewModeEventArgs`. These parallel FormView's event args but are separate types, matching Web Forms. -- **DetailsView field abstraction:** Uses `DetailsViewField` abstract base class and `DetailsViewAutoField` internal class for auto-generated fields. Field definitions can be added via `Fields` RenderFragment child content. External field components can register via `AddField`/`RemoveField` methods using a `DetailsViewFieldCollection` cascading value. -- **Data control paging pattern:** DetailsView uses `PageIndex` (zero-based) to index into the `Items` collection. Each page shows one record. Pager row renders numeric page links. `PageChangedEventArgs` is reused from the existing shared class. -- **DetailsView edit/insert mode rendering:** `DetailsViewAutoField.GetValue()` must respect the `DetailsViewMode` parameter. In `Edit` mode, render `` pre-filled with the property value. In `Insert` mode, render `` (empty). In `ReadOnly` mode, render plain text. Uses `RenderTreeBuilder.OpenElement/AddAttribute/CloseElement` pattern for input elements. +### Summary: Milestones 1–3 Implementation (2026-02-10 through 2026-02-12) -📌 Team update(2026-02-10): FileUpload needs InputFile integration — @onchange won't populate file data. Ship-blocking bug. — decided by Forge -📌 Team update (2026-02-10): ImageMap base class must be BaseStyledComponent, not BaseWebFormsComponent — decided by Forge -📌 Team update (2026-02-10): PRs #328 (ASCX CLI) and #309 (VS Snippets) shelved indefinitely — decided by Jeffrey T. Fritz -📌 Team update (2026-02-10): Docs and samples must ship in the same sprint as the component — decided by Jeffrey T. Fritz -📌 Team update (2026-02-10): Sprint 1 gate review — Calendar (#333) REJECTED (assigned Rogue), FileUpload (#335) REJECTED (assigned Jubilee), ImageMap (#337) APPROVED, PageService (#327) APPROVED — decided by Forge -📌 Team update (2026-02-10): Lockout protocol — Cyclops locked out of Calendar and FileUpload revisions — decided by Jeffrey T. Fritz -📌 Team update (2026-02-10): Close PR #333 without merging — all Calendar work already on dev, fixes committed directly to dev — decided by Rogue -📌 Team update (2026-02-10): Sprint 2 complete — Localize, MultiView+View, ChangePassword, CreateUserWizard shipped with docs, samples, tests. 709 tests passing. 41/53 components done. — decided by Squad -📌 Team update (2026-02-11): Sprint 3 scope: DetailsView + PasswordRecovery. Chart/Substitution/Xml deferred. 48/53 → target 50/53. — decided by Forge -📌 Team update (2026-02-11): Colossus added as dedicated integration test engineer. Rogue retains bUnit unit tests. — decided by Jeffrey T. Fritz -📌 Team update (2026-02-12): Sprint 3 gate review — DetailsView and PasswordRecovery APPROVED. 50/53 components (94%). — decided by Forge +Built Calendar (enum fix, async events), ImageMap (BaseStyledComponent, Guid IDs, Enabled propagation), FileUpload (InputFile integration, path sanitization), PasswordRecovery (3-step wizard, per-step EditForm, SubmitButtonStyle→LoginButtonStyle cascading), DetailsView (DataBoundComponent, auto-field reflection, mode switching, 10 events, paging). Image and Label upgraded to BaseStyledComponent (WI-15/WI-17). - Team update (2026-02-12): Milestone 4 planned Chart component with Chart.js via JS interop. 8 work items, design review required before implementation. decided by Forge + Squad +**Key patterns:** Enum files in `Enums/` with explicit int values. Instance-based Guid IDs (not static). `_ = callback.InvokeAsync()` for render-time events. `Path.GetFileName()` for file save security. Login controls inherit BaseWebFormsComponent with CascadingParameter styles. -- **Chart component architecture (WI-1/2/3):** Chart inherits `BaseStyledComponent`. Uses CascadingValue `"ParentChart"` for child registration (ChartSeries, ChartArea, ChartLegend, ChartTitle). JS interop via ES module `chart-interop.js` with lazy loading in `ChartJsInterop.cs`. `ChartConfigBuilder` is a pure static class converting component model → Chart.js JSON config, testable without browser. -- **Chart file paths:** - - Enums: `Enums/SeriesChartType.cs` (35 values), `Enums/ChartPalette.cs`, `Enums/Docking.cs`, `Enums/ChartDashStyle.cs` - - POCOs: `Axis.cs`, `DataPoint.cs` - - JS: `wwwroot/js/chart.min.js` (PLACEHOLDER), `wwwroot/js/chart-interop.js` - - C# interop: `ChartJsInterop.cs` - - Config builder: `ChartConfigBuilder.cs` (+ config snapshot classes) - - Components: `Chart.razor`/`.cs`, `ChartSeries.razor`/`.cs`, `ChartArea.razor`/`.cs`, `ChartLegend.razor`/`.cs`, `ChartTitle.razor`/`.cs` -- **Chart type mapping:** Web Forms `SeriesChartType.Point` maps to Chart.js `"scatter"`. Web Forms has no explicit "Scatter" enum value — `Point=0` is the equivalent. 8 types supported in Phase 1; unsupported throw `NotSupportedException`. -- **JS interop pattern for Chart:** Uses `IJSRuntime` directly (not the shared `BlazorWebFormsJsInterop` service) because Chart.js interop is chart-specific, not page-level. `ChartJsInterop` lazily imports the ES module and exposes `CreateChartAsync`, `UpdateChartAsync`, `DestroyChartAsync`. -- **BaseStyledComponent already has Width/Height as Unit type:** Chart adds `ChartWidth`/`ChartHeight` as string parameters for CSS dimension styling on the wrapper div, avoiding conflict with the base class Unit properties. -- **Instance-based canvas IDs:** Uses `Guid.NewGuid()` (truncated to 8 chars) for canvas element IDs, consistent with the ImageMap pattern that avoids static counters. -- **Feature audit — Editor Controls A–I (13 controls):** Created audit docs in `planning-docs/` comparing Web Forms API vs Blazor implementation for AdRotator, BulletedList, Button, Calendar, CheckBox, CheckBoxList, DropDownList, FileUpload, HiddenField, HyperLink, Image, ImageButton, ImageMap. -- **Common missing property: AccessKey.** Every component that inherits WebControl in Web Forms has AccessKey. Neither `BaseStyledComponent` nor `BaseWebFormsComponent` provides it. This is the single most pervasive gap — affects all 13 audited controls. -- **ToolTip inconsistently provided.** Some components (Button, FileUpload, Calendar, HyperLink, Image, ImageButton, ImageMap) add ToolTip directly. Others (AdRotator, BulletedList, CheckBox, CheckBoxList, DropDownList) do not. ToolTip should be on the base class. -- **Image base class mismatch.** `Image` inherits `BaseWebFormsComponent` but Web Forms `Image` inherits `WebControl`. This means Image is missing ALL style properties (CssClass, BackColor, ForeColor, Font, Width, Height, BorderColor, BorderStyle, BorderWidth, Style). ImageMap correctly uses `BaseStyledComponent` per team decision. Image should follow the same pattern. -- **HyperLink.NavigateUrl naming mismatch.** Web Forms uses `NavigateUrl`; Blazor uses `NavigationUrl`. This breaks migration — developers must rename the attribute. -- **List controls missing common ListControl properties.** BulletedList, CheckBoxList, and DropDownList all lack DataTextFormatString, AppendDataBoundItems, CausesValidation, and ValidationGroup. These are inherited from ListControl in Web Forms. -- **Calendar style sub-properties use CSS strings.** All 9 style sub-properties (DayStyle, TitleStyle, etc.) are implemented as CSS class strings instead of `TableItemStyle` objects. Functional but not API-compatible. -- **HiddenField correctly uses BaseWebFormsComponent.** Matches Web Forms where HiddenField inherits Control (not WebControl), so no style properties needed. -- **ChartSeries data binding fix:** `ToConfig()` now checks for `Items` + `YValueMembers` and extracts `DataPoint` objects via reflection. Uses `XValueMember` for X axis values and comma-separated `YValueMembers` for Y values. Falls back to manual `Points` collection when `Items` is null or `YValueMembers` is empty. Handles type conversion via `TryConvertToDouble()` for common numeric types. +### Summary: Milestone 4 Chart Component (2026-02-12) +Chart uses BaseStyledComponent, CascadingValue `"ParentChart"` for child registration. JS interop via separate `ChartJsInterop` (not shared service). `ChartConfigBuilder` is pure static class for testability. ChartWidth/ChartHeight as strings (avoid base Width/Height conflict). SeriesChartType.Point → Chart.js "scatter". 8 Phase 1 types; unsupported throw NotSupportedException. ChartSeries data binding via reflection on Items/XValueMember/YValueMembers. + +### Summary: Feature Audit — Editor Controls A–I (2026-02-23) + +Audited 13 controls. Found: AccessKey/ToolTip missing from base class (universal gap), Image needs BaseStyledComponent, HyperLink.NavigateUrl naming mismatch, list controls missing DataTextFormatString/AppendDataBoundItems/CausesValidation/ValidationGroup, Calendar styles use CSS strings instead of TableItemStyle objects. - Team update (2026-02-23): AccessKey/ToolTip must be added to BaseStyledComponent decided by Beast, Cyclops - Team update (2026-02-23): Label should inherit BaseStyledComponent instead of BaseWebFormsComponent decided by Beast - Team update (2026-02-23): DataBoundComponent style gap DataBoundStyledComponent recommended decided by Forge - Team update (2026-02-23): Chart implementation architecture consolidated (10 decisions) decided by Cyclops, Forge - Team update (2026-02-23): Validation Display property missing from all validators migration-blocking decided by Rogue - Team update (2026-02-23): ValidationSummary comma-split bug is data corruption risk decided by Rogue - Team update (2026-02-23): Login controls missing outer WebControl style properties decided by Rogue 📌 Team update (2026-02-12): DetailsView auto-generated fields must render in Edit/Insert mode — decided by Cyclops + +- **DataBoundComponent style inheritance (WI-07):** Changed `BaseDataBoundComponent` to inherit `BaseStyledComponent` instead of `BaseWebFormsComponent`. This gives ALL data controls (GridView, DetailsView, FormView, ListView, DataGrid, DataList, Repeater, TreeView, AdRotator, BulletedList, CheckBoxList, DropDownList, ListBox, RadioButtonList) the full IStyle property set (BackColor, CssClass, ForeColor, Font, etc.) from the base class. Removed duplicate IStyle implementations and CssClass properties from: GridView, DetailsView, DataGrid, DataList, TreeView, AdRotator, BulletedList, CheckBoxList, DropDownList, ListBox, RadioButtonList. DataList kept its `new string Style` parameter (user-supplied CSS) but removed its IStyle declaration and 9 duplicate style properties. ListView kept its obsolete `new string Style` parameter. FormView and Repeater needed no changes. +- **CausesValidation pattern for non-button controls (WI-49):** CheckBox, RadioButton, and TextBox now have `CausesValidation` (bool, default true), `ValidationGroup` (string), and a `[CascadingParameter(Name = "ValidationGroupCoordinator")] ValidationGroupCoordinator Coordinator` — identical to the pattern in `ButtonBaseComponent`. Validation is triggered in the existing `HandleChange` method for CheckBox/RadioButton. TextBox has the parameters but no handler wiring because the component has no `@onchange` binding in its template. +- **BaseListControl base class (WI-47/48):** Created `DataBinding/BaseListControl.cs` inheriting `DataBoundComponent`. Consolidates `StaticItems`, `DataTextField`, `DataValueField`, `GetItems()`, and `GetPropertyValue()` from all 5 list controls (BulletedList, CheckBoxList, DropDownList, ListBox, RadioButtonList). All 5 now inherit `BaseListControl`. This mirrors Web Forms `ListControl` as the shared base. +- **DataTextFormatString (WI-47):** `[Parameter] public string DataTextFormatString` on `BaseListControl`. Applied via `string.Format(DataTextFormatString, text)` at render time in `GetItems()`, not at bind time. Affects both static and data-bound items. Static items get a cloned `ListItem` to avoid mutating the source collection. +- **AppendDataBoundItems (WI-48):** `[Parameter] public bool AppendDataBoundItems` on `BaseListControl`, default `false`. When `false` and `Items != null`, static items are skipped in `GetItems()`. When `true`, static items always render before data-bound items. Matches Web Forms semantics where `DataBind()` clears `Items` by default. +- **List control inheritance chain:** `BaseListControl` → `DataBoundComponent` → `BaseDataBoundComponent` → `BaseStyledComponent` → `BaseWebFormsComponent`. All list controls get full style property set via this chain. +- **Orientation enum and Menu orientation (WI-50):** Created `Enums/Orientation.cs` (Horizontal=0, Vertical=1) using file-scoped namespace. Menu.razor.cs gets `[Parameter] public Orientation Orientation { get; set; } = Orientation.Vertical;`. Horizontal layout achieved via CSS class `horizontal` on the top-level `
    `, with `display: inline-block` on direct `
  • ` children. JS interop orientation string made dynamic from enum value. +- **Label AssociatedControlID (WI-51):** When `AssociatedControlID` is set (non-null/non-empty), Label renders `
` element. No conflicts with CascadingParameter sub-styles (TitleTextStyle, LabelStyle, etc.) because outer styles are `[Parameter]` and sub-styles are `[CascadingParameter]` — completely independent mechanisms. `SetFontsFromAttributes` called in `HandleUnknownAttributes` for Font-* attribute support. The `GetCssClassOrNull()` helper returns null when empty so the `class` attribute is omitted from HTML when no CssClass is set. + + Team update (2026-02-23): Menu Orientation Orientation parameter collides with enum type name in Razor samples must use local variable with fully-qualified type decided by Jubilee + Team update (2026-02-23): P2 test observation Login/ChangePassword/CreateUserWizard already inherit BaseStyledComponent, so WI-52 may have been a no-op or template-only change decided by Rogue + Team update (2026-02-23): Milestone 6 Work Plan ratified 54 WIs across P0/P1/P2 tiers targeting ~345 feature gaps decided by Forge + Team update (2026-02-23): UI overhaul requested ComponentCatalog (UI-2) and search (UI-8) assigned to Cyclops decided by Jeffrey T. Fritz diff --git a/.ai-team/agents/forge/history.md b/.ai-team/agents/forge/history.md index 8eb4f83df..58de63d6c 100644 --- a/.ai-team/agents/forge/history.md +++ b/.ai-team/agents/forge/history.md @@ -49,148 +49,17 @@ Evaluated 4 JS libraries for Chart component. D3 rejected (zero built-in charts, 📌 Team update (2026-02-12): Chart component feasibility confirmed — Chart.js recommended via JS interop. Effort: L. Target Milestone 4. — decided by Forge 📌 Team update (2026-02-12): Milestone 4 planned — Chart component with Chart.js via JS interop. 8 work items, design review required before implementation. — decided by Forge + Squad -### Feature Comparison Audit: Data Controls + Navigation Controls (2026-02-12) - -Completed full API surface audit of 12 controls (9 Data + 3 Navigation) comparing Web Forms API vs Blazor implementation. Created `planning-docs/{ControlName}.md` for each. - -**Key findings on control coverage:** - -1. **Best coverage:** Repeater (minimal Web Forms API, nearly 100% match), DataList (38 props, 8 events matching — excellent style/template support), SiteMapPath (27 props, 5 events — near-complete), DataPager (27 props, 7 events — solid paging). - -2. **Good but incomplete:** DetailsView (27 props, 16 events — strong CRUD events, missing style props; on sprint3 branch), TreeView (21 props, 11 events — good core + data binding + accessibility, missing node styles), Menu (16 props, 7 events — good rendering + JS interop, missing base styles and Orientation). - -3. **Weakest coverage:** GridView (9 props, 8 events — only basic table rendering, no paging/sorting/editing/selection), FormView (10 props, 12 events — good mode switching but missing nearly all display properties), ListView (14 props, 9 events — great templates, no CRUD events), Chart (14 props, 6 events — architectural deviation to Chart.js/canvas). - -**Recurring pattern — style property gap:** Controls inheriting DataBoundComponent (DataGrid, GridView, FormView, DetailsView, ListView) lack WebControl-level style properties (BackColor, ForeColor, Font, BorderColor, Width, Height, etc.) because DataBoundComponent inherits BaseWebFormsComponent, not BaseStyledComponent. DataList is the exception — it implements IStyle directly with all style parameters. - -**Recurring pattern — missing CRUD events:** GridView, ListView, and DataGrid are all missing row/item-level CRUD events (RowDeleting/RowDeleted, ItemEditing, etc.) that are essential for inline editing scenarios. Only DetailsView and FormView have these. - -**Recurring pattern — no PagerSettings:** All controls that support paging (GridView, DetailsView, FormView) are missing the PagerSettings configuration object that Web Forms uses to configure pager appearance. - -**DetailsView branch status:** DetailsView exists on `sprint3/detailsview-passwordrecovery` but is not on the current working branch (`milestone4/chart-component`). - -### Themes and Skins Migration Strategy (2026-02-12) - -- Evaluated 5 approaches for migrating Web Forms Themes/Skins to Blazor: CSS Custom Properties, CascadingValue ThemeProvider, Generated CSS Isolation, DI Service, and Hybrid. -- **Recommended CascadingValue ThemeProvider** — only approach that faithfully models both `Theme` (override) and `StyleSheetTheme` (default) semantics, supports SkinID selection, and can set any property (not just CSS-expressible ones). -- CSS-only approaches (1, 3, 5) cannot set non-CSS properties like `Width` (as HTML attribute), `ToolTip`, or `Visible` — which are valid skin properties in Web Forms. -- DI-based approach (4) works functionally but cannot scope themes to a page or subtree, unlike `CascadingValue` which mirrors Web Forms' per-page `@Page Theme=` directive. -- **Known bug:** `BaseWebFormsComponent.SkinID` is typed as `bool` instead of `string`. Must be fixed before any theme implementation. -- The library already uses CascadingParameters extensively (TableItemStyle, LoginControl styles) — ThemeProvider follows the same pattern. -- Implementation is opt-in and non-breaking: no `ThemeProvider` wrapper = no behavior change. -- Strategy is exploratory per Jeff's request — the README exclusion of themes/skins still stands until a decision to implement. - - Team update (2026-02-23): AccessKey/ToolTip must be added to BaseStyledComponent fixes all 20+ styled controls in one change decided by Beast, Cyclops - Team update (2026-02-23): DataBoundComponent style gap confirmed systemic DataBoundStyledComponent recommended decided by Forge - Team update (2026-02-23): GridView is highest-priority data control gap (no paging/sorting/editing) decided by Forge - Team update (2026-02-23): DetailsView/PasswordRecovery branch (sprint3) must be merged forward decided by Forge - Team update (2026-02-23): CascadingValue ThemeProvider recommended for Themes/Skins migration decided by Forge -📌 Team update (2026-02-10): Close PR #333 without merging — all Calendar work already on dev, PR branch has 0 unique commits — decided by Rogue -📌 Team update (2026-02-10): Sprint 2 complete — Localize, MultiView+View, ChangePassword, CreateUserWizard shipped with docs, samples, tests. 709 tests passing. 41/53 components done. — decided by Squad - -### 2026-02-10 — Sprint 3 Planning & Status Reconciliation - -**Status.md was significantly stale:** -- Calendar was merged to dev via commit d33e156 and PR #339 but still marked 🔴 Not Started -- FileUpload was merged via PRs #335 and #338 but still marked 🔴 Not Started -- Summary table said 41/53 (Editor: 20/27) but actual count of ✅ entries in the detailed section was already 23/27 for Editors (now 25/27 with Calendar + FileUpload fixed) -- The 27-count for Editor Controls groups MultiView and View as one logical component despite separate table rows -- Corrected total: 48/53 components complete (91%), 5 remaining - -**Sprint 3 scope decision:** -- DetailsView and PasswordRecovery are the two buildable components -- Chart deferred: requires SVG/Canvas rendering engine, no Blazor primitive equivalent -- Substitution deferred: Web Forms output caching has no Blazor architectural equivalent -- Xml deferred: XSLT transforms are a dead-end pattern with near-zero migration demand -- Post-Sprint 3 state: 50/53 (94%), library effectively feature-complete for practical migration - -**DetailsView design notes:** -- Must inherit BaseStyledComponent (Web Forms DetailsView → CompositeDataBoundControl → WebControl) -- Renders as `
` with one `` per field (vertical layout vs GridView's horizontal) -- Can reuse existing BoundField, TemplateField, CommandField, HyperLinkField, ButtonField from GridView -- Needs DetailsViewMode enum (ReadOnly=0, Edit=1, Insert=2) -- Needs 8 EventArgs classes for mode changes, CRUD operations - -**PasswordRecovery design notes:** -- Must inherit BaseStyledComponent -- 3-step wizard flow: UserName → Question → Success (same pattern as CreateUserWizard's 2-step) -- Can reuse existing LoginControls style sub-components (TitleTextStyle, TextBoxStyle, LabelStyle, etc.) -- Table-based HTML output matching ChangePassword's render pattern - -📌 Team update (2026-02-10): Sprint 3 plan ratified — DetailsView + PasswordRecovery. Chart/Substitution/Xml deferred indefinitely with migration docs. 48/53 → target 50/53. — decided by Forge -📌 Team update (2026-02-11): Colossus added as dedicated integration test engineer. Rogue retains bUnit unit tests. — decided by Jeffrey T. Fritz - -### 2026-02-11 — Sprint 3 Gate Review - -**DetailsView — APPROVED:** -- Inherits `DataBoundComponent` — correct for data-bound controls. Uses same `Items` property as GridView/ListView. -- All 10 Web Forms events implemented with correct `EventArgs` types. Pre-operation events support cancellation. -- `DetailsViewMode` enum (ReadOnly=0, Edit=1, Insert=2) matches Web Forms exactly. -- HTML output: `
` with one `` per field, command row with `` links, nested-table numeric pager — all match Web Forms. -- Auto-generation via reflection correctly generates fields from `ItemType` properties. -- Minor issues (non-blocking): `CombinedStyle` has CellPadding/CellSpacing logic mismatch, `cellspacing` hardcoded to 0 in template, docs use `DataSource` but actual parameter is `Items`. -- DetailsView docs `DataSource`→`Items` fix assigned to Beast. - -**PasswordRecovery — APPROVED:** -- Inherits `BaseWebFormsComponent` — consistent with ChangePassword and CreateUserWizard pattern. -- 3-step wizard flow (UserName → Question → Success) matches Web Forms exactly. -- Reuses existing `LoginCancelEventArgs`, `TableItemStyle`, `Style` cascading parameter pattern from other Login Controls. -- `SuccessTextStyle` sub-component added following existing `UiTableItemStyle` pattern. -- All 6 events implemented: `OnVerifyingUser`, `OnUserLookupError`, `OnVerifyingAnswer`, `OnAnswerLookupError`, `OnSendingMail`, `OnSendMailError`. -- `SetQuestion()` and `SkipToSuccess()` APIs provide developer control matching Web Forms extensibility. -- Table-based nested HTML output matches Web Forms PasswordRecovery output. -- Minor issues (non-blocking): `RenderOuterTable` declared but not used, `SubmitButtonType`/`SubmitButtonImageUrl` declared but not rendered, sample uses `e.Sender` casting instead of `@ref`. - -**Key Patterns Confirmed:** -- Login Controls consistently inherit `BaseWebFormsComponent` (not `BaseStyledComponent`) and use cascading `TableItemStyle`/`Style` objects for styling — this is an established project convention. -- Data-bound controls inherit `DataBoundComponent` which provides `Items` (not `DataSource`) as the primary binding parameter. -- Event naming in Login Controls uses `On` prefix (`OnVerifyingUser`, `OnChangingPassword`) — project convention, not Web Forms convention. -- Both components ship with docs, samples, and tests per Sprint 2 policy. - -**Sprint 3 Status:** -- 50/53 components complete (94%) -- 797 tests passing, 0 build errors -- 3 remaining (Chart, Substitution, Xml) deferred indefinitely -- Library is effectively feature-complete for practical Web Forms migration - -📌 Team update (2026-02-11): Sprint 3 gate review — DetailsView APPROVED, PasswordRecovery APPROVED. 50/53 complete (94%). — decided by Forge -### Chart Component Analysis (2026-02-13) - -Thorough review of `milestone4/chart-component` branch. Implementation is **substantially complete** for Phase 1 scope: - -**What's done:** -- `Chart.razor/.cs`: BaseStyledComponent inheritance, ChartWidth/ChartHeight/Palette/CssClass, canvas rendering, JS interop lifecycle (create/update/destroy) -- `ChartSeries.razor/.cs`: 13 properties, cascading parent registration -- `ChartArea.razor/.cs`: AxisX/AxisY (Axis POCO class) -- `ChartTitle.razor/.cs` & `ChartLegend.razor/.cs`: Text, Docking -- `ChartConfigBuilder.cs`: Pure static config builder (testable without canvas), 8 chart types mapped -- `ChartJsInterop.cs`: ES module loader for chart-interop.js -- Enums: `SeriesChartType` (35 values matching Web Forms), `ChartPalette` (12 palettes), `Docking` (4 positions), `ChartDashStyle` -- Supporting classes: `DataPoint` (XValue, YValues[], Label, Color, ToolTip), `Axis` (Title, Min, Max, Interval, IsLogarithmic) -- wwwroot: `chart.min.js` (Chart.js bundled), `chart-interop.js` (ES module wrapper) -- 140 unit tests passing — enums, DataPoint, Axis, ChartConfigBuilder output -- Docs: Comprehensive `Chart.md` with migration guide, code examples, feature tables -- Samples: 8 Blazor pages (Index/Column, Bar, Line, Area, Pie, Doughnut, Scatter, StackedColumn) -- BeforeWebForms: PieChart.aspx, LineChart.aspx reference samples - -**Gaps identified:** -1. **Data binding not implemented** — `XValueMember`, `YValueMembers`, `Items` parameters exist but `ToConfig()` ignores them. Docs show data-bound examples that will silently fail. -2. **27 chart types unsupported** — throw `NotSupportedException`. Clearly documented. -3. **No integration tests** — Colossus hasn't added Chart sample routes to smoke tests yet. -4. **No per-point coloring** — `DataPoint.Color` not wired to Chart.js output. -5. **No tooltips** — `DataPoint.ToolTip` and `ChartSeries.ToolTip` not wired. -6. **`IsValueShownAsLabel`** — not implemented. -7. **MarkerStyle** — parameter exists but not mapped. - -**Architecture assessment:** -- Clean separation: Components → Config objects → ChartConfigBuilder → JSON → JS interop -- Config builder is purely testable without browser context (140 tests) -- ES module pattern for JS loading is correct -- SSR/prerender handled gracefully (JSException caught) -- Dispose pattern handles circuit disconnection - -**Risk assessment:** -- Approach is sound — Chart.js is a solid choice -- First JS interop in project is well-executed -- Data binding gap is ship-blocking — docs promise it works -- Remaining gaps are Phase 2/3 features, not blockers +### Summary: Feature Audit & Themes/Skins Exploration (2026-02-12) + +Audited 12 data + navigation controls. Key findings: DataBoundComponent chain lacks style properties (systematic gap across 5 controls). GridView weakest (no paging/sorting/editing). Recommended DataBoundStyledComponent as fix. DetailsView strong on sprint3 branch but needs merge-forward. Evaluated 5 Themes/Skins approaches — recommended CascadingValue ThemeProvider. SkinID bug (bool→string). Implementation opt-in. + +### Summary: Sprint 3 Planning & Gate Review (2026-02-10 through 2026-02-11) + +Status.md reconciliation found actual 48/53 (not 41/53). Sprint 3: DetailsView + PasswordRecovery. Chart/Substitution/Xml deferred. DetailsView APPROVED (DataBoundComponent, 10 events, auto-fields via reflection). PasswordRecovery APPROVED (BaseWebFormsComponent, 3-step wizard, 6 events). Key patterns: Login Controls → BaseWebFormsComponent, Data → DataBoundComponent with Items, events use On prefix. 50/53 complete (94%), 797 tests. + +### Summary: Chart Component Gate Review (2026-02-13) + +Chart on milestone4 branch substantially complete. Architecture sound: Components→ConfigBuilder→JSON→JS interop. 140 tests. Data binding gap was ship-blocking (Items/XValueMember/YValueMembers ignored by ToConfig()). Conditionally approved pending fix. 27 unsupported chart types documented. Phase 2/3: per-point coloring, tooltips, IsValueShownAsLabel, MarkerStyle. + + Team update (2026-02-23): P2 features complete all 1,065 tests pass, 0 build errors decided by Squad + Team update (2026-02-23): UI overhaul requested by Jeffrey T. Fritz scope document created decided by Jeffrey T. Fritz diff --git a/.ai-team/agents/jubilee/history.md b/.ai-team/agents/jubilee/history.md index 4ed346208..903170816 100644 --- a/.ai-team/agents/jubilee/history.md +++ b/.ai-team/agents/jubilee/history.md @@ -8,73 +8,33 @@ ## Learnings + -### Sprint 1 — Sample Pages for Calendar, FileUpload, ImageMap (2026-02-10) +### Summary: Milestones 1–3 Sample Pages (2026-02-10 through 2026-02-12) -- **Sample page location:** New-style samples go in `samples/AfterBlazorServerSide/Components/Pages/ControlSamples/{ComponentName}/Index.razor` (the .NET 8+ `Components/Pages` layout). There's also an older `Pages/ControlSamples/` path used by some legacy pages — avoid it for new work. -- **Navigation:** Two places must be updated when adding a sample: `Components/Layout/NavMenu.razor` (TreeView-based nav) and `Components/Pages/ComponentList.razor` (flat list on the home page). Both are alphabetically ordered within their category sections. -- **Sample page pattern:** Each page uses `@page "/ControlSamples/{Name}"`, includes a ``, uses `

` for the top heading, `

`/`

` for subsections, `
` between sections, inline `
` blocks showing the markup, and an `@code {}` block at the bottom.
-- **PR branches can be read without checkout:** Used `git --no-pager show {branch}:{path}` to read component source from PR branches while staying on `dev`.
-- **Calendar already had a sample on dev** from an earlier Copilot commit — I improved it with PageTitle, better structure, code snippets per section, and additional CSS styling demos (weekend/other-month styles).
-- **FileUpload uses `@ref` pattern** for imperative access (checking `HasFile`, `FileName`) — this is the closest analog to the Web Forms code-behind pattern of `FileUpload1.HasFile`.
-- **ImageMap uses a `List` parameter** (not child components) — hot spots are defined in code and passed as a list, which differs from the Web Forms declarative `` child syntax.
+Sprint 1: Calendar (improved existing), FileUpload (@ref pattern), ImageMap (List parameter), PageService. Fixed PostedFileWrapper.SaveAs path traversal vulnerability. Sprint 3: DetailsView (Items parameter, inline data), PasswordRecovery (3-step flow, Sender casting). Nav ordering is semi-alphabetical. LoginControls need explicit `@using`.
 
-📌 Team update (2026-02-10): Docs and samples must ship in the same sprint as the component — decided by Jeffrey T. Fritz
-📌 Team update (2026-02-10): PRs #328 (ASCX CLI) and #309 (VS Snippets) shelved indefinitely — decided by Jeffrey T. Fritz
-📌 Team update (2026-02-10): Sprint 1 gate review — FileUpload (#335) REJECTED, assigned to Jubilee for path sanitization fix (Cyclops locked out) — decided by Forge
+### Summary: Milestone 4 Chart + Utility Samples (2026-02-12)
 
-### Security Fix — PostedFileWrapper.SaveAs path sanitization (PR #335)
+Chart: 8 basic + 4 advanced sample pages (DataBinding, MultiSeries, Styling, ChartAreas). Child components via CascadingValue. `SeriesChartType.Point` for scatter. Axis is POCO, not component. DataBinder: 3 Eval() signatures with Repeater. ViewState: @ref Panel with counter demo. Fixed NavMenu ordering and ComponentList entries. WebColor: use static fields, not FromName().
 
-- **Path traversal vulnerability:** `PostedFileWrapper.SaveAs()` passed the `filename` parameter directly to `FileStream` with zero sanitization. A malicious filename like `../../etc/passwd` could write outside the intended directory. The outer `FileUpload.SaveAs()` already had `Path.GetFileName()` sanitization, but the inner `PostedFileWrapper.SaveAs()` did not — creating a security bypass.
-- **Fix applied:** Added the same `Path.GetFileName()` + `Path.GetDirectoryName()` + `Path.Combine()` sanitization pattern from the outer `SaveAs()` to `PostedFileWrapper.SaveAs()`.
-- **Lesson:** When a class exposes multiple code paths to the same operation (e.g., `FileUpload.SaveAs()` and `PostedFileWrapper.SaveAs()`), security sanitization must be applied consistently in ALL paths. Wrapper/inner classes are easy to overlook.
-- **Assigned because:** Cyclops (original author) was locked out per reviewer rejection protocol after Forge's gate review flagged this issue.
+**Key patterns:** Samples in `Components/Pages/ControlSamples/{Name}/Index.razor`. Nav updates: NavMenu.razor + ComponentList.razor. `@using BlazorWebFormsComponents.LoginControls` required for login controls. `#pragma warning disable CS0618` for Obsolete APIs in demos.
 
-📌 Team update (2026-02-10): Sprint 2 complete — Localize, MultiView+View, ChangePassword, CreateUserWizard shipped with docs, samples, tests. 709 tests passing. 41/53 components done. — decided by Squad
-📌 Team update (2026-02-11): Sprint 3 scope: DetailsView + PasswordRecovery. Chart/Substitution/Xml deferred. 48/53 → target 50/53. — decided by Forge
-📌 Team update (2026-02-11): Colossus added as dedicated integration test engineer. Rogue retains bUnit unit tests. — decided by Jeffrey T. Fritz
+### Milestone 6 — Sample Page Updates for Base Class Features (WI-03, WI-06, WI-09, WI-12)
 
-### Sprint 3 — Sample Pages for DetailsView and PasswordRecovery (2026-02-11)
+- **Button AccessKey + ToolTip (WI-03, WI-06):** Added `AccessKey="b"` and `ToolTip="Click to submit"` to the existing Button demo in `Components/Pages/ControlSamples/Button/Index.razor`. Button already had `ToolTip` as a declared parameter rendering `title=` attribute. `AccessKey` goes through `AdditionalAttributes` capture — rendering depends on the component template including `accesskey` in its HTML output.
+- **GridView CssClass (WI-09):** Added `CssClass="table table-striped"` to the GridView default sample. GridView inherits style properties from `BaseStyledComponent` via `DataBoundComponent` → `BaseDataBoundComponent` → `BaseStyledComponent`, and `GridView.razor` already renders `class="@CssClass"` on its `

` element — so this works immediately. +- **Validator Display (WI-12):** Added `Display="ValidatorDisplay.Dynamic"` to the second `RequiredFieldValidator` in the RequiredFieldValidator sample. The `ValidatorDisplay` enum exists in `Enums/ValidatorDisplay.cs` with values `None`, `Static`, `Dynamic`. The attribute compiles via `AdditionalAttributes` capture on `BaseValidator` → `BaseStyledComponent` → `BaseWebFormsComponent`. Actual Display behavior (collapsing vs hidden vs none) depends on Cyclops implementing the `Display` parameter in `BaseValidator.razor.cs` and using it in the template. +- **Minimal change pattern:** For feature demos on existing samples, just add the new property to one existing component instance plus a brief explanatory note — no need for new sections or pages. -- **DetailsView sample:** Uses `Items` parameter with inline `List` data (same pattern as GridView RowSelection sample). Demonstrates auto-generated rows, paging between records with `AllowPaging`, Edit mode switching with `AutoGenerateEditButton`, and empty data text. Uses `Customer` model from `SharedSampleObjects.Models`. -- **PasswordRecovery sample:** Shows the 3-step flow (username → security question → success). Uses `SetQuestion()` on the component reference via the `LoginCancelEventArgs.Sender` cast pattern. Demonstrates custom text properties and help link configuration. -- **PasswordRecovery event pattern:** The `LoginCancelEventArgs` exposes a `Sender` property that must be cast back to `PasswordRecovery` to call `SetQuestion()` — this is the key integration point for the security question step. -- **Nav ordering note:** Data Components section in NavMenu.razor is not strictly alphabetical (DataList before DataGrid). I placed DetailsView after DataGrid and before FormView to maintain the closest alphabetical order without rearranging existing entries. - -📌 Team update (2026-02-12): Sprint 3 gate review — DetailsView and PasswordRecovery APPROVED. 50/53 components (94%). — decided by Forge - - Team update (2026-02-12): Milestone 4 planned Chart component with Chart.js via JS interop. 8 work items, design review required before implementation. decided by Forge + Squad - -### Milestone 4 — Chart Sample Pages (WI-6) - -- **Chart component API:** Uses child components (`ChartSeries`, `ChartArea`, `ChartTitle`, `ChartLegend`) inside a `` parent via `CascadingValue`. Data is provided through `List` on `ChartSeries.Points`, where each `DataPoint` has `XValue` (object) and `YValues` (double[]). Chart type is set via `SeriesChartType` enum on `ChartSeries.ChartType`. -- **8 sample pages created:** Index (Column), Line, Bar, Pie, Area, Doughnut, Scatter, StackedColumn — each under `Components/Pages/ControlSamples/Chart/`. -- **Multi-series demos:** Line (NY vs LA temps), StackedColumn (3 product lines) show how to add multiple `ChartSeries` children. -- **Scatter uses `Point` type:** The `SeriesChartType` enum has `Point` (not `Scatter`), so the scatter sample uses `SeriesChartType.Point`. -- **Axis config is a POCO:** `Axis` is a plain class (not a component), passed via parameter syntax `AxisX="@(new Axis { Title = "..." })"`. -- **NavMenu Chart node:** Added under Data Components with `Expanded="false"` and 8 sub-nodes for each chart type, alphabetically ordered. -- **ComponentList updated:** Replaced placeholder `Chart(?)` with a working link. - - - Team update (2026-02-23): DetailsView/PasswordRecovery branch (sprint3) must be merged forward decided by Forge - Team update (2026-02-23): AccessKey/ToolTip must be added to BaseStyledComponent decided by Beast, Cyclops -📌 Team update (2026-02-12): LoginControls sample pages MUST include `@using BlazorWebFormsComponents.LoginControls` — root _Imports.razor doesn't cover sub-namespaces. Never use external image URLs in samples; use local SVGs. — decided by Colossus - -### Utility Feature Sample Pages — DataBinder and ViewState - -- **DataBinder sample** (`Components/Pages/ControlSamples/DataBinder/Index.razor`): Demonstrates all three `Eval()` signatures with a Repeater — `DataBinder.Eval(container, "Prop")`, shorthand `Eval("Prop")` via `@using static`, and `Eval("Prop", "{0:C}")` with format strings. Each section has live demo + code block. Section 4 ("Moving On") shows the modern `@context.Property` approach side by side. -- **ViewState sample** (`Components/Pages/ControlSamples/ViewState/Index.razor`): Uses `@ref` to a Panel component to demo `ViewState.Add`/`ViewState["key"]` dictionary API. Shows a click counter and a multi-key settings form stored in ViewState, then contrasts with the modern C# field/property approach. `#pragma warning disable CS0618` suppresses the Obsolete warnings for the demo code. -- **Navigation fixes applied:** NavMenu.razor Login Components reordered (Login before LoginName), DataBinder and ViewState added to Utility Features (alphabetical: DataBinder, ID Rendering, PageService, ViewState). ComponentList.razor fixed: HyperLink moved before Image in Editor Controls, ImageMap removed from Editor Controls and added to Navigation Controls (per team decision), Utility Features column added. mkdocs.yml: ImageMap removed from Editor Controls nav (already in Navigation Controls). -- **Widget model reused:** DataBinder sample reuses `SharedSampleObjects.Models.Widget` with inline data (Laptop Stand, USB-C Hub, Mechanical Keyboard) for a product catalog demo. -- **Build verified:** `dotnet build` passes with 0 compilation errors (Debug config). Release config has a known transient Nerdbank.GitVersioning file-copy issue unrelated to this work. -### Chart Feature-Rich Sample Pages (2026-02-12) - -- **4 new sample pages added:** DataBinding, MultiSeries, Styling, ChartAreas — each demonstrating advanced Chart features. -- **DataBinding.razor:** Shows Web Forms-style data binding with `Items`, `XValueMember`, and `YValueMembers` parameters. Uses business object records (`SalesData`, `TrafficData`) instead of manual `DataPoint` creation. Includes Web Forms vs Blazor comparison code snippets. -- **MultiSeries.razor:** Demonstrates multiple series on one chart for comparisons — revenue channels (Online vs In-Store), regional sales (3 regions), and server performance metrics (CPU vs Memory). Shows the pattern of adding multiple `` children to one ``. -- **Styling.razor:** Showcases all 11 `ChartPalette` options with visual comparisons (BrightPastel, Berry, Chocolate, EarthTones, Excel, Fire, Grayscale, Light, Pastel, SeaGreen, SemiTransparent). Demonstrates custom colors via `WebColor` static fields (e.g., `WebColor.DodgerBlue`). -- **ChartAreas.razor:** Explains the `Axis` configuration options (Title, Minimum, Maximum, Interval, IsLogarithmic). Shows logarithmic scale for exponential data and constrained Y-axis for focused ranges. -- **Nav ordering pattern:** New samples added alphabetically within Chart node: Area, Bar, ChartAreas, Column, DataBinding, Doughnut, Line, MultiSeries, Pie, Scatter, StackedColumn, Styling. -- **WebColor usage:** Use static fields like `WebColor.DodgerBlue` not `WebColor.FromName("...")` which doesn't exist. +### P2 Feature Samples — DataTextFormatString, Menu Orientation, Label AssociatedControlID +- **DropDownList DataTextFormatString:** Added two new sections to the existing `Components/Pages/ControlSamples/DropDownList/Index.razor` — one showing `{0:C}` currency formatting with a `PricedProduct` model, and one showing `"Item: {0}"` prefix formatting. Both use data-bound items to demonstrate the feature realistically. +- **Menu Orientation (Horizontal):** Added a horizontal menu demo to the existing `Pages/ControlSamples/Menu/Index.razor`. The `Orientation` parameter requires a local variable in `@code` because the parameter name matches the enum type name — `BlazorWebFormsComponents.Enums.Orientation horizontal = BlazorWebFormsComponents.Enums.Orientation.Horizontal;` then `Orientation="@horizontal"` in markup. Added `@using BlazorWebFormsComponents.Enums` to the page. +- **Label AssociatedControlID:** Created a new sample page at `Components/Pages/ControlSamples/Label/Index.razor`. Demos basic Label (renders as ``), styled Label, and the key feature: `AssociatedControlID` which renders as `
`. -**Recommendation:** Evaluate whether these composite controls should inherit `BaseStyledComponent` or if CascadingParameter sub-element styles are sufficient. -**Why:** Migrating pages that set `CssClass` on Login controls will break. +**By:** Rogue, Cyclops +**What:** Login, ChangePassword, and CreateUserWizard were identified as missing outer-level WebControl style properties (BackColor, CssClass, ForeColor, Width, Height) because they inherited `BaseWebFormsComponent` instead of `BaseStyledComponent`. Resolution: all three changed to inherit `BaseStyledComponent` (Option A — base class change). Outer `
` elements now render CssClass and computed IStyle inline styles alongside `border-collapse:collapse;`. `[Parameter]` style properties do NOT conflict with `[CascadingParameter]` sub-styles (TitleTextStyle, LabelStyle, etc.) — completely independent mechanisms. LoginView still inherits `BaseWebFormsComponent`. LoginName and LoginStatus already had full style support. PasswordRecovery should follow the same pattern when ready. +**Why:** Migrating pages that set CssClass on Login controls would break. `BaseStyledComponent` extends `BaseWebFormsComponent` — no functionality lost. ### 2026-02-12: ChangePassword and CreateUserWizard sample pages require LoginControls using directive **By:** Colossus @@ -857,3 +856,495 @@ Cyclops is fixing the `ChartSeries.ToConfig()` bug where data binding is not imp 152 Chart tests (140 original + 12 new data binding tests). All passing. +### 2026-02-14: User directive — Sample website UI overhaul +**By:** Jeffrey T. Fritz (via Copilot) +**What:** Improve the UI of the samples/AfterBlazorServerSide website with a modern layout that demos each sample, feature, and component cleanly. Add a search feature. Update integration tests with this overhaul. +**Why:** User request — captured for team memory + +# Decision: BaseDataBoundComponent inherits BaseStyledComponent + +**By:** Cyclops +**Date:** 2026-02-23 +**Work Item:** WI-07 + +## What + +Changed the inheritance chain from: +``` +DataBoundComponent → BaseDataBoundComponent → BaseWebFormsComponent +``` +To: +``` +DataBoundComponent → BaseDataBoundComponent → BaseStyledComponent → BaseWebFormsComponent +``` + +This gives all data-bound controls the full IStyle property set (BackColor, BorderColor, BorderStyle, BorderWidth, CssClass, ForeColor, Font, Height, Width) from the base class. + +## Controls affected + +Removed duplicate IStyle declarations and style properties from: +- **GridView** — removed CssClass +- **DetailsView** — removed CssClass +- **DataGrid** — removed CssClass +- **DataList** — removed IStyle + 9 style properties; kept `new string Style` parameter +- **TreeView** — removed IStyle + 9 style properties +- **AdRotator** — removed IStyle + 9 style properties + Style computed property +- **BulletedList** — removed IStyle + 9 style properties + Style computed property +- **CheckBoxList** — removed IStyle + 9 style properties + Style computed property +- **DropDownList** — removed IStyle + 9 style properties + Style computed property +- **ListBox** — removed IStyle + 9 style properties + Style computed property +- **RadioButtonList** — removed IStyle + 9 style properties + Style computed property + +No changes needed: +- **FormView** — no duplicate properties +- **ListView** — only added `new` keyword to existing obsolete Style parameter +- **Repeater** — no duplicate properties + +## Why + +Data controls in Web Forms inherit from `DataBoundControl → WebControl`, which provides style properties. Our `BaseDataBoundComponent` was missing this, forcing each control to implement IStyle independently with duplicate property declarations. This caused ~70 style property gaps and made maintenance harder. + +## Impact + +- 949/949 tests pass — zero regressions +- All existing style rendering in templates (DataList, DetailsView, etc.) continues to work unchanged +- Controls that don't yet render styles in their templates can add rendering later per-control + +### 2026-02-23: Label AssociatedControlID switches rendered element +**By:** Cyclops +**What:** Label renders `
` renders `class="@GetCssClassOrNull()"` and `style="border-collapse:collapse;@Style"`, so CssClass and style properties already work on the outer element. ChangePassword and CreateUserWizard also already inherit BaseStyledComponent. + +2. **CausesValidation wiring is internal** — CheckBox, RadioButton, and TextBox all have CausesValidation and ValidationGroup parameters, and they wire to ValidationGroupCoordinator via CascadingParameter. Testing the full validation group triggering requires wrapping in `` + ``, which is already covered by Button tests. The P2 tests verify parameter existence and default values. + +3. **AppendDataBoundItems edge case** — When Items is null and AppendDataBoundItems is false, static items still render (by design — the replace behavior only kicks in when there ARE data-bound items to replace with). + +## Impact + +Team should be aware that Login/ChangePassword/CreateUserWizard BaseStyledComponent inheritance was already in place — WI-52's implementation may have been a no-op or only required template changes to wire `Style`/`CssClass` to the outer element. diff --git a/.ai-team/decisions/inbox/copilot-directive-2026-02-14-ui-overhaul.md b/.ai-team/decisions/inbox/copilot-directive-2026-02-14-ui-overhaul.md deleted file mode 100644 index 9690932e8..000000000 --- a/.ai-team/decisions/inbox/copilot-directive-2026-02-14-ui-overhaul.md +++ /dev/null @@ -1,4 +0,0 @@ -### 2026-02-14: User directive — Sample website UI overhaul -**By:** Jeffrey T. Fritz (via Copilot) -**What:** Improve the UI of the samples/AfterBlazorServerSide website with a modern layout that demos each sample, feature, and component cleanly. Add a search feature. Update integration tests with this overhaul. -**Why:** User request — captured for team memory diff --git a/.ai-team/decisions/inbox/forge-ui-overhaul-scope.md b/.ai-team/decisions/inbox/forge-ui-overhaul-scope.md deleted file mode 100644 index 17c854967..000000000 --- a/.ai-team/decisions/inbox/forge-ui-overhaul-scope.md +++ /dev/null @@ -1,320 +0,0 @@ -# Sample Website UI Overhaul — Scope & Work Breakdown - -**Author:** Forge -**Date:** 2026-02-13 -**Requested by:** Jeffrey T. Fritz - ---- - -## 1. Current State Analysis - -### Layout Structure -- **MainLayout.razor:** Classic sidebar + main content layout - - Fixed 250px sidebar (purple gradient background, sticky) - - Top row with Docs/About links - - Main content area with `@Body` -- **NavMenu.razor:** Uses `TreeView` component for navigation (176 lines of hardcoded TreeNode markup) - - Categories: Home → Utility Features → Editor → Data → Validation → Navigation → Login → Migration Guides - - No search functionality - - TreeView is nested 3-4 levels deep — complex to navigate - -### CSS Framework -- **Bootstrap 4.3.1** (2019 vintage — two major versions behind) -- Custom `site.css` (~200 lines) for layout, sidebar theming, validation states -- Open Iconic icon font (Bootstrap 4 era icons) -- No utility-first CSS — all custom classes - -### Sample Page Organization -- **34 top-level component folders** in `Components/Pages/ControlSamples/` -- Pattern: Each component folder contains `Index.razor` + variant pages + `Nav.razor` for sub-navigation -- No consistent structure — some have 1 page, some have 6+ -- `ComponentList.razor` on homepage shows flat list by category — **manually maintained, out of sync** (missing DetailsView, PasswordRecovery links) - -### Static Assets -- `wwwroot/css/` — Bootstrap + site.css -- `wwwroot/img/` — Sample images for AdRotator, Chart -- No favicon customization, no branding assets - -### Integration Tests -- **4 test files:** `ControlSampleTests.cs`, `InteractiveComponentTests.cs`, `HomePageTests.cs`, `PlaywrightFixture.cs` -- Tests use **semantic selectors** (element types, attributes) not CSS class selectors -- Example: `page.Locator("span[style*='font-weight']")`, `page.QuerySelectorAsync("canvas")` -- **Low risk from CSS changes** — tests don't depend on `.sidebar`, `.page`, etc. - ---- - -## 2. Proposed Design Direction - -### 2.1 Layout Structure - -**Recommendation: Modern sidebar + card-based demo area** - -``` -┌────────────────────────────────────────────────────────────┐ -│ [Logo] BlazorWebFormsComponents [Search: ______] [Docs]│ -├─────────────┬──────────────────────────────────────────────┤ -│ NAVIGATION │ BREADCRUMB: Home > Data Controls > GridView│ -│ ├──────────────────────────────────────────────┤ -│ [Search 🔍] │ ┌─────────────────────────────────────────┐ │ -│ │ │ GridView │ │ -│ ▼ Editor │ │ ─────────────────────────────────────── │ │ -│ Button │ │ Description text from component docs │ │ -│ CheckBox │ └─────────────────────────────────────────┘ │ -│ ... │ │ -│ ▼ Data │ ┌─────────────────────────────────────────┐ │ -│ GridView ←│ │ Live Demo │ │ -│ ListView │ │ ┌─────────────────────────────────────┐ │ │ -│ ... │ │ │ │ │ │ -│ ▼ Validation│ │ └─────────────────────────────────────┘ │ │ -│ ▼ Navigation│ └─────────────────────────────────────────┘ │ -│ ▼ Login │ │ -│ │ ┌─────────────────────────────────────────┐ │ -│ │ │ Code Example [Copy 📋]│ │ -│ │ │
...
│ │ -│ │ └─────────────────────────────────────────┘ │ -│ │ │ -│ │ ┌──────┐ ┌──────┐ ┌──────┐ │ -│ │ │Style │ │Events│ │Paging│ ← sub-pages │ -│ │ └──────┘ └──────┘ └──────┘ │ -└─────────────┴──────────────────────────────────────────────┘ -``` - -**Key changes:** -1. **Persistent top bar** with search input + branding -2. **Collapsible sidebar** with category grouping (current TreeView → simple `
` or Blazor Accordion) -3. **Card-based demo pages** — description card, live demo card, code example card -4. **Sub-page tabs** — replace current `Nav.razor` pattern with horizontal tabs - -### 2.2 CSS Approach - -**Recommendation: Bootstrap 5.3 (latest stable)** - -| Option | Pros | Cons | Verdict | -|--------|------|------|---------| -| **Bootstrap 5.3** | Familiar to team, minimal learning curve, great docs, no jQuery | Needs migration from 4.3 classes | ✅ **RECOMMENDED** | -| Tailwind CSS | Modern, utility-first | Build tooling, different paradigm | ❌ Overkill for sample site | -| FluentUI Blazor | Microsoft ecosystem | Heavy dependency, learning curve | ❌ Different library, confusing | -| Custom CSS only | Full control | Maintenance burden, no responsive grid | ❌ Not worth it | - -**Bootstrap 4→5 breaking changes to address:** -- `ml-*` → `ms-*`, `mr-*` → `me-*` (margin utilities) -- `pl-*` → `ps-*`, `pr-*` → `pe-*` (padding utilities) -- `data-toggle` → `data-bs-toggle` (JS attributes — not used) -- `form-group` → `mb-3` (form layout) -- `.close` → `.btn-close` (close buttons) -- No jQuery dependency (already not using it) - -### 2.3 Component Organization - -**Current:** Flat navigation duplicated in TreeView (NavMenu) + ComponentList + manual sample pages - -**Proposed:** -1. **Single source of truth:** `ComponentCatalog.json` or static class with component metadata: - ```json - { - "components": [ - { - "name": "Button", - "category": "Editor", - "route": "/ControlSamples/Button", - "description": "Server-side button control", - "subPages": ["Style", "Events", "JavaScript"] - } - ] - } - ``` -2. **Auto-generate NavMenu** from catalog -3. **Auto-generate ComponentList** from catalog -4. **Template-driven sample pages** — reduce boilerplate - -### 2.4 Search Implementation - -**Recommendation: Client-side search with Fuse.js or similar** - -| Approach | Pros | Cons | Verdict | -|----------|------|------|---------| -| **Client-side JS (Fuse.js)** | Zero server load, instant results, works offline | 50KB+ bundle, client rendering | ✅ **RECOMMENDED** | -| Blazor input + filter | No JS, type-safe | Re-renders on every keystroke | ⚠️ Viable fallback | -| Server-side API | Scalable | Overkill for <100 pages | ❌ Unnecessary | - -**Implementation:** -1. Generate `search-index.json` at build time from component catalog -2. Include component name, category, description, keywords -3. Fuse.js fuzzy search with highlighting -4. Results show in dropdown below search input -5. Keyboard navigation (arrow keys + Enter) - ---- - -## 3. Work Breakdown - -| ID | Title | Owner | Size | Dependencies | Notes | -|----|-------|-------|------|--------------|-------| -| UI-1 | Upgrade Bootstrap 4.3→5.3 | Jubilee | M | — | Replace CSS files, update utility classes in site.css | -| UI-2 | Create ComponentCatalog data source | Cyclops | S | — | JSON or static class with all 50+ components | -| UI-3 | Redesign MainLayout.razor | Jubilee | M | UI-1 | New layout structure, top bar, breadcrumbs | -| UI-4 | Redesign NavMenu from catalog | Jubilee | M | UI-2, UI-3 | Replace TreeView with Bootstrap 5 Accordion | -| UI-5 | Create SamplePageTemplate | Jubilee | M | UI-3 | Card layout: description, demo, code, sub-tabs | -| UI-6 | Migrate sample pages to template | Jubilee | L | UI-5 | 34 component folders, ~80 pages total | -| UI-7 | Update ComponentList.razor | Jubilee | S | UI-2 | Generate from catalog, add missing components | -| UI-8 | Implement search (Fuse.js) | Cyclops | M | UI-2 | Index generation, search component, dropdown | -| UI-9 | Update integration tests | Colossus | M | UI-3, UI-4 | Verify all routes, update any broken selectors | -| UI-10 | Add dark mode toggle | Jubilee | S | UI-1 | Bootstrap 5 color modes, localStorage persistence | -| UI-11 | Update branding/favicon | Beast | S | — | BlazorWebFormsComponents logo, favicon.ico | -| UI-12 | Documentation for new layout | Beast | S | UI-6 | Update any docs referencing sample site | - -### Dependency Graph - -``` - ┌──────┐ - │ UI-1 │ Bootstrap upgrade (Jubilee) - └──┬───┘ - │ - ┌───────┼────────┐ - ▼ ▼ ▼ -┌──────┐ ┌──────┐ ┌──────┐ -│ UI-2 │ │ UI-3 │ │UI-10 │ -│Catalog│ │Layout│ │Dark │ -│(Cyc) │ │(Jub) │ │Mode │ -└──┬───┘ └──┬───┘ └──────┘ - │ │ - ├────────┤ - ▼ ▼ -┌──────┐ ┌──────┐ -│ UI-4 │ │ UI-5 │ -│NavMenu│ │Templat│ -│(Jub) │ │(Jub) │ -└──┬───┘ └──┬───┘ - │ │ - │ ▼ - │ ┌──────┐ - │ │ UI-6 │ - │ │Migrate│ - │ │(Jub) │ - │ └──┬───┘ - │ │ - ├────────┤ - ▼ ▼ -┌──────┐ ┌──────┐ -│ UI-7 │ │ UI-8 │ -│CompLst│ │Search│ -│(Jub) │ │(Cyc) │ -└──────┘ └──┬───┘ - │ - ▼ - ┌──────┐ - │ UI-9 │ - │Tests │ - │(Col) │ - └──────┘ -``` - -### Parallel Execution Plan - -**Phase 1 (parallel):** -- UI-1: Bootstrap upgrade -- UI-2: ComponentCatalog -- UI-11: Branding - -**Phase 2 (after Phase 1):** -- UI-3: MainLayout redesign -- UI-10: Dark mode - -**Phase 3 (after Phase 2):** -- UI-4: NavMenu -- UI-5: SamplePageTemplate - -**Phase 4 (after Phase 3):** -- UI-6: Migrate pages (largest item) -- UI-7: ComponentList -- UI-8: Search - -**Phase 5 (after Phase 4):** -- UI-9: Integration tests -- UI-12: Documentation - ---- - -## 4. Risk Assessment - -### 4.1 Integration Test Breakage Risk: **LOW** - -Current tests use: -- Element selectors: `button`, `input[type='submit']`, `canvas`, `table`, `a`, `li` -- Attribute selectors: `span[style*='font-weight']`, `img[src='/img/CSharp.png']` -- ID selectors: `#event-count`, `#event-details` -- Class selectors: `.item-row`, `.alt-item-row` (component output, not layout) - -**No layout CSS class selectors found in tests.** Tests target component output, not page structure. - -**Mitigation:** UI-9 (Colossus) runs full test suite after each major phase. Fix any breakage immediately. - -### 4.2 Hardcoded Selectors: **MEDIUM** - -Found hardcoded patterns: -- `NavMenu.razor` line 6: `navbar-brand` class (Bootstrap 4) -- `ComponentList.razor` line 66: `col-md=3` (typo! should be `col-md-3`) -- `site.css` references `.sidebar`, `.page`, `.main`, `.top-row` - -**Mitigation:** UI-3 (MainLayout) and UI-4 (NavMenu) will replace these classes. Grep for all Bootstrap 4 class usages before Phase 2. - -### 4.3 Search Implementation: **MEDIUM** - -Client-side search requires: -1. JS interop for Fuse.js (first non-Chart JS in sample app) -2. Build-time index generation (manual or automated) -3. Keyboard navigation UX - -**Mitigation:** -- Use existing JS interop patterns from Chart component -- Start with manual index; automate later if needed -- Keep scope to basic dropdown; no fancy UX - -### 4.4 Large Migration Scope: **HIGH** - -UI-6 touches ~80 files across 34 component folders. Risk of: -- Inconsistent migration -- Broken links -- Lost sample code - -**Mitigation:** -- Create template first (UI-5) -- Migrate 2-3 components as pilot (Button, GridView, Calendar) -- Review pilot with Jeff before proceeding -- Use checklist to track progress - -### 4.5 Bootstrap 4→5 Breaking Changes: **LOW** - -Most changes are utility class renames. No jQuery dependency to remove. - -**Mitigation:** -- Run `grep -r "ml-\|mr-\|pl-\|pr-"` to find all usages -- Batch replace with `ms-`/`me-`/`ps-`/`pe-` -- Verify responsive behavior after upgrade - ---- - -## 5. Open Questions for Jeff - -1. **Dark mode priority?** UI-10 is nice-to-have. Include in Phase 2 or defer? -2. **Search scope?** Component names only, or also search within docs/descriptions? -3. **Branding assets?** Do you have a BlazorWebFormsComponents logo, or should Beast create one? -4. **Migration guide updates?** Should we update the MasterPages migration guide to reference the new layout? - ---- - -## 6. Recommendation - -**Proceed with UI-1, UI-2, UI-11 in parallel immediately.** These are foundational and have no dependencies. - -**Estimated total effort:** 3-4 sprints (assuming 2-day sprints) -- Phase 1-2: 1 sprint -- Phase 3: 1 sprint -- Phase 4: 1-2 sprints (UI-6 is large) -- Phase 5: 0.5 sprint - -**Owners:** -- Jubilee: UI-1, UI-3, UI-4, UI-5, UI-6, UI-7, UI-10 (frontend lead) -- Cyclops: UI-2, UI-8 (catalog + search logic) -- Colossus: UI-9 (integration tests) -- Beast: UI-11, UI-12 (branding + docs) diff --git a/.ai-team/skills/base-class-upgrade/SKILL.md b/.ai-team/skills/base-class-upgrade/SKILL.md new file mode 100644 index 000000000..9e6fbe851 --- /dev/null +++ b/.ai-team/skills/base-class-upgrade/SKILL.md @@ -0,0 +1,63 @@ +# Skill: Upgrading a Control to BaseStyledComponent + +## When to Use +When a component currently inherits `BaseWebFormsComponent` and needs IStyle properties (BackColor, BorderColor, BorderStyle, BorderWidth, CssClass, Font, ForeColor, Height, Width) on its outer rendered element. + +## Steps + +### 1. Change the base class in `.razor.cs` +```csharp +// Before +public partial class MyControl : BaseWebFormsComponent +// After +public partial class MyControl : BaseStyledComponent +``` + +### 2. Change `@inherits` in `.razor` +```razor +@* Before *@ +@inherits BaseWebFormsComponent +@* After *@ +@inherits BaseStyledComponent +``` + +### 3. Apply styles to the outer HTML element +```html + +
+``` + +### 4. Add the `GetCssClassOrNull()` helper +Either in `@code {}` block or in `.razor.cs`: +```csharp +private string GetCssClassOrNull() +{ + return !string.IsNullOrEmpty(CssClass) ? CssClass : null; +} +``` +Returning null when empty omits the `class` attribute from rendered HTML entirely. + +### 5. Add Font attribute parsing in `HandleUnknownAttributes()` +```csharp +protected override void HandleUnknownAttributes() +{ + // existing sub-style attribute parsing... + this.SetFontsFromAttributes(AdditionalAttributes); + base.HandleUnknownAttributes(); +} +``` + +### 6. Remove duplicate IStyle properties +If the component previously declared its own `CssClass`, `BackColor`, etc., remove them — they now come from `BaseStyledComponent`. + +## Key Facts +- `BaseStyledComponent` extends `BaseWebFormsComponent` — no functionality is lost. +- `Style` property on `BaseStyledComponent` is `protected string` (computed getter), not a `[Parameter]`. +- `[Parameter]` outer styles do NOT conflict with `[CascadingParameter]` sub-styles. +- `this.ToStyle()` extension (from `HasStyleExtensions`) builds inline CSS from all IStyle properties. +- The `GetCssClassOrNull()` pattern ensures clean HTML — no empty `class=""` attributes. + +## Reference Implementations +- **Simple control:** `Label.razor` / `Label.razor.cs` (WI-17) +- **Login controls with sub-styles:** `Login.razor.cs`, `ChangePassword.razor.cs`, `CreateUserWizard.razor.cs` (WI-52) +- **Data controls:** `DataList.razor.cs` (WI-07 via BaseDataBoundComponent) diff --git a/.ai-team/skills/shared-base-extraction/SKILL.md b/.ai-team/skills/shared-base-extraction/SKILL.md new file mode 100644 index 000000000..f2ccfb76f --- /dev/null +++ b/.ai-team/skills/shared-base-extraction/SKILL.md @@ -0,0 +1,44 @@ +# Skill: Extracting a Shared Base Class from Sibling Components + +**confidence:** low +**source:** earned + +## When to Use +When multiple components in the same inheritance tier share identical properties, methods, or logic that should live in a common base class. Signs: 3+ components with copy-pasted `[Parameter]` declarations and helper methods. + +## Steps + +### 1. Identify the duplicated surface +List every property, method, and field that appears identically (or near-identically) across the sibling components. Only consolidate members that are truly shared by ALL siblings. + +### 2. Create the intermediate base class +Place it in the same directory/namespace as its parent. Inherit from the existing shared parent: +```csharp +public class BaseListControl : DataBoundComponent +``` + +### 3. Move shared members to the base +- `[Parameter]` properties: move as-is +- Helper methods: make `protected` if subclasses or `.razor` templates call them, `private` otherwise +- New feature parameters go here too — that's often the motivation for the extraction + +### 4. Update each subclass +- Change `: OldParent` to `: NewBase` in `.razor.cs` +- Change `@inherits OldParent` to `@inherits NewBase` in `.razor` +- Remove the now-inherited members from each subclass +- Keep subclass-specific members untouched + +### 5. Build and verify +A successful build confirms no accidental member hiding or missing references. Watch for CS0263 (partial class base mismatch) — both `.razor` and `.razor.cs` must agree on the base class. + +## Key Facts +- In Blazor, `@inherits` in the `.razor` file and `: BaseClass` in the `.razor.cs` must specify the same type for partial classes. +- `protected` methods in the base are accessible from `.razor` template code (`@code` blocks and inline expressions). +- When creating new `ListItem` copies to apply formatting, preserve all properties (Text, Value, Selected, Enabled) to avoid data loss. +- Apply rendering transforms (like format strings) in the shared `GetItems()` at render time, not at bind time, so they affect both static and data-bound items. + +## Checklist +- [ ] All sibling components compile with the new base +- [ ] No duplicate `[Parameter]` declarations remain in subclasses +- [ ] Subclass-specific members are preserved +- [ ] `@inherits` directives updated in all `.razor` files diff --git a/docs/DataControls/FormView.md b/docs/DataControls/FormView.md index 3999061f6..a87addf81 100644 --- a/docs/DataControls/FormView.md +++ b/docs/DataControls/FormView.md @@ -7,12 +7,16 @@ The FormView component is meant to emulate the asp:FormView control in markup an - OnDataBinding and OnDataBound events trigger - ModeChanging and ModeChanged events - Insert, Edit, Update, Delete actions and supporting events + - **HeaderText** / **HeaderTemplate** - Renders a header row above the form content. `HeaderTemplate` takes precedence over `HeaderText` when both are specified. + - **FooterText** / **FooterTemplate** - Renders a footer row below the form content. `FooterTemplate` takes precedence over `FooterText` when both are specified. + - **EmptyDataText** / **EmptyDataTemplate** - Displays content when the data source is empty or null. `EmptyDataTemplate` takes precedence over `EmptyDataText` when both are specified. ## Usage Notes - **ItemType attribute** - Required to specify the type of items in the collection - **Context attribute** - For Web Forms compatibility, use `Context="Item"` to access the current item as `@Item` in templates (ItemTemplate, EditItemTemplate, InsertItemTemplate) instead of Blazor's default `@context` - **ID** - Use `@ref` instead of `ID` when referencing the component in code +- **Template precedence** - When both a text property and its corresponding template are set, the template always takes precedence (e.g., `HeaderTemplate` overrides `HeaderText`) ## Web Forms Declarative Syntax @@ -145,3 +149,205 @@ The FormView component is meant to emulate the asp:FormView control in markup an ``` ## Blazor Syntax + +``` html + + OnDataBinding=EventCallBack + OnDataBound=EventCallBack + OnItemDeleting=EventCallBack + OnItemDeleted=EventCallBack + OnItemInserting=EventCallBack + OnItemInserted=EventCallBack + OnItemUpdating=EventCallBack + OnItemUpdated=EventCallBack + SelectMethod=SelectHandler + Visible=bool +> + + + + + + + + + + + + + + + + + + + +``` + +## Examples + +### Basic FormView with Header, Footer, and Empty State + +```razor + + +

Name: @Item.Name

+

Title: @Item.Title

+
+
+ +@code { + private List Employees = new() + { + new Employee { Name = "Jane Smith", Title = "Developer" } + }; + + public class Employee + { + public string Name { get; set; } = ""; + public string Title { get; set; } = ""; + } +} +``` + +### Custom Header and Footer Templates + +```razor + + +
+

Product Information

+
+
+
+ +

Product: @Item.Name

+

Price: @Item.Price.ToString("C")

+
+ + + +
+ +@code { + private List Products = new() + { + new Product { Name = "Widget", Price = 9.99m } + }; + + public class Product + { + public string Name { get; set; } = ""; + public decimal Price { get; set; } + } +} +``` + +### Empty Data Template + +```razor +@* Shows custom content when no data is available *@ + + +

Order #@Item.Id — @Item.Description

+
+ +
+ No orders found. Try adjusting your search criteria. +
+
+
+ +@code { + private List FilteredItems = new(); // empty list + + public class Order + { + public int Id { get; set; } + public string Description { get; set; } = ""; + } +} +``` + +### Migration Example: Header and Footer + +**Web Forms:** +```aspx + + +

<%# Eval("Name") %>

+
+
+``` + +**Blazor:** +```razor + + +

@Item.Name

+
+
+``` + +**Web Forms (with templates):** +```aspx + + +

Customer Record

+
+ +

<%# Eval("Name") %>

+
+ +

No data found.

+
+ + End of record + +
+``` + +**Blazor:** +```razor + + +

Customer Record

+
+ +

@Item.Name

+
+ +

No data found.

+
+ + End of record + +
+``` + +## See Also + +- [GridView](GridView.md) - For tabular data display +- [DetailsView](DetailsView.md) - Similar single-record display +- [DataList](DataList.md) - For repeating data templates diff --git a/docs/DataControls/GridView.md b/docs/DataControls/GridView.md index 90acbb959..b94c0cbd0 100644 --- a/docs/DataControls/GridView.md +++ b/docs/DataControls/GridView.md @@ -4,12 +4,23 @@ The GridView component is meant to emulate the asp:GridView control in markup an - Readonly grid - Bound, Button, Hyperlink, and Template columns +- **Paging** - `AllowPaging`, `PageSize`, `PageIndex`, `PageIndexChanged` event +- **Sorting** - `AllowSorting`, `SortExpression`, `SortDirection`, `Sorting`/`Sorted` events +- **Row Editing** - `EditIndex`, `RowEditing`, `RowUpdating`, `RowDeleting`, `RowCancelingEdit` events ### Blazor Notes - The `RowCommand.CommandSource` object will be populated with the `ButtonField` object - **Context attribute** - When using ``, add `Context="Item"` to access the current row item as `@Item` instead of Blazor's default `@context` - **ItemType cascading** - The `ItemType` parameter is automatically cascaded from the GridView to child columns. You only need to specify it once on the GridView, and all child columns (BoundField, TemplateField, HyperLinkField, ButtonField) will automatically infer the type. For backward compatibility, you can still explicitly specify `ItemType` on individual columns if desired. +- **Paging** - When `AllowPaging="true"`, the GridView automatically paginates the data source using `Skip()`/`Take()`. A numeric pager is rendered below the grid. The `PageIndexChanged` event fires with a `PageChangedEventArgs` containing `NewPageIndex`, `OldPageIndex`, `TotalPages`, `StartRowIndex`, and `Cancel`. +- **Sorting** - When `AllowSorting="true"`, column headers become clickable. You must handle the `Sorting` event to apply the sort to your data source. The `Sorted` event fires after the sort completes. Both events use `GridViewSortEventArgs` with `SortExpression`, `SortDirection`, and `Cancel` properties. +- **Row Editing** - Set `EditIndex` to the zero-based row index to put a row in edit mode (`-1` means no row is being edited). An auto-generated command column appears when at least one editing event callback is registered. + +!!! warning "Differences from Web Forms" + - `PageIndexChanging` is not implemented; use `PageIndexChanged` with the `Cancel` property instead. + - `PagerTemplate` is not yet supported; only the built-in numeric pager is available. + - Sorting does not automatically re-sort the data source; you must handle the `Sorting` event and apply the sort yourself. ## Web Forms Declarative Syntax @@ -349,10 +360,13 @@ Currently, not every syntax element of Web Forms GridView is supported. In the m ``` html + PageSize=int + RowCancelingEdit=EventCallBack + RowDeleting=EventCallBack + RowEditing=EventCallBack + RowUpdating=EventCallBack SelectMethod=SelectHandler + SortDirection=SortDirection + SortExpression=string + Sorted=EventCallBack + Sorting=EventCallBack TabIndex=int Visible=bool > @@ -408,3 +433,163 @@ Currently, not every syntax element of Web Forms GridView is supported. In the m ``` + +## Examples + +### Basic Paging + +```razor +@* GridView with paging enabled, 5 items per page *@ + + +

Page @(currentPageIndex + 1)

+ +@code { + private List Products = new(); + private int currentPageIndex = 0; + + private void HandlePageChanged(PageChangedEventArgs e) + { + currentPageIndex = e.NewPageIndex; + } +} +``` + +### Sorting + +```razor +@* GridView with sortable columns *@ + + + + + + + +@code { + private List Products = new(); + private string sortExpression = ""; + private SortDirection sortDirection = SortDirection.Ascending; + + private IEnumerable SortedProducts => sortExpression switch + { + "Name" => sortDirection == SortDirection.Ascending + ? Products.OrderBy(p => p.Name) + : Products.OrderByDescending(p => p.Name), + "Price" => sortDirection == SortDirection.Ascending + ? Products.OrderBy(p => p.Price) + : Products.OrderByDescending(p => p.Price), + _ => Products + }; + + private void HandleSorting(GridViewSortEventArgs e) + { + sortExpression = e.SortExpression; + sortDirection = e.SortDirection; + } +} +``` + +### Row Editing + +```razor +@* GridView with inline edit, update, delete, and cancel *@ + + + + + + + +@code { + private List Products = new(); + private int editIndex = -1; + + private void HandleEdit(GridViewEditEventArgs e) + { + editIndex = e.NewEditIndex; + } + + private void HandleUpdate(GridViewUpdateEventArgs e) + { + // Apply changes to Products[e.RowIndex] + editIndex = -1; + } + + private void HandleDelete(GridViewDeleteEventArgs e) + { + Products.RemoveAt(e.RowIndex); + editIndex = -1; + } + + private void HandleCancelEdit(GridViewCancelEditEventArgs e) + { + editIndex = -1; + } +} +``` + +### Paging with Sorting + +```razor +@* Combining paging and sorting *@ + + + + + + + +@code { + private List Products = new(); + private int currentPageIndex = 0; + private string sortExpression = ""; + private SortDirection sortDirection = SortDirection.Ascending; + + private IEnumerable SortedProducts => sortExpression switch + { + "Name" => sortDirection == SortDirection.Ascending + ? Products.OrderBy(p => p.Name) + : Products.OrderByDescending(p => p.Name), + "Price" => sortDirection == SortDirection.Ascending + ? Products.OrderBy(p => p.Price) + : Products.OrderByDescending(p => p.Price), + _ => Products + }; + + private void HandlePageChanged(PageChangedEventArgs e) + { + currentPageIndex = e.NewPageIndex; + } + + private void HandleSorting(GridViewSortEventArgs e) + { + sortExpression = e.SortExpression; + sortDirection = e.SortDirection; + } +} +``` diff --git a/docs/EditorControls/Calendar.md b/docs/EditorControls/Calendar.md index 042c7e228..fd005c385 100644 --- a/docs/EditorControls/Calendar.md +++ b/docs/EditorControls/Calendar.md @@ -25,14 +25,19 @@ Original Microsoft documentation: https://docs.microsoft.com/en-us/dotnet/api/sy - Customizable navigation text (`NextMonthText`, `PrevMonthText`, `SelectWeekText`, `SelectMonthText`) - First day of week configuration (`FirstDayOfWeek`) - Cell padding and spacing options -- Style attributes for different day types: - - `TitleStyleCss` - Title bar style - - `DayHeaderStyleCss` - Day header style - - `DayStyleCss` - Regular day style - - `TodayDayStyleCss` - Today's date style - - `SelectedDayStyleCss` - Selected date style - - `OtherMonthDayStyleCss` - Days from other months style - - `WeekendDayStyleCss` - Weekend day style +- **TableItemStyle sub-components** for rich styling (preferred): + - `` - Regular day cells + - `` - Title bar + - `` - Day name headers + - `` - Today's date cell + - `` - Selected date cell + - `` - Days from adjacent months + - `` - Weekend day cells + - `` - Next/previous navigation links + - `` - Week/month selector column +- Each TableItemStyle sub-component supports: `CssClass`, `BackColor`, `ForeColor`, `BorderColor`, `BorderStyle`, `BorderWidth`, `Height`, `Width`, `HorizontalAlign`, `VerticalAlign`, `Wrap`, `Font-Bold`, `Font-Italic`, `Font-Size`, `Font-Name`, etc. +- Legacy CSS string properties (deprecated but still functional): + - `TitleStyleCss`, `DayHeaderStyleCss`, `DayStyleCss`, `TodayDayStyleCss`, `SelectedDayStyleCss`, `OtherMonthDayStyleCss`, `WeekendDayStyleCss`, `NextPrevStyleCss`, `SelectorStyleCss` - `Visible` property to show/hide the calendar - `CssClass` for custom CSS styling - `ToolTip` for accessibility @@ -43,7 +48,9 @@ Original Microsoft documentation: https://docs.microsoft.com/en-us/dotnet/api/sy - `Caption` and `CaptionAlign` properties not implemented - `TodaysDate` property not implemented (use `DateTime.Today`) - `UseAccessibleHeader` not implemented -- Individual style objects (`DayStyle`, `TitleStyle`, etc.) not supported - use CSS class names instead + +!!! note "Style Properties Migration" + Individual style objects (`DayStyle`, `TitleStyle`, etc.) from Web Forms are now supported via **TableItemStyle sub-components** such as ``, ``, etc. The older CSS string properties (`DayStyleCss`, `TitleStyleCss`, etc.) still work but are **deprecated**. See the [migration example](#migrating-from-css-string-properties-to-tableitemstyle) below. ## Web Forms Declarative Syntax @@ -107,16 +114,26 @@ Original Microsoft documentation: https://docs.microsoft.com/en-us/dotnet/api/sy ShowTitle="True|False" TitleFormat="Month|MonthYear" VisibleDate="DateTime" - TitleStyleCss="string" - DayStyleCss="string" - TodayDayStyleCss="string" - SelectedDayStyleCss="string" - OtherMonthDayStyleCss="string" - WeekendDayStyleCss="string" - DayHeaderStyleCss="string" - CssClass="string" /> + CssClass="string"> + + @* TableItemStyle sub-components (preferred) *@ + + + + + + + + + + + ``` +!!! warning "Deprecated CSS String Properties" + The following properties still work but are deprecated. Use the TableItemStyle sub-components above instead: + `TitleStyleCss`, `DayStyleCss`, `TodayDayStyleCss`, `SelectedDayStyleCss`, `OtherMonthDayStyleCss`, `WeekendDayStyleCss`, `DayHeaderStyleCss`, `NextPrevStyleCss`, `SelectorStyleCss` + ## Usage Examples ### Basic Calendar @@ -173,7 +190,21 @@ Original Microsoft documentation: https://docs.microsoft.com/en-us/dotnet/api/sy @bind-SelectedDate="selectedDate" /> ``` -### Styled Calendar +### Styled Calendar (using TableItemStyle sub-components) + +```razor + + + + + + + + + +``` + +### Styled Calendar (legacy CSS string properties — deprecated) ```razor - - + OnSelectionChanged="HandleSelectionChanged"> + + ``` ```csharp @@ -313,12 +338,41 @@ protected void Calendar1_SelectionChanged(object sender, EventArgs e) } ``` +### Migrating from CSS String Properties to TableItemStyle + +If you previously used the CSS string properties, migrate to the new sub-components: + +**Before (deprecated):** +```razor + +``` + +**After (preferred):** +```razor + + + + + + + +``` + +!!! tip "Best Practice" + The TableItemStyle sub-components also support inline style properties like `BackColor`, `ForeColor`, and `Font-Bold` — matching the Web Forms ``, ``, etc. child elements. This makes migration from Web Forms markup even more direct. + ### Key Differences -1. **Style Properties**: Use CSS classes instead of inline style objects -2. **Event Handlers**: Use EventCallback pattern instead of event delegates -3. **Data Binding**: Use `@bind-SelectedDate` for two-way binding -4. **Day Rendering**: The `OnDayRender` event provides day information but cannot inject custom HTML into cells +1. **Style Properties**: Use TableItemStyle sub-components (``, ``, etc.) for a direct match to Web Forms style child elements, or use `CssClass` for CSS-based styling. Legacy CSS string properties (`DayStyleCss`, etc.) are deprecated. +2. **DayNameFormat / TitleFormat**: Use enum values directly — `DayNameFormat="Full"`, `DayNameFormat="Short"`, `DayNameFormat="FirstLetter"`, `DayNameFormat="FirstTwoLetters"`, `DayNameFormat="Shortest"` for day names; `TitleFormat="Month"` or `TitleFormat="MonthYear"` for the title. +3. **Event Handlers**: Use EventCallback pattern instead of event delegates +4. **Data Binding**: Use `@bind-SelectedDate` for two-way binding +5. **Day Rendering**: The `OnDayRender` event provides day information but cannot inject custom HTML into cells ## Common Scenarios diff --git a/docs/NavigationControls/HyperLink.md b/docs/NavigationControls/HyperLink.md index 30c05f580..3849295bf 100644 --- a/docs/NavigationControls/HyperLink.md +++ b/docs/NavigationControls/HyperLink.md @@ -3,7 +3,7 @@ It may seem strange that we have a HyperLink component when there already is an ## Blazor Features Supported ### Core Properties -- `NavigationUrl` - the URL to link when HyperLink component is clicked +- `NavigateUrl` - the URL to link when HyperLink component is clicked - `Text` - the text content of the HyperLink component - `Target` - the target window or frame in which to display the Web page content linked to when the HyperLink component is clicked (e.g., "_blank", "_self", "_parent", "_top") - `ToolTip` - displays a tooltip on hover (rendered as the `title` attribute) @@ -63,7 +63,7 @@ It may seem strange that we have a HyperLink component when there already is an ### Blazor Syntax ```html - ``` -### Blazor without NavigationUrl (renders as plain anchor) +### Blazor without NavigateUrl (renders as plain anchor) ```html diff --git a/planning-docs/MILESTONE6-PLAN.md b/planning-docs/MILESTONE6-PLAN.md new file mode 100644 index 000000000..7b4cfabf3 --- /dev/null +++ b/planning-docs/MILESTONE6-PLAN.md @@ -0,0 +1,179 @@ +# Milestone 6 — Feature Gap Closure Work Plan + +**Created:** 2026-02-14 +**Author:** Forge (Lead / Web Forms Reviewer) +**Branch:** `milestone6/feature-implementation` +**Baseline:** dev (post-PR #341 M4/M5 merge + PR #340 sprint3 merge) + +--- + +## Goals + +Close the highest-impact feature gaps identified in the 53-control audit (SUMMARY.md). Milestone 6 focuses on **base class fixes that sweep across many controls** (P0) and **individual control improvements for the weakest data controls** (P1). Substitution and Xml remain deferred. + +### Resolved Pre-Conditions + +- ✅ `sprint3/detailsview-passwordrecovery` merged to dev via PR #340 — DetailsView and PasswordRecovery are available +- ✅ Chart component + audit merged via PR #341 +- ⚠️ PasswordRecovery audit doc (`planning-docs/PasswordRecovery.md`) shows 0% because audit ran before merge — needs re-audit (WI-43) + +--- + +## Work Items + +### P0 — Base Class Fixes (~180 gaps closed across 7 changes) + +| ID | Title | Description | Agent | Dependencies | Size | Priority | +|----|-------|-------------|-------|-------------|------|----------| +| WI-01 | Add `AccessKey` to `BaseWebFormsComponent` | Add `[Parameter] public string AccessKey { get; set; }` to `src/BlazorWebFormsComponents/BaseWebFormsComponent.cs`. Render as `accesskey` HTML attribute. This property is inherited by all 53 controls. ~40 gaps closed. | Cyclops | — | S | P0 | +| WI-02 | Tests for `AccessKey` on base class | Add bUnit tests in a new `BaseWebFormsComponentAccessKeyTests` file verifying `accesskey` attribute renders on representative controls (Button, Label, GridView). Test empty string = no attribute. | Rogue | WI-01 | S | P0 | +| WI-03 | Verify `AccessKey` in samples | Spot-check 3 existing sample pages (Button, Calendar, GridView) to confirm no rendering regressions from the new attribute. Add one `AccessKey` usage example to Button sample. | Jubilee | WI-01 | S | P0 | +| WI-04 | Add `ToolTip` to `BaseWebFormsComponent` | Add `[Parameter] public string ToolTip { get; set; }` to `src/BlazorWebFormsComponents/BaseWebFormsComponent.cs`. Render as `title` HTML attribute. Controls that already implement ToolTip directly (Button, Calendar, DataList, FileUpload, HyperLink, Image, ImageButton, ImageMap) must be checked for conflicts — remove duplicates, let base class handle it. ~35 gaps closed. | Cyclops | — | M | P0 | +| WI-05 | Tests for `ToolTip` on base class | bUnit tests verifying `title` attribute on representative controls. Test that controls with existing ToolTip still work (no double-render). Test empty = no attribute. | Rogue | WI-04 | S | P0 | +| WI-06 | Verify `ToolTip` in samples | Confirm no regressions on controls that already had ToolTip (Calendar, Button, Image). Add ToolTip example to one sample page. | Jubilee | WI-04 | S | P0 | +| WI-07 | Make `DataBoundComponent` inherit `BaseStyledComponent` | Change `src/BlazorWebFormsComponents/DataBinding/DataBoundComponent.cs` (or its parent `BaseDataBoundComponent`) to inherit `BaseStyledComponent` instead of `BaseWebFormsComponent`. Verify compilation of all data controls: DataGrid, DetailsView, FormView, GridView, ListView. DataList already has IStyle — verify no conflict. Remove any duplicate CssClass declarations on controls that added it directly (DetailsView, GridView). ~70 gaps closed across 5+ controls. | Cyclops | — | M | P0 | +| WI-08 | Tests for `DataBoundComponent` style properties | bUnit tests on GridView and FormView verifying BackColor, CssClass, ForeColor, Width, Height render correctly after inheritance change. Test that DataList's explicit IStyle still works. | Rogue | WI-07 | M | P0 | +| WI-09 | Verify data control samples with styles | Check GridView, FormView, DetailsView, ListView sample pages for rendering regressions. Add one style example (CssClass + BackColor) to GridView sample. | Jubilee | WI-07 | S | P0 | +| WI-10 | Add `Display` property to `BaseValidator` | Add `ValidatorDisplay` enum (`None=0, Static=1, Dynamic=2`) in `src/BlazorWebFormsComponents/Enums/`. Add `[Parameter] public ValidatorDisplay Display` to `src/BlazorWebFormsComponents/Validations/BaseValidator.razor.cs`. Default: `Static`. When `None`, render `style="display:none"`. When `Dynamic`, render `style="display:none"` when valid, visible when invalid. When `Static`, render `style="visibility:hidden"` when valid (reserves space). 6 gaps closed. | Cyclops | — | S | P0 | +| WI-11 | Tests for `Display` on validators | bUnit tests on RequiredFieldValidator for each Display mode. Verify `style` attribute output for valid/invalid states in Static vs Dynamic vs None modes. | Rogue | WI-10 | S | P0 | +| WI-12 | Verify validator samples with Display | Update one validator sample page to demonstrate `Display="Dynamic"` usage. | Jubilee | WI-10 | S | P0 | +| WI-13 | Add `SetFocusOnError` to `BaseValidator` | Add `[Parameter] public bool SetFocusOnError { get; set; }` to `BaseValidator.razor.cs`. When true and validation fails, call `JSRuntime.InvokeVoidAsync("eval", "document.getElementById('X').focus()")` targeting the validated control. 6 gaps closed. | Cyclops | — | S | P0 | +| WI-14 | Tests for `SetFocusOnError` | bUnit tests verifying the parameter exists and is false by default. JS interop mock test for focus call when validation fails with `SetFocusOnError=true`. | Rogue | WI-13 | S | P0 | +| WI-15 | Change `Image` base class to `BaseStyledComponent` | Change `src/BlazorWebFormsComponents/Image.razor.cs` to inherit `BaseStyledComponent` instead of `BaseWebFormsComponent`. Verify `IImageComponent` still works. Update `.razor` to use `CombinedStyle` for style rendering. 11 gaps closed. Aligns Image with ImageMap (already fixed). | Cyclops | — | S | P0 | +| WI-16 | Tests for `Image` style properties | bUnit tests for BackColor, CssClass, Font, ForeColor, Height, Width, BorderColor, BorderStyle, BorderWidth on Image component. | Rogue | WI-15 | S | P0 | +| WI-17 | Change `Label` base class to `BaseStyledComponent` | Change `src/BlazorWebFormsComponents/Label.razor.cs` to inherit `BaseStyledComponent` instead of `BaseWebFormsComponent`. Update `.razor` to use `CombinedStyle`. 11 gaps closed. | Cyclops | — | S | P0 | +| WI-18 | Tests for `Label` style properties | bUnit tests for style properties on Label (same pattern as WI-16). | Rogue | WI-17 | S | P0 | + +### P1 — Individual Control Improvements (high migration impact) + +| ID | Title | Description | Agent | Dependencies | Size | Priority | +|----|-------|-------------|-------|-------------|------|----------| +| WI-19 | GridView: Add paging | Add `AllowPaging`, `PageSize` (default 10), `PageIndex` (0-based) parameters to `src/BlazorWebFormsComponents/GridView.razor.cs`. Add `PageIndexChanging`/`PageIndexChanged` events with `GridViewPageEventArgs`. Render pager row with numeric links matching DetailsView's existing pager pattern. Add `PagerStyle` (TableItemStyle). | Cyclops | WI-07 | L | P1 | +| WI-20 | GridView paging tests | bUnit tests: paging renders correct page of items, PageIndexChanged fires, PageSize respected, boundary conditions (page 0, last page, empty data). | Rogue | WI-19 | M | P1 | +| WI-21 | GridView paging sample | New sample page `Components/Pages/ControlSamples/GridView/Paging.razor` demonstrating paged GridView with 50+ items. | Jubilee | WI-19 | S | P1 | +| WI-22 | GridView: Add sorting | Add `AllowSorting` (bool), `SortDirection` (`SortDirection` enum: Ascending=0, Descending=1), `SortExpression` (string) parameters. Add `Sorting`/`Sorted` events with `GridViewSortEventArgs`. Render header cells as clickable `
` links when sorting enabled. | Cyclops | WI-07 | M | P1 | +| WI-23 | GridView sorting tests | bUnit tests: sort header renders as links, Sorting event fires with correct expression, toggle ascending/descending. | Rogue | WI-22 | M | P1 | +| WI-24 | GridView sorting sample | New sample page `Components/Pages/ControlSamples/GridView/Sorting.razor` with sortable columns. | Jubilee | WI-22 | S | P1 | +| WI-25 | GridView: Add row editing | Add `EditIndex` (int, default -1), `EditRowStyle` (TableItemStyle). Add events: `RowEditing` (`GridViewEditEventArgs`), `RowUpdating` (`GridViewUpdateEventArgs`), `RowDeleting` (`GridViewDeleteEventArgs`), `RowCancelingEdit` (`GridViewCancelEditEventArgs`). Render Edit/Update/Cancel/Delete command links in edit mode. | Cyclops | WI-07 | M | P1 | +| WI-26 | GridView row editing tests | bUnit tests: EditIndex renders edit template, RowEditing/RowUpdating/RowDeleting events fire, CancelEdit resets EditIndex. | Rogue | WI-25 | M | P1 | +| WI-27 | GridView row editing sample | New sample page `Components/Pages/ControlSamples/GridView/InlineEditing.razor`. | Jubilee | WI-25 | S | P1 | +| WI-28 | Calendar: style strings → TableItemStyle | Convert 9 style properties (DayStyle, TitleStyle, DayHeaderStyle, TodayDayStyle, SelectedDayStyle, OtherMonthDayStyle, WeekendDayStyle, NextPrevStyle, SelectorStyle) from `string` parameters (e.g. `DayStyleCss`) to cascading `TableItemStyle` sub-components matching existing pattern in DetailsView/Login controls. Keep backward compat: old string props → `[Obsolete]`. | Cyclops | — | M | P1 | +| WI-29 | Calendar TableItemStyle tests | bUnit tests verifying each of the 9 style sub-components cascades correctly and renders appropriate CSS. | Rogue | WI-28 | S | P1 | +| WI-30 | Calendar style sample update | Update existing Calendar sample to use `` sub-component syntax instead of string CSS. | Jubilee | WI-28 | S | P1 | +| WI-31 | Calendar: DayNameFormat/TitleFormat → enums | Create `DayNameFormat` enum (Full, Short, FirstLetter, FirstTwoLetters, Shortest) and `TitleFormat` enum (Month, MonthYear) in `src/BlazorWebFormsComponents/Enums/`. Change Calendar parameters from `string` to these enums. | Cyclops | — | S | P1 | +| WI-32 | Calendar enum tests | bUnit tests for each DayNameFormat and TitleFormat value rendering correctly. | Rogue | WI-31 | S | P1 | +| WI-33 | FormView: CssClass, headers, empty data | Add `CssClass` (if not inherited from WI-07), `HeaderText`, `HeaderTemplate`, `FooterText`, `FooterTemplate`, `EmptyDataText`, `EmptyDataTemplate`, `AllowPaging` (explicit bool, default true) to `FormView.razor.cs`. ~15 gaps closed. | Cyclops | WI-07 | M | P1 | +| WI-34 | FormView improvement tests | bUnit tests for header/footer rendering, EmptyDataText/EmptyDataTemplate, CssClass attribute. | Rogue | WI-33 | M | P1 | +| WI-35 | FormView sample update | Update existing FormView sample to demonstrate HeaderText, EmptyDataText, and CssClass. | Jubilee | WI-33 | S | P1 | +| WI-36 | HyperLink: Rename `NavigationUrl` → `NavigateUrl` | Rename the parameter in `src/BlazorWebFormsComponents/HyperLink.razor.cs` from `NavigationUrl` to `NavigateUrl`. Add `[Obsolete] NavigationUrl` property that forwards to `NavigateUrl` for backward compat. Update `.razor` template. 1 gap closed — but a blocking migration name mismatch. | Cyclops | — | S | P1 | +| WI-37 | HyperLink rename tests + sample | Update existing bUnit tests for `NavigateUrl`. Update sample page. Verify `[Obsolete]` forwarding works. | Rogue | WI-36 | S | P1 | +| WI-38 | ValidationSummary: Add HeaderText, ShowSummary, ValidationGroup | Add `HeaderText` (string), `ShowSummary` (bool, default true), `ValidationGroup` (string) to `src/BlazorWebFormsComponents/Validations/AspNetValidationSummary.razor.cs`. HeaderText renders as first `
  • ` or `

    ` above the error list. ValidationGroup filters displayed errors. | Cyclops | — | S | P1 | +| WI-39 | ValidationSummary tests | bUnit tests for HeaderText rendering, ShowSummary=false hides summary, ValidationGroup filtering. | Rogue | WI-38 | S | P1 | +| WI-40 | ValidationSummary sample update | Update validator sample page to demonstrate HeaderText and ValidationGroup on AspNetValidationSummary. | Jubilee | WI-38 | S | P1 | + +### P1 — Documentation & Audit Updates + +| ID | Title | Description | Agent | Dependencies | Size | Priority | +|----|-------|-------------|-------|-------------|------|----------| +| WI-41 | Re-audit PasswordRecovery | Update `planning-docs/PasswordRecovery.md` — the audit shows 0% because files weren't on branch during audit. PasswordRecovery now exists via PR #340. Re-run feature comparison against actual component. Update SUMMARY.md Login Controls totals. | Forge | — | S | P1 | +| WI-42 | GridView docs update | Update `docs/DataControls/GridView.md` to document new paging, sorting, and editing features. Add migration examples for each. | Beast | WI-19, WI-22, WI-25 | M | P1 | +| WI-43 | Calendar docs update | Update `docs/DataControls/Calendar.md` to reflect TableItemStyle sub-components and enum changes. | Beast | WI-28, WI-31 | S | P1 | +| WI-44 | FormView docs update | Update `docs/DataControls/FormView.md` to document new header/footer/empty data features. | Beast | WI-33 | S | P1 | + +### P1 — Integration Tests + +| ID | Title | Description | Agent | Dependencies | Size | Priority | +|----|-------|-------------|-------|-------------|------|----------| +| WI-45 | Integration tests for GridView features | Add Playwright smoke + interaction tests for new GridView sample pages (Paging, Sorting, InlineEditing). Add to `ControlSampleTests.cs` and `InteractiveComponentTests.cs`. | Colossus | WI-21, WI-24, WI-27 | M | P1 | +| WI-46 | Integration tests for updated samples | Add/update Playwright tests for any updated Calendar, FormView, and ValidationSummary sample pages. | Colossus | WI-30, WI-35, WI-40 | S | P1 | + +### P2 — Nice-to-Have + +| ID | Title | Description | Agent | Dependencies | Size | Priority | +|----|-------|-------------|-------|-------------|------|----------| +| WI-47 | ListControl `DataTextFormatString` | Add `DataTextFormatString` parameter to `BaseListControl` (or each ListControl). Applies `string.Format` to item text during rendering. Affects BulletedList, CheckBoxList, DropDownList, ListBox, RadioButtonList. | Cyclops | — | S | P2 | +| WI-48 | ListControl `AppendDataBoundItems` | Add `AppendDataBoundItems` (bool) to `BaseListControl`. When true, data-bound items are appended to statically defined items instead of replacing them. | Cyclops | — | S | P2 | +| WI-49 | CausesValidation on CheckBox, RadioButton, TextBox | Add `CausesValidation` (bool) and `ValidationGroup` (string) parameters to CheckBox, RadioButton, TextBox. Wire to form submission validation. | Cyclops | — | S | P2 | +| WI-50 | Menu `Orientation` property | Add `Orientation` enum (Horizontal, Vertical) to `src/BlazorWebFormsComponents/Menu.razor.cs`. Currently hardcoded to vertical rendering. | Cyclops | — | S | P2 | +| WI-51 | Label `AssociatedControlID` | Add `AssociatedControlID` (string) to Label. Renders `

  • ` wrapper. ~30 gaps closed. | Cyclops | — | M | P2 | +| WI-53 | P2 tests batch | bUnit tests for WI-47 through WI-52 features. One test file per feature. | Rogue | WI-47 to WI-52 | M | P2 | +| WI-54 | P2 sample updates | Update sample pages for ListControl format string, Menu orientation, Label for-attribute. | Jubilee | WI-47 to WI-52 | S | P2 | + +--- + +## Execution Order + +### Phase 1: Base Class Sweep (P0, parallel tracks) + +``` +Track A: WI-01 (AccessKey) → WI-02 (tests) + WI-03 (samples) +Track B: WI-04 (ToolTip) → WI-05 (tests) + WI-06 (samples) +Track C: WI-07 (DataBound style) → WI-08 (tests) + WI-09 (samples) +Track D: WI-10 (Validator Display) → WI-11 (tests) + WI-12 (samples) +Track E: WI-13 (SetFocusOnError) → WI-14 (tests) +Track F: WI-15 (Image base) → WI-16 (tests) +Track G: WI-17 (Label base) → WI-18 (tests) +``` + +Tracks A–G can run in parallel. WI-07 must complete before any P1 data control work. + +### Phase 2: GridView Overhaul (P1, sequential within GridView) + +``` +WI-19 (paging) → WI-20 + WI-21 → WI-22 (sorting) → WI-23 + WI-24 → WI-25 (editing) → WI-26 + WI-27 +WI-42 (GridView docs) after WI-19 + WI-22 + WI-25 +WI-45 (integration tests) after WI-21 + WI-24 + WI-27 +``` + +### Phase 3: Other P1 Controls (parallel) + +``` +Track H: WI-28 (Calendar styles) → WI-29 + WI-30 → WI-43 (docs) +Track I: WI-31 (Calendar enums) → WI-32 +Track J: WI-33 (FormView) → WI-34 + WI-35 → WI-44 (docs) +Track K: WI-36 (HyperLink rename) → WI-37 +Track L: WI-38 (ValidationSummary) → WI-39 + WI-40 +Track M: WI-41 (PasswordRecovery re-audit) — no dependencies +WI-46 (integration tests) after H + J + L +``` + +### Phase 4: P2 Nice-to-Have (if time permits) + +``` +WI-47 through WI-54 — parallel implementation, batch testing, batch samples +``` + +--- + +## Summary Stats + +| Priority | Work Items | Est. Gaps Closed | Agents Involved | +|----------|-----------|-----------------|-----------------| +| P0 | 18 (WI-01 to WI-18) | ~180 | Cyclops, Rogue, Jubilee | +| P1 | 28 (WI-19 to WI-46) | ~120 | All agents | +| P2 | 8 (WI-47 to WI-54) | ~45 | Cyclops, Rogue, Jubilee | +| **Total** | **54** | **~345** | **6 agents** | + +### Expected Health After Milestone 6 + +| Metric | Before (M5) | After (M6 P0+P1) | +|--------|-------------|-------------------| +| Overall Health | 66.3% | ~85% (est.) | +| Matching features | 1,272 | ~1,572 | +| GridView Health | 20.7% | ~55% | +| Data Controls avg | 53.2% | ~70% | +| Validation Controls avg | 76.5% | ~82% | +| 100% controls | 6 | 6 (no new 100% — gains are breadth) | + +--- + +## Key Decisions + +1. **P0 base class changes are the highest-ROI work** — 7 changes close ~180 gaps. This is the most efficient use of engineering time. +2. **GridView is the #1 P1 priority** — most-used Web Forms data control at 20.7% health. Paging → sorting → editing is the correct sequence. +3. **PasswordRecovery audit is stale, not the component** — the component ships; only the audit doc needs updating (WI-41). +4. **Login controls outer styles moved to P2** — these controls use CascadingParameter-based sub-styles by convention. Adding WebControl-level styles to the outer `
    ` wrapper is useful but lower priority than GridView/Calendar/FormView improvements. +5. **Skip Substitution and Xml** — per existing team decision, both remain deferred. +6. **HyperLink rename is small but blocking** — `NavigationUrl` vs `NavigateUrl` is a migration-breaking name mismatch that affects every HyperLink in every migrated app. +7. **Calendar style conversion is technically breaking** — old `DayStyleCss` string params become `[Obsolete]` with `` sub-component replacements. Backward compat via obsolete forwarding. diff --git a/planning-docs/PasswordRecovery.md b/planning-docs/PasswordRecovery.md index f19ab978a..fbd0e9f32 100644 --- a/planning-docs/PasswordRecovery.md +++ b/planning-docs/PasswordRecovery.md @@ -2,123 +2,119 @@ **ASP.NET Docs:** https://learn.microsoft.com/en-us/dotnet/api/system.web.ui.webcontrols.passwordrecovery?view=netframework-4.8 **Blazor Component:** `BlazorWebFormsComponents.LoginControls.PasswordRecovery` -**Implementation Status:** 🔴 Not Found in Source - -> **Note:** Despite history.md referencing PasswordRecovery tests in Sprint 3 (29 bUnit tests), no `PasswordRecovery.razor` or `PasswordRecovery.razor.cs` file exists in `src/BlazorWebFormsComponents/LoginControls/`. The component may exist on a different branch or may have been removed. This audit documents the expected Web Forms API for future implementation reference. +**Implementation Status:** ✅ Implemented ## Properties -### PasswordRecovery-Specific Properties (Web Forms API) +### PasswordRecovery-Specific Properties | Property | Web Forms Type | Blazor Status | Notes | |----------|---------------|---------------|-------| -| AnswerLabelText | `string` | 🔴 Missing | Default: "Answer:" | -| AnswerRequiredErrorMessage | `string` | 🔴 Missing | | -| BorderPadding | `int` | 🔴 Missing | Default: 1 | -| GeneralFailureText | `string` | 🔴 Missing | Default: "Your attempt to retrieve your password was not successful." | -| HelpPageIconUrl | `string` | 🔴 Missing | | -| HelpPageText | `string` | 🔴 Missing | | -| HelpPageUrl | `string` | 🔴 Missing | | -| MailDefinition | `MailDefinition` | 🔴 Missing | Email config for sending recovered password | -| MembershipProvider | `string` | 🔴 Missing | | -| QuestionFailureText | `string` | 🔴 Missing | | -| QuestionInstructionText | `string` | 🔴 Missing | | -| QuestionLabelText | `string` | 🔴 Missing | | -| QuestionTitleText | `string` | 🔴 Missing | | -| RenderOuterTable | `bool` | 🔴 Missing | | -| SuccessPageUrl | `string` | 🔴 Missing | | -| SuccessText | `string` | 🔴 Missing | | -| SubmitButtonImageUrl | `string` | 🔴 Missing | | -| SubmitButtonText | `string` | 🔴 Missing | Default: "Submit" | -| SubmitButtonType | `ButtonType` | 🔴 Missing | | -| UserName | `string` | 🔴 Missing | | -| UserNameFailureText | `string` | 🔴 Missing | | -| UserNameInstructionText | `string` | 🔴 Missing | | -| UserNameLabelText | `string` | 🔴 Missing | Default: "User Name:" | -| UserNameRequiredErrorMessage | `string` | 🔴 Missing | | -| UserNameTitleText | `string` | 🔴 Missing | Default: "Forgot Your Password?" | - -### Template Properties (Web Forms API) +| AnswerLabelText | `string` | 🔴 Missing | Web Forms default: "Answer:" — not implemented; `QuestionLabelText` serves a similar role | +| AnswerRequiredErrorMessage | `string` | ✅ Match | Default: "Answer is required." | +| BorderPadding | `int` | ✅ Match | Default: 1 | +| GeneralFailureText | `string` | ✅ Match | Default: "Your attempt to retrieve your password was not successful. Please try again." | +| HelpPageIconUrl | `string` | ✅ Match | | +| HelpPageText | `string` | ✅ Match | | +| HelpPageUrl | `string` | ✅ Match | | +| MailDefinition | `MailDefinition` | N/A | Email sending is a server concern; string placeholder parameter exists | +| MembershipProvider | `string` | N/A | Marked `[Obsolete]` — server-side membership provider | +| QuestionFailureText | `string` | ✅ Match | Default: "Your answer could not be verified. Please try again." | +| QuestionInstructionText | `string` | ✅ Match | Default: "Answer the following question to receive your password." | +| QuestionLabelText | `string` | ✅ Match | Default: "Answer:" | +| QuestionTitleText | `string` | ✅ Match | Default: "Identity Confirmation" | +| RenderOuterTable | `bool` | ⚠️ Needs Work | Parameter exists (default: true) but not wired to rendering logic | +| SuccessPageUrl | `string` | ✅ Match | Navigates via NavigationManager | +| SuccessText | `string` | ✅ Match | Default: "Your password has been sent to you." | +| SubmitButtonImageUrl | `string` | ⚠️ Needs Work | Parameter exists but not used in default rendering | +| SubmitButtonText | `string` | ✅ Match | Default: "Submit" | +| SubmitButtonType | `ButtonType` | ⚠️ Needs Work | Parameter exists but default rendering always uses `` | +| UserName | `string` | ✅ Match | Public property backed by internal Model | +| UserNameFailureText | `string` | ✅ Match | Default: "Your attempt to retrieve your password was not successful. Please try again." | +| UserNameInstructionText | `string` | ✅ Match | Default: "Enter your User Name to receive your password." | +| UserNameLabelText | `string` | ✅ Match | Default: "User Name:" | +| UserNameRequiredErrorMessage | `string` | ✅ Match | Default: "User Name is required." | +| UserNameTitleText | `string` | ✅ Match | Default: "Forgot Your Password?" | + +### Template Properties | Property | Web Forms Type | Blazor Status | Notes | |----------|---------------|---------------|-------| -| QuestionTemplate | `ITemplate` | 🔴 Missing | Step 2: Security question template | -| SuccessTemplate | `ITemplate` | 🔴 Missing | Step 3: Success message template | -| UserNameTemplate | `ITemplate` | 🔴 Missing | Step 1: Username entry template | +| QuestionTemplate | `ITemplate` | ✅ Match | `RenderFragment` — Step 2: Security question template | +| SuccessTemplate | `ITemplate` | ✅ Match | `RenderFragment` — Step 3: Success message template | +| UserNameTemplate | `ITemplate` | ✅ Match | `RenderFragment` — Step 1: Username entry template | -### Style Properties (Web Forms API) +### Style Properties (via CascadingParameters) | Property | Web Forms Type | Blazor Status | Notes | |----------|---------------|---------------|-------| -| FailureTextStyle | `TableItemStyle` | 🔴 Missing | | -| HyperLinkStyle | `Style` | 🔴 Missing | | -| InstructionTextStyle | `TableItemStyle` | 🔴 Missing | | -| LabelStyle | `TableItemStyle` | 🔴 Missing | | -| SubmitButtonStyle | `Style` | 🔴 Missing | | -| SuccessTextStyle | `TableItemStyle` | 🔴 Missing | | -| TextBoxStyle | `Style` | 🔴 Missing | | -| TitleTextStyle | `TableItemStyle` | 🔴 Missing | | -| ValidatorTextStyle | `Style` | 🔴 Missing | | +| FailureTextStyle | `TableItemStyle` | ✅ Match | Via CascadingParameter | +| HyperLinkStyle | `Style` | ✅ Match | Via CascadingParameter (as TableItemStyle) | +| InstructionTextStyle | `TableItemStyle` | ✅ Match | Via CascadingParameter | +| LabelStyle | `TableItemStyle` | ✅ Match | Via CascadingParameter | +| SubmitButtonStyle | `Style` | ✅ Match | Via CascadingParameter (mapped as "LoginButtonStyle") | +| SuccessTextStyle | `TableItemStyle` | ✅ Match | Via CascadingParameter | +| TextBoxStyle | `Style` | ✅ Match | Via CascadingParameter | +| TitleTextStyle | `TableItemStyle` | ✅ Match | Via CascadingParameter | +| ValidatorTextStyle | `Style` | ✅ Match | Via CascadingParameter | ### Inherited from WebControl | Property | Web Forms Type | Blazor Status | Notes | |----------|---------------|---------------|-------| -| AccessKey | `string` | 🔴 Missing | | -| BackColor | `Color` | 🔴 Missing | | -| BorderColor | `Color` | 🔴 Missing | | -| BorderStyle | `BorderStyle` | 🔴 Missing | | -| BorderWidth | `Unit` | 🔴 Missing | | -| CssClass | `string` | 🔴 Missing | | -| Enabled | `bool` | 🔴 Missing | | -| Font | `FontInfo` | 🔴 Missing | | -| ForeColor | `Color` | 🔴 Missing | | -| Height | `Unit` | 🔴 Missing | | -| Style | `CssStyleCollection` | 🔴 Missing | | -| TabIndex | `short` | 🔴 Missing | | -| ToolTip | `string` | 🔴 Missing | | -| Width | `Unit` | 🔴 Missing | | +| AccessKey | `string` | ✅ Match | Via `BaseWebFormsComponent` | +| BackColor | `Color` | 🔴 Missing | Inherits `BaseWebFormsComponent`, not `BaseStyledComponent` | +| BorderColor | `Color` | 🔴 Missing | Same | +| BorderStyle | `BorderStyle` | 🔴 Missing | Same | +| BorderWidth | `Unit` | 🔴 Missing | Same | +| CssClass | `string` | 🔴 Missing | Same | +| Enabled | `bool` | ✅ Match | Via `BaseWebFormsComponent` | +| Font | `FontInfo` | 🔴 Missing | Same | +| ForeColor | `Color` | 🔴 Missing | Same | +| Height | `Unit` | 🔴 Missing | Same | +| Style | `CssStyleCollection` | 🔴 Missing | Same | +| TabIndex | `short` | ✅ Match | Via `BaseWebFormsComponent` | +| ToolTip | `string` | 🔴 Missing | Same | +| Width | `Unit` | 🔴 Missing | Same | ### Inherited from Control | Property | Web Forms Type | Blazor Status | Notes | |----------|---------------|---------------|-------| -| ID | `string` | 🔴 Missing | | -| Visible | `bool` | 🔴 Missing | | +| ID | `string` | ✅ Match | Via `BaseWebFormsComponent.ID` | +| Visible | `bool` | ✅ Match | Via `BaseWebFormsComponent.Visible` | ## Events | Event | Web Forms Signature | Blazor Status | Notes | |-------|-------------------|---------------|-------| -| AnswerLookupError | `EventHandler` | 🔴 Missing | Incorrect security answer | -| SendingMail | `MailMessageEventHandler` | 🔴 Missing | Email sending | -| SendMailError | `SendMailErrorEventHandler` | 🔴 Missing | Email error | -| UserLookupError | `EventHandler` | 🔴 Missing | User not found | -| VerifyingAnswer | `LoginCancelEventHandler` | 🔴 Missing | Before verifying answer | -| VerifyingUser | `LoginCancelEventHandler` | 🔴 Missing | Before verifying user | +| AnswerLookupError | `EventHandler` | ✅ Match | `EventCallback OnAnswerLookupError` | +| SendingMail | `MailMessageEventHandler` | ✅ Match | `EventCallback OnSendingMail` | +| SendMailError | `SendMailErrorEventHandler` | ✅ Match | `EventCallback OnSendMailError` | +| UserLookupError | `EventHandler` | ✅ Match | `EventCallback OnUserLookupError` | +| VerifyingAnswer | `LoginCancelEventHandler` | ✅ Match | `EventCallback OnVerifyingAnswer` | +| VerifyingUser | `LoginCancelEventHandler` | ✅ Match | `EventCallback OnVerifyingUser` | ## Methods | Method | Web Forms Signature | Blazor Status | Notes | |--------|-------------------|---------------|-------| -| DataBind() | `void DataBind()` | 🔴 Missing | | -| Focus() | `void Focus()` | 🔴 Missing | | -| FindControl() | `Control FindControl(string)` | 🔴 Missing | | +| DataBind() | `void DataBind()` | N/A | No-op stub | +| Focus() | `void Focus()` | 🔴 Missing | Server-initiated focus requires JS interop | +| FindControl() | `Control FindControl(string)` | ✅ Match | Via `BaseWebFormsComponent` | ## HTML Output Comparison Web Forms `PasswordRecovery` renders a 3-step wizard: -1. **Step 1 (UserName):** Username input with submit button -2. **Step 2 (Question):** Security question with answer input +1. **Step 1 (UserName):** Username input with submit button inside nested `
    ` layout +2. **Step 2 (Question):** Security question display with answer input 3. **Step 3 (Success):** Success message -Each step renders in a `
    ` layout. The Blazor component does not exist in the source tree, so no output comparison is possible. +The Blazor component produces matching table structures with `cellspacing="0"`, `cellpadding`, `border-collapse:collapse`. Field IDs follow the Web Forms pattern (`{ID}_UserName`, `{ID}_Answer`, `{ID}_SubmitButton`, `{ID}_HelpLink`). Steps 1 and 2 wrap content in an `EditForm` for Blazor validation. Help links and icons are conditionally rendered matching the Web Forms pattern. Style properties are applied via CascadingParameters and the `HandleUnknownAttributes` pattern used by other login controls. ## Summary -- **Matching:** 0 properties, 0 events -- **Needs Work:** 0 -- **Missing:** ALL — 52 properties, 6 events, 3 methods (component not found in source) -- **N/A (server-only):** N/A - -> ⚠️ **Action Required:** Locate the PasswordRecovery component. History.md references Sprint 3 delivery with 29 tests. Check `dev` branch or other feature branches. +- **Matching:** 36 properties, 6 events +- **Needs Work:** 3 properties (RenderOuterTable, SubmitButtonImageUrl, SubmitButtonType) +- **Missing:** 12 properties (AnswerLabelText + 11 WebControl style properties) +- **N/A (server-only):** 2 properties (MailDefinition, MembershipProvider) diff --git a/planning-docs/SUMMARY.md b/planning-docs/SUMMARY.md index c4c48fb77..ebb3a83ee 100644 --- a/planning-docs/SUMMARY.md +++ b/planning-docs/SUMMARY.md @@ -11,32 +11,31 @@ | Metric | Properties | Events | **Total** | |--------|-----------|--------|-----------| -| ✅ **Matching** | 931 | 341 | **1,272** | -| ⚠️ **Needs Work** | 50 | 1 | **51** | -| 🔴 **Missing** | 505 | 92 | **597** | -| ➖ **N/A (server-only)** | — | — | **264** | -| **Total Applicable** | **1,486** | **434** | **1,920** | +| ✅ **Matching** | 967 | 347 | **1,314** | +| ⚠️ **Needs Work** | 53 | 1 | **54** | +| 🔴 **Missing** | 465 | 86 | **551** | +| ➖ **N/A (server-only)** | — | — | **266** | +| **Total Applicable** | **1,485** | **434** | **1,919** | -### Overall Health: **66.3%** +### Overall Health: **68.5%** -Of 1,920 applicable features (properties + events), 1,272 fully match Web Forms behavior. An additional 51 exist but need API corrections. 597 features are missing entirely. +Of 1,919 applicable features (properties + events), 1,314 fully match Web Forms behavior. An additional 54 exist but need API corrections. 551 features are missing entirely. -**6 controls are 100% feature-complete.** 3 controls are not started (Substitution, Xml, PasswordRecovery on current branch). GridView is the weakest implemented control at 20.7% coverage. +**6 controls are 100% feature-complete.** 2 controls are not started (Substitution, Xml). GridView is the weakest implemented control at 20.7% coverage. --- ## 2. Critical Findings -### 🚨 UNMERGED COMPONENTS — DetailsView & PasswordRecovery +### 🚨 UNMERGED COMPONENT — DetailsView -**Branch `sprint3/detailsview-passwordrecovery` was NEVER merged to `dev`.** +**PasswordRecovery has been merged to `dev`** and the re-audit shows **73.7% health** (42 matching features out of 57 applicable). DetailsView remains on the unmerged `sprint3/detailsview-passwordrecovery` branch. -- `status.md` incorrectly lists both as ✅ Complete +- `status.md` incorrectly lists DetailsView as ✅ Complete - **DetailsView** exists on the branch with 27 matching properties, 16 matching events, and strong CRUD support — but is inaccessible from the current working branch -- **PasswordRecovery** exists on the branch (29 bUnit tests referenced in history) but the component files are NOT found on the current branch — the audit shows **0 matching features** from the current branch perspective -- This means the actual shipped component count is **48/53 (91%)**, not 50/53 (94%) +- This means the actual shipped component count is **49/53 (92%)**, not 50/53 (94%) -**Action Required:** Merge `sprint3/detailsview-passwordrecovery` into `dev` immediately. +**Action Required:** Merge DetailsView from `sprint3/detailsview-passwordrecovery` into `dev`. ### 🔴 Base Class Inheritance Gap — DataBoundComponent\ @@ -174,10 +173,10 @@ Calendar has 9 style sub-properties (DayStyle, TitleStyle, etc.) implemented as | ChangePassword | 49 | 2 | 16 | 73.1% | | Login | 40 | 0 | 16 | 71.4% | | CreateUserWizard | 64 | 3 | 27 | 68.1% | -| PasswordRecovery ⚠️ | 0 | 0 | 58 | 0% | -| **TOTALS** | **176/38** | **5/0** | **110/13** | **62.6%** | +| PasswordRecovery | 42 | 3 | 12 | 73.7% | +| **TOTALS** | **212/44** | **8/0** | **70/7** | **75.1%** | -⚠️ PasswordRecovery exists only on unmerged branch `sprint3/detailsview-passwordrecovery` +⚠️ DetailsView exists only on unmerged branch `sprint3/detailsview-passwordrecovery` ### Not Started / Deferred (2 controls) @@ -228,7 +227,7 @@ These 6 controls match 100% of applicable Web Forms features: | # | Fix | Impact | Notes | |---|-----|--------|-------| -| 8 | **Merge `sprint3/detailsview-passwordrecovery` to dev** | 2 controls restored | Unblocks DetailsView + PasswordRecovery | +| 8 | **Merge DetailsView from `sprint3/detailsview-passwordrecovery` to dev** | 1 control restored | Unblocks DetailsView (PasswordRecovery already merged) | | 9 | **GridView: Add paging** (AllowPaging, PageSize, PageIndex, PagerSettings) | Most-used data control feature | GridView is ~80% of data grid usage in Web Forms apps | | 10 | **GridView: Add sorting** (AllowSorting, SortDirection, SortExpression, Sorted/Sorting events) | Critical for data display | | | 11 | **GridView: Add row editing events** (RowEditing, RowUpdating, RowDeleting, RowCancelingEdit) | Inline editing support | | @@ -272,22 +271,20 @@ These 6 controls match 100% of applicable Web Forms features: ## 6. Unmerged Branch Alert -> ### ⚠️ CRITICAL: Branch `sprint3/detailsview-passwordrecovery` Must Be Merged +> ### ⚠️ CRITICAL: DetailsView Must Be Merged from `sprint3/detailsview-passwordrecovery` > -> **Two fully-reviewed, gate-approved components exist ONLY on this branch:** +> **PasswordRecovery has been merged** and re-audited at **73.7% health**. One component remains on the unmerged branch: > > | Component | Properties | Events | Tests | Gate Status | > |-----------|-----------|--------|-------|-------------| > | DetailsView | 27 match, 23 missing | 16 match, 2 missing | Referenced in Sprint 3 | ✅ APPROVED by Forge | -> | PasswordRecovery | 0 on current branch | 0 on current branch | 29 bUnit tests referenced | ✅ APPROVED by Forge | > > **Impact of NOT merging:** -> - `status.md` claims 50/53 (94%) but actual shipped count is 48/53 (91%) -> - PasswordRecovery audit shows 0% because the files don't exist on the current branch +> - `status.md` claims 50/53 (94%) but actual shipped count is 49/53 (92%) > - DetailsView features (strong CRUD events, auto-generated rows, edit mode) are inaccessible -> - Any work on `milestone4/chart-component` branch diverges further from these components +> - Any work on `milestone4/chart-component` branch diverges further from this component > -> **Recommended action:** Merge to `dev` immediately, then rebase current milestone branch. +> **Recommended action:** Merge DetailsView to `dev`, then rebase current milestone branch. --- @@ -329,22 +326,22 @@ These 6 controls match 100% of applicable Web Forms features: | 32 | RequiredFieldValidator | Validation | 75.9% | Adequate | | 33 | CompareValidator | Validation | 75.0% | Adequate | | 34 | Calendar | Data | 74.5% | Adequate | -| 35 | ChangePassword | Login | 73.1% | Adequate | -| 36 | DataList | Data | 73.0% | Adequate | -| 37 | BulletedList | Data | 71.4% | Adequate | -| 38 | Login | Login | 71.4% | Adequate | -| 39 | ValidationSummary | Validation | 71.4% | Adequate | -| 40 | CreateUserWizard | Login | 68.1% | Needs Work | -| 41 | DetailsView | Data | 63.2% | ⚠️ Unmerged branch | -| 42 | TreeView | Navigation | 57.1% | Needs Work | -| 43 | Image | Editor | 56.0% | Needs Work | -| 44 | Label | Editor | 54.2% | Needs Work | -| 45 | DataGrid | Data | 44.6% | Needs Work | -| 46 | Menu | Navigation | 37.7% | Significant gaps | -| 47 | FormView | Data | 34.9% | Significant gaps | -| 48 | ListView | Data | 34.3% | Significant gaps | -| 49 | Chart | Data | 32.3% | Architectural deviation | -| 50 | GridView | Data | 20.7% | 🔴 Critical gaps | -| 51 | PasswordRecovery | Login | 0% | ⚠️ Unmerged branch | +| 35 | PasswordRecovery | Login | 73.7% | Adequate | +| 36 | ChangePassword | Login | 73.1% | Adequate | +| 37 | DataList | Data | 73.0% | Adequate | +| 38 | BulletedList | Data | 71.4% | Adequate | +| 39 | Login | Login | 71.4% | Adequate | +| 40 | ValidationSummary | Validation | 71.4% | Adequate | +| 41 | CreateUserWizard | Login | 68.1% | Needs Work | +| 42 | DetailsView | Data | 63.2% | ⚠️ Unmerged branch | +| 43 | TreeView | Navigation | 57.1% | Needs Work | +| 44 | Image | Editor | 56.0% | Needs Work | +| 45 | Label | Editor | 54.2% | Needs Work | +| 46 | DataGrid | Data | 44.6% | Needs Work | +| 47 | Menu | Navigation | 37.7% | Significant gaps | +| 48 | FormView | Data | 34.9% | Significant gaps | +| 49 | ListView | Data | 34.3% | Significant gaps | +| 50 | Chart | Data | 32.3% | Architectural deviation | +| 51 | GridView | Data | 20.7% | 🔴 Critical gaps | | 52 | Substitution | Deferred | 0% | Intentionally deferred | | 53 | Xml | Deferred | 0% | Intentionally deferred | diff --git a/samples/AfterBlazorServerSide.Tests/ControlSampleTests.cs b/samples/AfterBlazorServerSide.Tests/ControlSampleTests.cs index 873d5dbd6..edfca67e0 100644 --- a/samples/AfterBlazorServerSide.Tests/ControlSampleTests.cs +++ b/samples/AfterBlazorServerSide.Tests/ControlSampleTests.cs @@ -67,6 +67,9 @@ public async Task EditorControl_Loads_WithoutErrors(string path) [InlineData("/ControlSamples/GridView/BindAttribute")] [InlineData("/ControlSamples/GridView/TemplateFields")] [InlineData("/ControlSamples/GridView/RowSelection")] + [InlineData("/ControlSamples/GridView/Paging")] + [InlineData("/ControlSamples/GridView/Sorting")] + [InlineData("/ControlSamples/GridView/InlineEditing")] [InlineData("/ControlSamples/FormView/Simple")] [InlineData("/ControlSamples/FormView/Edit")] [InlineData("/ControlSamples/DetailsView")] diff --git a/samples/AfterBlazorServerSide.Tests/InteractiveComponentTests.cs b/samples/AfterBlazorServerSide.Tests/InteractiveComponentTests.cs index edbd911db..69cecd793 100644 --- a/samples/AfterBlazorServerSide.Tests/InteractiveComponentTests.cs +++ b/samples/AfterBlazorServerSide.Tests/InteractiveComponentTests.cs @@ -1992,4 +1992,156 @@ public async Task Chart_AllTypes_CanvasHasRenderingContext(string path) await page.CloseAsync(); } } + + [Fact] + public async Task GridView_Paging_ClickNextPage() + { + // Arrange + var page = await _fixture.NewPageAsync(); + var consoleErrors = new List(); + + page.Console += (_, msg) => + { + if (msg.Type == "error") + { + if (!System.Text.RegularExpressions.Regex.IsMatch(msg.Text, @"^\[\d{4}-\d{2}-\d{2}T") + && !msg.Text.StartsWith("Failed to load resource")) + consoleErrors.Add(msg.Text); + } + }; + + try + { + await page.GotoAsync($"{_fixture.BaseUrl}/ControlSamples/GridView/Paging", new PageGotoOptions + { + WaitUntil = WaitUntilState.NetworkIdle, + Timeout = 30000 + }); + + // Verify initial state - table has data rows (page size 10) + var rows = page.Locator("table tbody tr"); + await Expect(rows).ToHaveCountAsync(10); + + // Click page 2 link in the pager footer + var page2Link = page.Locator("table tfoot a").Filter(new() { HasTextString = "2" }); + await page2Link.ClickAsync(); + await page.WaitForTimeoutAsync(500); + + // Verify page indicator updated + var pageInfo = page.Locator("p", new() { HasTextRegex = new System.Text.RegularExpressions.Regex(@"Current Page:\s*2") }); + await Expect(pageInfo).ToBeVisibleAsync(); + + Assert.Empty(consoleErrors); + } + finally + { + await page.CloseAsync(); + } + } + + [Fact] + public async Task GridView_Sorting_ClickColumnHeader() + { + // Arrange + var page = await _fixture.NewPageAsync(); + var consoleErrors = new List(); + + page.Console += (_, msg) => + { + if (msg.Type == "error") + { + if (!System.Text.RegularExpressions.Regex.IsMatch(msg.Text, @"^\[\d{4}-\d{2}-\d{2}T") + && !msg.Text.StartsWith("Failed to load resource")) + consoleErrors.Add(msg.Text); + } + }; + + try + { + await page.GotoAsync($"{_fixture.BaseUrl}/ControlSamples/GridView/Sorting", new PageGotoOptions + { + WaitUntil = WaitUntilState.NetworkIdle, + Timeout = 30000 + }); + + // Verify initial sort state shows (none) + var sortInfo = page.Locator("p", new() { HasTextRegex = new System.Text.RegularExpressions.Regex(@"Sort Column:") }); + await Expect(sortInfo).ToBeVisibleAsync(); + var initialText = await sortInfo.TextContentAsync(); + Assert.Contains("(none)", initialText); + + // Click "Name" column header to sort + var nameHeader = page.Locator("thead th a").Filter(new() { HasTextString = "Name" }); + await nameHeader.ClickAsync(); + await page.WaitForTimeoutAsync(500); + + // Verify sort state updated to show Name column + var updatedText = await sortInfo.TextContentAsync(); + Assert.Contains("Name", updatedText); + Assert.Contains("Ascending", updatedText); + + Assert.Empty(consoleErrors); + } + finally + { + await page.CloseAsync(); + } + } + + [Fact] + public async Task GridView_InlineEditing_ClickEdit() + { + // Arrange + var page = await _fixture.NewPageAsync(); + var consoleErrors = new List(); + + page.Console += (_, msg) => + { + if (msg.Type == "error") + { + if (!System.Text.RegularExpressions.Regex.IsMatch(msg.Text, @"^\[\d{4}-\d{2}-\d{2}T") + && !msg.Text.StartsWith("Failed to load resource")) + consoleErrors.Add(msg.Text); + } + }; + + try + { + await page.GotoAsync($"{_fixture.BaseUrl}/ControlSamples/GridView/InlineEditing", new PageGotoOptions + { + WaitUntil = WaitUntilState.NetworkIdle, + Timeout = 30000 + }); + + // Verify initial edit index is -1 + var editInfo = page.Locator("p", new() { HasTextRegex = new System.Text.RegularExpressions.Regex(@"Edit Index:") }); + var initialText = await editInfo.TextContentAsync(); + Assert.Contains("-1", initialText); + + // Click the Edit link on the first row + var editLink = page.Locator("tbody tr:first-child a").Filter(new() { HasTextString = "Edit" }); + await editLink.ClickAsync(); + await page.WaitForTimeoutAsync(500); + + // Verify edit mode activated - input fields should appear in the row + var inputs = page.Locator("tbody tr:first-child input[type='text']"); + var inputCount = await inputs.CountAsync(); + Assert.True(inputCount > 0, "Input fields should appear when editing a row"); + + // Verify Update and Cancel links appear + var updateLink = page.Locator("tbody tr:first-child a").Filter(new() { HasTextString = "Update" }); + await Expect(updateLink).ToBeVisibleAsync(); + + var cancelLink = page.Locator("tbody tr:first-child a").Filter(new() { HasTextString = "Cancel" }); + await Expect(cancelLink).ToBeVisibleAsync(); + + Assert.Empty(consoleErrors); + } + finally + { + await page.CloseAsync(); + } + } + + private ILocatorAssertions Expect(ILocator locator) => Assertions.Expect(locator); } diff --git a/samples/AfterBlazorServerSide/ComponentCatalog.cs b/samples/AfterBlazorServerSide/ComponentCatalog.cs index 40c9a234b..71fd7029c 100644 --- a/samples/AfterBlazorServerSide/ComponentCatalog.cs +++ b/samples/AfterBlazorServerSide/ComponentCatalog.cs @@ -33,6 +33,8 @@ public static class ComponentCatalog new("Image", "Editor", "/ControlSamples/Image", "Displays an image with alt text support", Keywords: new[] { "img", "picture" }), new("ImageMap", "Editor", "/ControlSamples/ImageMap", "Image with clickable hotspot regions"), + new("Label", "Editor", "/ControlSamples/Label", "Renders text as span or accessible label element", + Keywords: new[] { "text", "label", "accessibility" }), new("LinkButton", "Editor", "/ControlSamples/LinkButton", "Button rendered as a hyperlink", new[] { "JavaScript" }, new[] { "link", "postback" }), diff --git a/samples/AfterBlazorServerSide/Components/Pages/ComponentList.razor b/samples/AfterBlazorServerSide/Components/Pages/ComponentList.razor index e7047abd8..92004e70f 100644 --- a/samples/AfterBlazorServerSide/Components/Pages/ComponentList.razor +++ b/samples/AfterBlazorServerSide/Components/Pages/ComponentList.razor @@ -12,6 +12,7 @@
  • HiddenField
  • HyperLink
  • Image
  • +
  • Label
  • LinkButton
  • Literal
  • Localize
  • diff --git a/samples/AfterBlazorServerSide/Components/Pages/ControlSamples/Button/Index.razor b/samples/AfterBlazorServerSide/Components/Pages/ControlSamples/Button/Index.razor index d3985988a..9c738384d 100644 --- a/samples/AfterBlazorServerSide/Components/Pages/ControlSamples/Button/Index.razor +++ b/samples/AfterBlazorServerSide/Components/Pages/ControlSamples/Button/Index.razor @@ -4,7 +4,10 @@ - @@ -52,6 +55,9 @@ <EditForm Model="@@exampleModel" OnValidSubmit="@@HandleValidSubmit">
      <InputText id="name" @@ref="Name.Current" @@bind-Value="exampleModel.Name" />
      <RequiredFieldValidator Type="string" ControlToValidate="@@Name" ForeColor="Red" Text="Name is required." />
    +
    +  <InputNumber id="number" @@ref="Number.Current" @@bind-Value="exampleModel.Number" />
    +  <RequiredFieldValidator Type="int?" ControlToValidate="@@Number" Display="ValidatorDisplay.Dynamic" Text="Number is required." />
      <button type="submit">Submit</button>
    </EditForm>
    diff --git a/samples/AfterBlazorServerSide/Components/Pages/ControlSamples/Validations/ValidationSummarySample.razor b/samples/AfterBlazorServerSide/Components/Pages/ControlSamples/Validations/ValidationSummarySample.razor index 70401abbc..d2d1cacc3 100644 --- a/samples/AfterBlazorServerSide/Components/Pages/ControlSamples/Validations/ValidationSummarySample.razor +++ b/samples/AfterBlazorServerSide/Components/Pages/ControlSamples/Validations/ValidationSummarySample.razor @@ -27,8 +27,8 @@ -
    Bold Bullet List
    - +
    Bold Bullet List with HeaderText
    +
    Lime List
    @@ -63,5 +63,5 @@ <RequiredFieldValidator Type="string" ControlToValidate="@@Name" Text="Name is required." ErrorMessage="Name is required but in summary!" />
    <RegularExpressionValidator ValidationExpression="^[0-9]{5}$" ControlToValidate="@@Name" Text="Not a 5 digit number." />
    -<AspNetValidationSummary Font_Bold="true" DisplayMode="BulletList" /> +<AspNetValidationSummary Font_Bold="true" DisplayMode="BulletList" HeaderText="Please fix these errors:" />
    diff --git a/samples/AfterBlazorServerSide/Pages/ControlSamples/IDRendering.razor b/samples/AfterBlazorServerSide/Pages/ControlSamples/IDRendering.razor index fc2db0a4e..666002a52 100644 --- a/samples/AfterBlazorServerSide/Pages/ControlSamples/IDRendering.razor +++ b/samples/AfterBlazorServerSide/Pages/ControlSamples/IDRendering.razor @@ -80,7 +80,7 @@
    - +
    diff --git a/samples/AfterBlazorServerSide/Pages/ControlSamples/Menu/DatabindingSitemap.razor b/samples/AfterBlazorServerSide/Pages/ControlSamples/Menu/DatabindingSitemap.razor index 494027a3e..c22fe7885 100644 --- a/samples/AfterBlazorServerSide/Pages/ControlSamples/Menu/DatabindingSitemap.razor +++ b/samples/AfterBlazorServerSide/Pages/ControlSamples/Menu/DatabindingSitemap.razor @@ -11,7 +11,6 @@ DisappearAfter="2000" StaticDisplayLevels="2" StaticSubmenuIndent="10" - orientation="Vertical" font-names="Arial" target="_blank" DataSource="MenuSource" diff --git a/samples/AfterBlazorServerSide/Pages/ControlSamples/Menu/DynamicMenuStyleSample.razor b/samples/AfterBlazorServerSide/Pages/ControlSamples/Menu/DynamicMenuStyleSample.razor index 53086ccb6..5981ffaea 100644 --- a/samples/AfterBlazorServerSide/Pages/ControlSamples/Menu/DynamicMenuStyleSample.razor +++ b/samples/AfterBlazorServerSide/Pages/ControlSamples/Menu/DynamicMenuStyleSample.razor @@ -9,7 +9,6 @@ diff --git a/samples/AfterBlazorServerSide/Pages/ControlSamples/Menu/Index.razor b/samples/AfterBlazorServerSide/Pages/ControlSamples/Menu/Index.razor index 037105b1c..0f2652790 100644 --- a/samples/AfterBlazorServerSide/Pages/ControlSamples/Menu/Index.razor +++ b/samples/AfterBlazorServerSide/Pages/ControlSamples/Menu/Index.razor @@ -1,4 +1,5 @@ @page "/ControlSamples/Menu" +@using BlazorWebFormsComponents.Enums

    Menu Component homepage

    @@ -11,7 +12,6 @@ DisappearAfter="2000" StaticDisplayLevels="2" StaticSubmenuIndent="10" - orientation="Vertical" font-names="Arial" target="_blank" runat="server"> @@ -59,6 +59,46 @@
    +

    Horizontal Menu

    + +

    Set Orientation to display menu items horizontally. This is useful for top navigation bars.

    + + + + + + + + + + + + + + + + + + +
    +

    Code:

    <Menu id="NavigationMenu" orientation="Vertical">
    @@ -68,5 +108,17 @@       <MenuItem text="Music" tooltip="Music">...</MenuItem>
        </MenuItem>
      </Items>
    +</Menu>

    +<!-- Horizontal orientation -->
    +<Menu id="HorizontalMenu" Orientation="horizontal">
    +  <Items>
    +    <MenuItem text="Home" />
    +    <MenuItem text="Products">...</MenuItem>
    +    <MenuItem text="About" />
    +  </Items>
    </Menu>
    + +@code { + BlazorWebFormsComponents.Enums.Orientation horizontal = BlazorWebFormsComponents.Enums.Orientation.Horizontal; +} diff --git a/src/BlazorWebFormsComponents.Test/BaseWebFormsComponent/AccessKeyTests.razor b/src/BlazorWebFormsComponents.Test/BaseWebFormsComponent/AccessKeyTests.razor new file mode 100644 index 000000000..f38523055 --- /dev/null +++ b/src/BlazorWebFormsComponents.Test/BaseWebFormsComponent/AccessKeyTests.razor @@ -0,0 +1,48 @@ + +@code { + + [Fact] + public void Button_WithAccessKey_RendersAccessKeyAttribute() + { + // Arrange & Act + var cut = Render(@ } diff --git a/src/BlazorWebFormsComponents/Calendar.razor b/src/BlazorWebFormsComponents/Calendar.razor index c0e256b13..d311cb8ed 100644 --- a/src/BlazorWebFormsComponents/Calendar.razor +++ b/src/BlazorWebFormsComponents/Calendar.razor @@ -1,6 +1,18 @@ @inherits BaseStyledComponent @using BlazorWebFormsComponents.Enums + + @DayStyleContent + @TitleStyleContent + @DayHeaderStyleContent + @TodayDayStyleContent + @SelectedDayStyleContent + @OtherMonthDayStyleContent + @WeekendDayStyleContent + @NextPrevStyleContent + @SelectorStyleContent + + @if (Visible) {
    @if (ShowNextPrevMonth && SelectionMode == CalendarSelectionMode.DayWeekMonth) { - } @if (ShowNextPrevMonth) { - } - @if (ShowNextPrevMonth) { - } @@ -45,11 +57,11 @@ @if (SelectionMode == CalendarSelectionMode.DayWeek || SelectionMode == CalendarSelectionMode.DayWeekMonth) { - + } @foreach (var day in GetDayHeaders()) { - } @@ -60,14 +72,14 @@ @if (SelectionMode == CalendarSelectionMode.DayWeek || SelectionMode == CalendarSelectionMode.DayWeekMonth) { - } @foreach (var date in week) { var dayArgs = CreateDayRenderArgs(date); - @foreach (IColumn column in ColumnList) { - + @if (AllowSorting && !string.IsNullOrEmpty(column.SortExpression)) + { + + } + else + { + + } + } + @if (ShowCommandColumn) + { + } @@ -18,10 +29,11 @@ @if (Items != null && Items.Any()) { var index = 0; - @foreach (ItemType item in Items) + @foreach (ItemType item in PagedItems) { + var rowIndex = index; - + index++; @@ -30,12 +42,37 @@ else { - } + @if (AllowPaging && TotalPages > 1) + { + + + + + + }
    - @SelectMonthText + + @SelectMonthText + @((MarkupString)PrevMonthText) + @GetTitleText() + @((MarkupString)NextMonthText)
    + @GetDayName(day)
    + @((MarkupString)SelectWeekText) + @if (dayArgs.IsSelectable) { diff --git a/src/BlazorWebFormsComponents/Calendar.razor.cs b/src/BlazorWebFormsComponents/Calendar.razor.cs index f36883c05..589e9193c 100644 --- a/src/BlazorWebFormsComponents/Calendar.razor.cs +++ b/src/BlazorWebFormsComponents/Calendar.razor.cs @@ -1,4 +1,5 @@ using BlazorWebFormsComponents.Enums; +using BlazorWebFormsComponents.Interfaces; using Microsoft.AspNetCore.Components; using System; using System.Collections.Generic; @@ -8,7 +9,7 @@ namespace BlazorWebFormsComponents { - public partial class Calendar : BaseStyledComponent + public partial class Calendar : BaseStyledComponent, ICalendarStyleContainer { private DateTime _visibleMonth; private readonly HashSet _selectedDays = new HashSet(); @@ -134,13 +135,13 @@ public DateTime VisibleDate /// Format for displaying day names. /// [Parameter] - public string DayNameFormat { get; set; } = "Short"; + public DayNameFormat DayNameFormat { get; set; } = DayNameFormat.Short; /// /// Format for the title. /// [Parameter] - public string TitleFormat { get; set; } = "MonthYear"; + public TitleFormat TitleFormat { get; set; } = TitleFormat.MonthYear; /// /// Text for next month link. @@ -190,33 +191,66 @@ public DateTime VisibleDate [Parameter] public string ToolTip { get; set; } - // Style properties for different day types + // Legacy CSS string style properties (backward compatible) +#pragma warning disable CS0618 [Parameter] + [Obsolete("Use sub-component instead")] public string TitleStyleCss { get; set; } [Parameter] + [Obsolete("Use sub-component instead")] public string DayHeaderStyleCss { get; set; } [Parameter] + [Obsolete("Use sub-component instead")] public string DayStyleCss { get; set; } [Parameter] + [Obsolete("Use sub-component instead")] public string TodayDayStyleCss { get; set; } [Parameter] + [Obsolete("Use sub-component instead")] public string SelectedDayStyleCss { get; set; } [Parameter] + [Obsolete("Use sub-component instead")] public string OtherMonthDayStyleCss { get; set; } [Parameter] + [Obsolete("Use sub-component instead")] public string WeekendDayStyleCss { get; set; } [Parameter] + [Obsolete("Use sub-component instead")] public string NextPrevStyleCss { get; set; } [Parameter] + [Obsolete("Use sub-component instead")] public string SelectorStyleCss { get; set; } +#pragma warning restore CS0618 + + // TableItemStyle properties (ICalendarStyleContainer) + public TableItemStyle DayStyle { get; internal set; } = new TableItemStyle(); + public TableItemStyle TitleStyle { get; internal set; } = new TableItemStyle(); + public TableItemStyle DayHeaderStyle { get; internal set; } = new TableItemStyle(); + public TableItemStyle TodayDayStyle { get; internal set; } = new TableItemStyle(); + public TableItemStyle SelectedDayStyle { get; internal set; } = new TableItemStyle(); + public TableItemStyle OtherMonthDayStyle { get; internal set; } = new TableItemStyle(); + public TableItemStyle WeekendDayStyle { get; internal set; } = new TableItemStyle(); + public TableItemStyle NextPrevStyle { get; internal set; } = new TableItemStyle(); + public TableItemStyle SelectorStyle { get; internal set; } = new TableItemStyle(); + + // RenderFragment parameters for style sub-components + [Parameter] public RenderFragment DayStyleContent { get; set; } + [Parameter] public RenderFragment TitleStyleContent { get; set; } + [Parameter] public RenderFragment DayHeaderStyleContent { get; set; } + [Parameter] public RenderFragment TodayDayStyleContent { get; set; } + [Parameter] public RenderFragment SelectedDayStyleContent { get; set; } + [Parameter] public RenderFragment OtherMonthDayStyleContent { get; set; } + [Parameter] public RenderFragment WeekendDayStyleContent { get; set; } + [Parameter] public RenderFragment NextPrevStyleContent { get; set; } + [Parameter] public RenderFragment SelectorStyleContent { get; set; } #endregion @@ -249,7 +283,7 @@ private string GetBorder() private string GetTitleText() { - var format = TitleFormat == "Month" ? "MMMM" : "MMMM yyyy"; + var format = TitleFormat == TitleFormat.Month ? "MMMM" : "MMMM yyyy"; return _visibleMonth.ToString(format, CultureInfo.CurrentCulture); } @@ -269,10 +303,10 @@ private string GetDayName(DayOfWeek day) { return DayNameFormat switch { - "Full" => CultureInfo.CurrentCulture.DateTimeFormat.GetDayName(day), - "FirstLetter" => SafeSubstring(CultureInfo.CurrentCulture.DateTimeFormat.GetDayName(day), 0, 1), - "FirstTwoLetters" => SafeSubstring(CultureInfo.CurrentCulture.DateTimeFormat.GetDayName(day), 0, 2), - "Shortest" => CultureInfo.CurrentCulture.DateTimeFormat.GetShortestDayName(day), + Enums.DayNameFormat.Full => CultureInfo.CurrentCulture.DateTimeFormat.GetDayName(day), + Enums.DayNameFormat.FirstLetter => SafeSubstring(CultureInfo.CurrentCulture.DateTimeFormat.GetDayName(day), 0, 1), + Enums.DayNameFormat.FirstTwoLetters => SafeSubstring(CultureInfo.CurrentCulture.DateTimeFormat.GetDayName(day), 0, 2), + Enums.DayNameFormat.Shortest => CultureInfo.CurrentCulture.DateTimeFormat.GetShortestDayName(day), _ => CultureInfo.CurrentCulture.DateTimeFormat.GetAbbreviatedDayName(day) }; } @@ -396,6 +430,7 @@ private async Task HandleMonthClick() await InvokeAsync(StateHasChanged); } + #pragma warning disable CS0612, CS0618 private string GetDayCellCss(DateTime date) { var isToday = date.Date == DateTime.Today; @@ -403,17 +438,72 @@ private string GetDayCellCss(DateTime date) var isOtherMonth = date.Month != _visibleMonth.Month; var isWeekend = date.DayOfWeek == DayOfWeek.Saturday || date.DayOfWeek == DayOfWeek.Sunday; - // Priority order for styling - if (isSelected && !string.IsNullOrEmpty(SelectedDayStyleCss)) - return SelectedDayStyleCss; - if (isToday && !string.IsNullOrEmpty(TodayDayStyleCss)) - return TodayDayStyleCss; - if (isOtherMonth && !string.IsNullOrEmpty(OtherMonthDayStyleCss)) - return OtherMonthDayStyleCss; - if (isWeekend && !string.IsNullOrEmpty(WeekendDayStyleCss)) - return WeekendDayStyleCss; - - return DayStyleCss; + if (isSelected) + { + var css = GetEffectiveCss(SelectedDayStyle, SelectedDayStyleCss); + if (!string.IsNullOrEmpty(css)) return css; + } + if (isToday) + { + var css = GetEffectiveCss(TodayDayStyle, TodayDayStyleCss); + if (!string.IsNullOrEmpty(css)) return css; + } + if (isOtherMonth) + { + var css = GetEffectiveCss(OtherMonthDayStyle, OtherMonthDayStyleCss); + if (!string.IsNullOrEmpty(css)) return css; + } + if (isWeekend) + { + var css = GetEffectiveCss(WeekendDayStyle, WeekendDayStyleCss); + if (!string.IsNullOrEmpty(css)) return css; + } + + return GetEffectiveCss(DayStyle, DayStyleCss); + } + #pragma warning restore CS0612, CS0618 + + private string GetDayCellStyle(DateTime date) + { + var isToday = date.Date == DateTime.Today; + var isSelected = _selectedDays.Contains(date.Date); + var isOtherMonth = date.Month != _visibleMonth.Month; + var isWeekend = date.DayOfWeek == DayOfWeek.Saturday || date.DayOfWeek == DayOfWeek.Sunday; + + if (isSelected) + { + var style = SelectedDayStyle?.ToString(); + if (!string.IsNullOrEmpty(style)) return style; + } + if (isToday) + { + var style = TodayDayStyle?.ToString(); + if (!string.IsNullOrEmpty(style)) return style; + } + if (isOtherMonth) + { + var style = OtherMonthDayStyle?.ToString(); + if (!string.IsNullOrEmpty(style)) return style; + } + if (isWeekend) + { + var style = WeekendDayStyle?.ToString(); + if (!string.IsNullOrEmpty(style)) return style; + } + + return DayStyle?.ToString(); + } + + private static string GetEffectiveCss(TableItemStyle tableItemStyle, string legacyCss) + { + if (!string.IsNullOrEmpty(tableItemStyle?.CssClass)) + return tableItemStyle.CssClass; + return legacyCss; + } + + private static string GetEffectiveStyle(TableItemStyle tableItemStyle) + { + return tableItemStyle?.ToString(); } /// diff --git a/src/BlazorWebFormsComponents/CalendarDayHeaderStyle.razor b/src/BlazorWebFormsComponents/CalendarDayHeaderStyle.razor new file mode 100644 index 000000000..c6fe55148 --- /dev/null +++ b/src/BlazorWebFormsComponents/CalendarDayHeaderStyle.razor @@ -0,0 +1 @@ +@inherits UiTableItemStyle diff --git a/src/BlazorWebFormsComponents/CalendarDayHeaderStyle.razor.cs b/src/BlazorWebFormsComponents/CalendarDayHeaderStyle.razor.cs new file mode 100644 index 000000000..866ad1d36 --- /dev/null +++ b/src/BlazorWebFormsComponents/CalendarDayHeaderStyle.razor.cs @@ -0,0 +1,20 @@ +using BlazorWebFormsComponents.Interfaces; +using Microsoft.AspNetCore.Components; + +namespace BlazorWebFormsComponents +{ + public partial class CalendarDayHeaderStyle : UiTableItemStyle + { + [CascadingParameter(Name = "ParentCalendar")] + protected ICalendarStyleContainer ParentCalendar { get; set; } + + protected override void OnInitialized() + { + if (ParentCalendar != null) + { + theStyle = ParentCalendar.DayHeaderStyle; + } + base.OnInitialized(); + } + } +} diff --git a/src/BlazorWebFormsComponents/CalendarDayStyle.razor b/src/BlazorWebFormsComponents/CalendarDayStyle.razor new file mode 100644 index 000000000..c6fe55148 --- /dev/null +++ b/src/BlazorWebFormsComponents/CalendarDayStyle.razor @@ -0,0 +1 @@ +@inherits UiTableItemStyle diff --git a/src/BlazorWebFormsComponents/CalendarDayStyle.razor.cs b/src/BlazorWebFormsComponents/CalendarDayStyle.razor.cs new file mode 100644 index 000000000..9335c5c1c --- /dev/null +++ b/src/BlazorWebFormsComponents/CalendarDayStyle.razor.cs @@ -0,0 +1,20 @@ +using BlazorWebFormsComponents.Interfaces; +using Microsoft.AspNetCore.Components; + +namespace BlazorWebFormsComponents +{ + public partial class CalendarDayStyle : UiTableItemStyle + { + [CascadingParameter(Name = "ParentCalendar")] + protected ICalendarStyleContainer ParentCalendar { get; set; } + + protected override void OnInitialized() + { + if (ParentCalendar != null) + { + theStyle = ParentCalendar.DayStyle; + } + base.OnInitialized(); + } + } +} diff --git a/src/BlazorWebFormsComponents/CalendarNextPrevStyle.razor b/src/BlazorWebFormsComponents/CalendarNextPrevStyle.razor new file mode 100644 index 000000000..c6fe55148 --- /dev/null +++ b/src/BlazorWebFormsComponents/CalendarNextPrevStyle.razor @@ -0,0 +1 @@ +@inherits UiTableItemStyle diff --git a/src/BlazorWebFormsComponents/CalendarNextPrevStyle.razor.cs b/src/BlazorWebFormsComponents/CalendarNextPrevStyle.razor.cs new file mode 100644 index 000000000..6cbf0eff0 --- /dev/null +++ b/src/BlazorWebFormsComponents/CalendarNextPrevStyle.razor.cs @@ -0,0 +1,20 @@ +using BlazorWebFormsComponents.Interfaces; +using Microsoft.AspNetCore.Components; + +namespace BlazorWebFormsComponents +{ + public partial class CalendarNextPrevStyle : UiTableItemStyle + { + [CascadingParameter(Name = "ParentCalendar")] + protected ICalendarStyleContainer ParentCalendar { get; set; } + + protected override void OnInitialized() + { + if (ParentCalendar != null) + { + theStyle = ParentCalendar.NextPrevStyle; + } + base.OnInitialized(); + } + } +} diff --git a/src/BlazorWebFormsComponents/CalendarOtherMonthDayStyle.razor b/src/BlazorWebFormsComponents/CalendarOtherMonthDayStyle.razor new file mode 100644 index 000000000..c6fe55148 --- /dev/null +++ b/src/BlazorWebFormsComponents/CalendarOtherMonthDayStyle.razor @@ -0,0 +1 @@ +@inherits UiTableItemStyle diff --git a/src/BlazorWebFormsComponents/CalendarOtherMonthDayStyle.razor.cs b/src/BlazorWebFormsComponents/CalendarOtherMonthDayStyle.razor.cs new file mode 100644 index 000000000..3920745f9 --- /dev/null +++ b/src/BlazorWebFormsComponents/CalendarOtherMonthDayStyle.razor.cs @@ -0,0 +1,20 @@ +using BlazorWebFormsComponents.Interfaces; +using Microsoft.AspNetCore.Components; + +namespace BlazorWebFormsComponents +{ + public partial class CalendarOtherMonthDayStyle : UiTableItemStyle + { + [CascadingParameter(Name = "ParentCalendar")] + protected ICalendarStyleContainer ParentCalendar { get; set; } + + protected override void OnInitialized() + { + if (ParentCalendar != null) + { + theStyle = ParentCalendar.OtherMonthDayStyle; + } + base.OnInitialized(); + } + } +} diff --git a/src/BlazorWebFormsComponents/CalendarSelectedDayStyle.razor b/src/BlazorWebFormsComponents/CalendarSelectedDayStyle.razor new file mode 100644 index 000000000..c6fe55148 --- /dev/null +++ b/src/BlazorWebFormsComponents/CalendarSelectedDayStyle.razor @@ -0,0 +1 @@ +@inherits UiTableItemStyle diff --git a/src/BlazorWebFormsComponents/CalendarSelectedDayStyle.razor.cs b/src/BlazorWebFormsComponents/CalendarSelectedDayStyle.razor.cs new file mode 100644 index 000000000..451c2df21 --- /dev/null +++ b/src/BlazorWebFormsComponents/CalendarSelectedDayStyle.razor.cs @@ -0,0 +1,20 @@ +using BlazorWebFormsComponents.Interfaces; +using Microsoft.AspNetCore.Components; + +namespace BlazorWebFormsComponents +{ + public partial class CalendarSelectedDayStyle : UiTableItemStyle + { + [CascadingParameter(Name = "ParentCalendar")] + protected ICalendarStyleContainer ParentCalendar { get; set; } + + protected override void OnInitialized() + { + if (ParentCalendar != null) + { + theStyle = ParentCalendar.SelectedDayStyle; + } + base.OnInitialized(); + } + } +} diff --git a/src/BlazorWebFormsComponents/CalendarSelectorStyle.razor b/src/BlazorWebFormsComponents/CalendarSelectorStyle.razor new file mode 100644 index 000000000..c6fe55148 --- /dev/null +++ b/src/BlazorWebFormsComponents/CalendarSelectorStyle.razor @@ -0,0 +1 @@ +@inherits UiTableItemStyle diff --git a/src/BlazorWebFormsComponents/CalendarSelectorStyle.razor.cs b/src/BlazorWebFormsComponents/CalendarSelectorStyle.razor.cs new file mode 100644 index 000000000..0c1a7ddfa --- /dev/null +++ b/src/BlazorWebFormsComponents/CalendarSelectorStyle.razor.cs @@ -0,0 +1,20 @@ +using BlazorWebFormsComponents.Interfaces; +using Microsoft.AspNetCore.Components; + +namespace BlazorWebFormsComponents +{ + public partial class CalendarSelectorStyle : UiTableItemStyle + { + [CascadingParameter(Name = "ParentCalendar")] + protected ICalendarStyleContainer ParentCalendar { get; set; } + + protected override void OnInitialized() + { + if (ParentCalendar != null) + { + theStyle = ParentCalendar.SelectorStyle; + } + base.OnInitialized(); + } + } +} diff --git a/src/BlazorWebFormsComponents/CalendarTitleStyle.razor b/src/BlazorWebFormsComponents/CalendarTitleStyle.razor new file mode 100644 index 000000000..c6fe55148 --- /dev/null +++ b/src/BlazorWebFormsComponents/CalendarTitleStyle.razor @@ -0,0 +1 @@ +@inherits UiTableItemStyle diff --git a/src/BlazorWebFormsComponents/CalendarTitleStyle.razor.cs b/src/BlazorWebFormsComponents/CalendarTitleStyle.razor.cs new file mode 100644 index 000000000..236cf1c45 --- /dev/null +++ b/src/BlazorWebFormsComponents/CalendarTitleStyle.razor.cs @@ -0,0 +1,20 @@ +using BlazorWebFormsComponents.Interfaces; +using Microsoft.AspNetCore.Components; + +namespace BlazorWebFormsComponents +{ + public partial class CalendarTitleStyle : UiTableItemStyle + { + [CascadingParameter(Name = "ParentCalendar")] + protected ICalendarStyleContainer ParentCalendar { get; set; } + + protected override void OnInitialized() + { + if (ParentCalendar != null) + { + theStyle = ParentCalendar.TitleStyle; + } + base.OnInitialized(); + } + } +} diff --git a/src/BlazorWebFormsComponents/CalendarTodayDayStyle.razor b/src/BlazorWebFormsComponents/CalendarTodayDayStyle.razor new file mode 100644 index 000000000..c6fe55148 --- /dev/null +++ b/src/BlazorWebFormsComponents/CalendarTodayDayStyle.razor @@ -0,0 +1 @@ +@inherits UiTableItemStyle diff --git a/src/BlazorWebFormsComponents/CalendarTodayDayStyle.razor.cs b/src/BlazorWebFormsComponents/CalendarTodayDayStyle.razor.cs new file mode 100644 index 000000000..ef9e101d0 --- /dev/null +++ b/src/BlazorWebFormsComponents/CalendarTodayDayStyle.razor.cs @@ -0,0 +1,20 @@ +using BlazorWebFormsComponents.Interfaces; +using Microsoft.AspNetCore.Components; + +namespace BlazorWebFormsComponents +{ + public partial class CalendarTodayDayStyle : UiTableItemStyle + { + [CascadingParameter(Name = "ParentCalendar")] + protected ICalendarStyleContainer ParentCalendar { get; set; } + + protected override void OnInitialized() + { + if (ParentCalendar != null) + { + theStyle = ParentCalendar.TodayDayStyle; + } + base.OnInitialized(); + } + } +} diff --git a/src/BlazorWebFormsComponents/CalendarWeekendDayStyle.razor b/src/BlazorWebFormsComponents/CalendarWeekendDayStyle.razor new file mode 100644 index 000000000..c6fe55148 --- /dev/null +++ b/src/BlazorWebFormsComponents/CalendarWeekendDayStyle.razor @@ -0,0 +1 @@ +@inherits UiTableItemStyle diff --git a/src/BlazorWebFormsComponents/CalendarWeekendDayStyle.razor.cs b/src/BlazorWebFormsComponents/CalendarWeekendDayStyle.razor.cs new file mode 100644 index 000000000..8bfca67e1 --- /dev/null +++ b/src/BlazorWebFormsComponents/CalendarWeekendDayStyle.razor.cs @@ -0,0 +1,20 @@ +using BlazorWebFormsComponents.Interfaces; +using Microsoft.AspNetCore.Components; + +namespace BlazorWebFormsComponents +{ + public partial class CalendarWeekendDayStyle : UiTableItemStyle + { + [CascadingParameter(Name = "ParentCalendar")] + protected ICalendarStyleContainer ParentCalendar { get; set; } + + protected override void OnInitialized() + { + if (ParentCalendar != null) + { + theStyle = ParentCalendar.WeekendDayStyle; + } + base.OnInitialized(); + } + } +} diff --git a/src/BlazorWebFormsComponents/CheckBox.razor.cs b/src/BlazorWebFormsComponents/CheckBox.razor.cs index f122a6647..24bd60673 100644 --- a/src/BlazorWebFormsComponents/CheckBox.razor.cs +++ b/src/BlazorWebFormsComponents/CheckBox.razor.cs @@ -1,4 +1,5 @@ using BlazorWebFormsComponents.Enums; +using BlazorWebFormsComponents.Validations; using Microsoft.AspNetCore.Components; using System; using System.Threading.Tasks; @@ -10,6 +11,15 @@ public partial class CheckBox : BaseStyledComponent private string _inputId => !string.IsNullOrEmpty(ClientID) ? ClientID : _generatedInputId; private readonly string _generatedInputId = Guid.NewGuid().ToString("N"); + [Parameter] + public bool CausesValidation { get; set; } = true; + + [Parameter] + public string ValidationGroup { get; set; } + + [CascadingParameter(Name = "ValidationGroupCoordinator")] + protected ValidationGroupCoordinator Coordinator { get; set; } + [Parameter] public bool Checked { get; set; } @@ -31,6 +41,12 @@ public partial class CheckBox : BaseStyledComponent private async Task HandleChange(ChangeEventArgs e) { Checked = (bool)e.Value; + + if (CausesValidation && Coordinator != null) + { + Coordinator.ValidateGroup(ValidationGroup); + } + await CheckedChanged.InvokeAsync(Checked); await OnCheckedChanged.InvokeAsync(e); } diff --git a/src/BlazorWebFormsComponents/CheckBoxList.razor b/src/BlazorWebFormsComponents/CheckBoxList.razor index 6ba6a7527..17d69b3e1 100644 --- a/src/BlazorWebFormsComponents/CheckBoxList.razor +++ b/src/BlazorWebFormsComponents/CheckBoxList.razor @@ -1,7 +1,7 @@ @using BlazorWebFormsComponents.DataBinding @using BlazorWebFormsComponents.Enums @typeparam TItem -@inherits DataBoundComponent +@inherits BaseListControl @if (Visible) { diff --git a/src/BlazorWebFormsComponents/CheckBoxList.razor.cs b/src/BlazorWebFormsComponents/CheckBoxList.razor.cs index 62a632cd1..2d8e34cbf 100644 --- a/src/BlazorWebFormsComponents/CheckBoxList.razor.cs +++ b/src/BlazorWebFormsComponents/CheckBoxList.razor.cs @@ -13,28 +13,10 @@ namespace BlazorWebFormsComponents /// Represents a list control that renders a group of checkboxes for multi-select scenarios. /// /// The type of items in the data source. - public partial class CheckBoxList : DataBoundComponent, IStyle + public partial class CheckBoxList : BaseListControl { private string _baseId = Guid.NewGuid().ToString("N").Substring(0, 8); - /// - /// Gets or sets the collection of static list items in the CheckBoxList. - /// - [Parameter] - public ListItemCollection StaticItems { get; set; } = new(); - - /// - /// Gets or sets the field of the data source that provides the text content of the list items. - /// - [Parameter] - public string DataTextField { get; set; } - - /// - /// Gets or sets the field of the data source that provides the value of each list item. - /// - [Parameter] - public string DataValueField { get; set; } - /// /// Gets or sets the number of columns to display in the CheckBoxList. /// @@ -128,36 +110,6 @@ public int SelectedIndex } } - // IStyle implementation - [Parameter] - public WebColor BackColor { get; set; } - - [Parameter] - public WebColor BorderColor { get; set; } - - [Parameter] - public BorderStyle BorderStyle { get; set; } - - [Parameter] - public Unit BorderWidth { get; set; } - - [Parameter] - public string CssClass { get; set; } - - [Parameter] - public FontInfo Font { get; set; } = new FontInfo(); - - [Parameter] - public WebColor ForeColor { get; set; } - - [Parameter] - public Unit Height { get; set; } - - [Parameter] - public Unit Width { get; set; } - - protected string Style => this.ToStyle().NullIfEmpty(); - private async Task HandleChange(ListItem item, ChangeEventArgs e) { var isChecked = (bool)e.Value; @@ -173,35 +125,5 @@ private async Task HandleChange(ListItem item, ChangeEventArgs e) await OnSelectedIndexChanged.InvokeAsync(e); } - private IEnumerable GetItems() - { - // Return static items first - foreach (var item in StaticItems) - { - yield return item; - } - - // Then data-bound items - if (Items != null) - { - foreach (var dataItem in Items) - { - yield return new ListItem - { - Text = GetPropertyValue(dataItem, DataTextField), - Value = GetPropertyValue(dataItem, DataValueField) - }; - } - } - } - - private string GetPropertyValue(TItem item, string propertyName) - { - if (string.IsNullOrEmpty(propertyName)) - return item?.ToString() ?? string.Empty; - - var prop = typeof(TItem).GetProperty(propertyName); - return prop?.GetValue(item)?.ToString() ?? string.Empty; - } } } diff --git a/src/BlazorWebFormsComponents/DataBinding/BaseDataBoundComponent.cs b/src/BlazorWebFormsComponents/DataBinding/BaseDataBoundComponent.cs index e89abecfd..8d300166e 100644 --- a/src/BlazorWebFormsComponents/DataBinding/BaseDataBoundComponent.cs +++ b/src/BlazorWebFormsComponents/DataBinding/BaseDataBoundComponent.cs @@ -3,7 +3,7 @@ namespace BlazorWebFormsComponents.DataBinding { - public class BaseDataBoundComponent : BaseWebFormsComponent + public class BaseDataBoundComponent : BaseStyledComponent { [Parameter] public virtual object DataSource { get; set; } diff --git a/src/BlazorWebFormsComponents/DataBinding/BaseListControl.cs b/src/BlazorWebFormsComponents/DataBinding/BaseListControl.cs new file mode 100644 index 000000000..bccb019a8 --- /dev/null +++ b/src/BlazorWebFormsComponents/DataBinding/BaseListControl.cs @@ -0,0 +1,105 @@ +using Microsoft.AspNetCore.Components; +using System.Collections.Generic; + +namespace BlazorWebFormsComponents.DataBinding; + +/// +/// Base class for list controls that display a collection of items (DropDownList, CheckBoxList, RadioButtonList, ListBox, BulletedList). +/// Emulates the ASP.NET Web Forms ListControl base class. +/// +/// The type of items in the data source. +public class BaseListControl : DataBoundComponent +{ + /// + /// Gets or sets the collection of static list items. + /// + [Parameter] + public ListItemCollection StaticItems { get; set; } = new(); + + /// + /// Gets or sets the field of the data source that provides the text content of the list items. + /// + [Parameter] + public string DataTextField { get; set; } + + /// + /// Gets or sets the field of the data source that provides the value of each list item. + /// + [Parameter] + public string DataValueField { get; set; } + + /// + /// Gets or sets a formatting string used to control how data bound to the list control is displayed. + /// For example, "{0:C}" formats item text as currency. + /// + [Parameter] + public string DataTextFormatString { get; set; } + + /// + /// Gets or sets a value indicating whether data-bound items are appended to statically defined items. + /// When false (default), data-bound items replace static items. When true, they are appended. + /// + [Parameter] + public bool AppendDataBoundItems { get; set; } + + /// + /// Gets all items from both static and data-bound sources, applying format string and append logic. + /// + protected IEnumerable GetItems() + { + // Include static items when AppendDataBoundItems is true, or when there are no data-bound items + if (AppendDataBoundItems || Items == null) + { + foreach (var item in StaticItems) + { + yield return FormatItem(item); + } + } + + // Then data-bound items + if (Items != null) + { + foreach (var dataItem in Items) + { + var text = GetPropertyValue(dataItem, DataTextField); + yield return new ListItem + { + Text = FormatText(text), + Value = GetPropertyValue(dataItem, DataValueField) + }; + } + } + } + + /// + /// Gets the value of a property from a data item by property name. + /// + protected string GetPropertyValue(TItem item, string propertyName) + { + if (string.IsNullOrEmpty(propertyName)) + return item?.ToString() ?? string.Empty; + + var prop = typeof(TItem).GetProperty(propertyName); + return prop?.GetValue(item)?.ToString() ?? string.Empty; + } + + private string FormatText(string text) + { + if (!string.IsNullOrEmpty(DataTextFormatString)) + return string.Format(DataTextFormatString, text); + return text; + } + + private ListItem FormatItem(ListItem item) + { + if (string.IsNullOrEmpty(DataTextFormatString)) + return item; + return new ListItem + { + Text = FormatText(item.Text), + Value = item.Value, + Selected = item.Selected, + Enabled = item.Enabled + }; + } +} diff --git a/src/BlazorWebFormsComponents/DataGrid.razor.cs b/src/BlazorWebFormsComponents/DataGrid.razor.cs index 77a46889c..ccb33b0f6 100644 --- a/src/BlazorWebFormsComponents/DataGrid.razor.cs +++ b/src/BlazorWebFormsComponents/DataGrid.razor.cs @@ -27,11 +27,6 @@ public partial class DataGrid : DataBoundComponent, IRowColl /// [Parameter] public string DataKeyField { get; set; } - /// - /// The css class of the DataGrid - /// - [Parameter] public string CssClass { get; set; } - /// /// Show or hide the header row /// diff --git a/src/BlazorWebFormsComponents/DataList.razor.cs b/src/BlazorWebFormsComponents/DataList.razor.cs index 05514424d..96ab31ef1 100644 --- a/src/BlazorWebFormsComponents/DataList.razor.cs +++ b/src/BlazorWebFormsComponents/DataList.razor.cs @@ -8,7 +8,7 @@ namespace BlazorWebFormsComponents { - public partial class DataList : DataBoundComponent, IStyle, IDataListStyleContainer + public partial class DataList : DataBoundComponent, IDataListStyleContainer { private static readonly Dictionary _GridLines = new Dictionary { {DataListEnum.None, null }, @@ -18,7 +18,6 @@ public partial class DataList : DataBoundComponent, IStyle, }; protected string CalculatedStyle { get; set; } protected string? CalculatedGridLines { get => _GridLines[this.GridLines]; } - [Parameter] public string AccessKey { get; set; } [Parameter] public string Caption { get; set; } [Parameter] public VerticalAlign CaptionAlign { get; set; } = VerticalAlign.NotSet; [Parameter] public int CellPadding { get; set; } @@ -44,19 +43,9 @@ public partial class DataList : DataBoundComponent, IStyle, [Parameter] public RenderFragment SeparatorStyleContent { get; set; } [Parameter] public bool ShowHeader { get; set; } = true; [Parameter] public bool ShowFooter { get; set; } = true; - [Parameter] public string Style { get; set; } + [Parameter] public new string Style { get; set; } [Parameter] public string ToolTip { get; set; } [Parameter] public bool UseAccessibleHeader { get; set; } = false; - [Parameter] public WebColor BackColor { get; set; } - [Parameter] public WebColor BorderColor { get; set; } - [Parameter] public BorderStyle BorderStyle { get; set; } - [Parameter] public Unit BorderWidth { get; set; } - [Parameter] public string CssClass { get; set; } - [Parameter] public FontInfo Font { get; set; } = new FontInfo(); - [Parameter] public WebColor ForeColor { get; set; } - [Parameter] public Unit Height { get; set; } - [Parameter] public Unit Width { get; set; } - [Parameter] public EventCallback OnItemDataBound { get; set; } diff --git a/src/BlazorWebFormsComponents/DetailsView.razor.cs b/src/BlazorWebFormsComponents/DetailsView.razor.cs index db854c3cb..03c05385b 100644 --- a/src/BlazorWebFormsComponents/DetailsView.razor.cs +++ b/src/BlazorWebFormsComponents/DetailsView.razor.cs @@ -60,12 +60,6 @@ public partial class DetailsView : DataBoundComponent [Parameter] public bool AutoGenerateInsertButton { get; set; } - /// - /// Gets or sets the CSS class for the control. - /// - [Parameter] - public string CssClass { get; set; } - /// /// Gets or sets the gridlines style for the table. /// diff --git a/src/BlazorWebFormsComponents/DropDownList.razor b/src/BlazorWebFormsComponents/DropDownList.razor index cac4eb4af..b735bc5b8 100644 --- a/src/BlazorWebFormsComponents/DropDownList.razor +++ b/src/BlazorWebFormsComponents/DropDownList.razor @@ -1,6 +1,6 @@ @using BlazorWebFormsComponents.DataBinding @typeparam TItem -@inherits DataBoundComponent +@inherits BaseListControl @if (Visible) { diff --git a/src/BlazorWebFormsComponents/DropDownList.razor.cs b/src/BlazorWebFormsComponents/DropDownList.razor.cs index ffd1ce373..e1e4c81e4 100644 --- a/src/BlazorWebFormsComponents/DropDownList.razor.cs +++ b/src/BlazorWebFormsComponents/DropDownList.razor.cs @@ -9,143 +9,64 @@ namespace BlazorWebFormsComponents { - /// - /// Represents a drop-down list control that allows the user to select a single item from a list. - /// - /// The type of items in the data source. - public partial class DropDownList : DataBoundComponent, IStyle - { - /// - /// Gets or sets the collection of list items in the DropDownList. - /// - [Parameter] - public ListItemCollection StaticItems { get; set; } = new(); - - /// - /// Gets or sets the selected value. - /// - [Parameter] - public string SelectedValue { get; set; } - - /// - /// Gets or sets the event callback that is invoked when the selected value changes. - /// - [Parameter] - public EventCallback SelectedValueChanged { get; set; } - - /// - /// Gets or sets the zero-based index of the selected item. - /// - [Parameter] - public int SelectedIndex { get; set; } = -1; - - /// - /// Gets or sets the event callback that is invoked when the selected index changes. - /// - [Parameter] - public EventCallback SelectedIndexChanged { get; set; } - - /// - /// Gets or sets the field of the data source that provides the text content of the list items. - /// - [Parameter] - public string DataTextField { get; set; } - - /// - /// Gets or sets the field of the data source that provides the value of each list item. - /// - [Parameter] - public string DataValueField { get; set; } - - /// - /// Gets or sets the event callback that is invoked when the selected index changes. - /// - [Parameter] - public EventCallback OnSelectedIndexChanged { get; set; } - - /// - /// 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. - /// - [Parameter, Obsolete("AutoPostBack is not supported in Blazor. Use OnSelectedIndexChanged event instead.")] - public bool AutoPostBack { get; set; } - - /// - /// Gets the currently selected item. - /// - public ListItem SelectedItem => GetItems().FirstOrDefault(i => i.Value == SelectedValue); - - // IStyle implementation - [Parameter] - public WebColor BackColor { get; set; } - - [Parameter] - public WebColor BorderColor { get; set; } - - [Parameter] - public BorderStyle BorderStyle { get; set; } - - [Parameter] - public Unit BorderWidth { get; set; } - - [Parameter] - public string CssClass { get; set; } - - [Parameter] - public FontInfo Font { get; set; } = new FontInfo(); - - [Parameter] - public WebColor ForeColor { get; set; } - - [Parameter] - public Unit Height { get; set; } - - [Parameter] - public Unit Width { get; set; } - - protected string Style => this.ToStyle().NullIfEmpty(); - - private async Task HandleChange(ChangeEventArgs e) - { - SelectedValue = e.Value?.ToString(); - await SelectedValueChanged.InvokeAsync(SelectedValue); - - var items = GetItems().ToList(); - SelectedIndex = items.FindIndex(i => i.Value == SelectedValue); - await SelectedIndexChanged.InvokeAsync(SelectedIndex); - - await OnSelectedIndexChanged.InvokeAsync(e); - } - - private IEnumerable GetItems() - { - // Return static Items first - foreach (var item in StaticItems) - { - yield return item; - } - - // Then data-bound items - if (Items != null) - { - foreach (var dataItem in Items) - { - yield return new ListItem - { - Text = GetPropertyValue(dataItem, DataTextField), - Value = GetPropertyValue(dataItem, DataValueField) - }; - } - } - } +/// +/// Represents a drop-down list control that allows the user to select a single item from a list. +/// +/// The type of items in the data source. +public partial class DropDownList : BaseListControl +{ +/// +/// Gets or sets the selected value. +/// +[Parameter] +public string SelectedValue { get; set; } + +/// +/// Gets or sets the event callback that is invoked when the selected value changes. +/// +[Parameter] +public EventCallback SelectedValueChanged { get; set; } + +/// +/// Gets or sets the zero-based index of the selected item. +/// +[Parameter] +public int SelectedIndex { get; set; } = -1; + +/// +/// Gets or sets the event callback that is invoked when the selected index changes. +/// +[Parameter] +public EventCallback SelectedIndexChanged { get; set; } + +/// +/// Gets or sets the event callback that is invoked when the selected index changes. +/// +[Parameter] +public EventCallback OnSelectedIndexChanged { get; set; } + +/// +/// 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. +/// +[Parameter, Obsolete("AutoPostBack is not supported in Blazor. Use OnSelectedIndexChanged event instead.")] +public bool AutoPostBack { get; set; } + +/// +/// Gets the currently selected item. +/// +public ListItem SelectedItem => GetItems().FirstOrDefault(i => i.Value == SelectedValue); + +private async Task HandleChange(ChangeEventArgs e) +{ +SelectedValue = e.Value?.ToString(); +await SelectedValueChanged.InvokeAsync(SelectedValue); - private string GetPropertyValue(TItem item, string propertyName) - { - if (string.IsNullOrEmpty(propertyName)) - return item?.ToString() ?? string.Empty; +var items = GetItems().ToList(); +SelectedIndex = items.FindIndex(i => i.Value == SelectedValue); +await SelectedIndexChanged.InvokeAsync(SelectedIndex); - var prop = typeof(TItem).GetProperty(propertyName); - return prop?.GetValue(item)?.ToString() ?? string.Empty; - } - } +await OnSelectedIndexChanged.InvokeAsync(e); +} } +} \ No newline at end of file diff --git a/src/BlazorWebFormsComponents/Enums/DayNameFormat.cs b/src/BlazorWebFormsComponents/Enums/DayNameFormat.cs new file mode 100644 index 000000000..c379b8736 --- /dev/null +++ b/src/BlazorWebFormsComponents/Enums/DayNameFormat.cs @@ -0,0 +1,10 @@ +namespace BlazorWebFormsComponents.Enums; + +public enum DayNameFormat +{ + Full = 0, + Short = 1, + FirstLetter = 2, + FirstTwoLetters = 3, + Shortest = 4 +} diff --git a/src/BlazorWebFormsComponents/Enums/Orientation.cs b/src/BlazorWebFormsComponents/Enums/Orientation.cs new file mode 100644 index 000000000..74c64615c --- /dev/null +++ b/src/BlazorWebFormsComponents/Enums/Orientation.cs @@ -0,0 +1,7 @@ +namespace BlazorWebFormsComponents.Enums; + +public enum Orientation +{ + Horizontal = 0, + Vertical = 1 +} diff --git a/src/BlazorWebFormsComponents/Enums/SortDirection.cs b/src/BlazorWebFormsComponents/Enums/SortDirection.cs new file mode 100644 index 000000000..c421d6db1 --- /dev/null +++ b/src/BlazorWebFormsComponents/Enums/SortDirection.cs @@ -0,0 +1,8 @@ +namespace BlazorWebFormsComponents.Enums +{ + public enum SortDirection + { + Ascending = 0, + Descending = 1 + } +} diff --git a/src/BlazorWebFormsComponents/Enums/TitleFormat.cs b/src/BlazorWebFormsComponents/Enums/TitleFormat.cs new file mode 100644 index 000000000..67e5c3a69 --- /dev/null +++ b/src/BlazorWebFormsComponents/Enums/TitleFormat.cs @@ -0,0 +1,7 @@ +namespace BlazorWebFormsComponents.Enums; + +public enum TitleFormat +{ + Month = 0, + MonthYear = 1 +} diff --git a/src/BlazorWebFormsComponents/Enums/ValidatorDisplay.cs b/src/BlazorWebFormsComponents/Enums/ValidatorDisplay.cs new file mode 100644 index 000000000..cf548d5a5 --- /dev/null +++ b/src/BlazorWebFormsComponents/Enums/ValidatorDisplay.cs @@ -0,0 +1,8 @@ +namespace BlazorWebFormsComponents.Enums; + +public enum ValidatorDisplay +{ + None = 0, + Static = 1, + Dynamic = 2 +} diff --git a/src/BlazorWebFormsComponents/FormView.razor b/src/BlazorWebFormsComponents/FormView.razor index d609f78bc..a105d876e 100644 --- a/src/BlazorWebFormsComponents/FormView.razor +++ b/src/BlazorWebFormsComponents/FormView.razor @@ -2,62 +2,108 @@ @inherits DataBoundComponent @typeparam ItemType - - + + + } - - @switch (CurrentMode) { - case Enums.FormViewMode.ReadOnly: - @ItemTemplate(CurrentItem); - break; - case Enums.FormViewMode.Edit: - @EditItemTemplate(CurrentItem); - break; + @if (Items == null || !Items.Any()) + { + @* Empty Data Row *@ + @if (EmptyDataTemplate != null) + { + + + + } + else if (!string.IsNullOrEmpty(EmptyDataText)) + { + + + + } + } + else + { + + - - - - + @* Footer Row *@ + @if (FooterTemplate != null || !string.IsNullOrEmpty(FooterText)) + { + + + + }
    - @if (CurrentItem == null) - { - Loading... - } - else - { - DataBinding(EventArgs.Empty); + @* Header Row *@ + @if (HeaderTemplate != null || !string.IsNullOrEmpty(HeaderText)) + { +
    + @if (HeaderTemplate != null) + { + @HeaderTemplate + } + else + { + @HeaderText + } +
    + @EmptyDataTemplate +
    @EmptyDataText
    + @if (CurrentItem == null) + { + Loading... + } + else + { + DataBinding(EventArgs.Empty); + + + @switch (CurrentMode) { + case Enums.FormViewMode.ReadOnly: + @ItemTemplate(CurrentItem); + break; + case Enums.FormViewMode.Edit: + @EditItemTemplate(CurrentItem); + break; case Enums.FormViewMode.Insert: @InsertItemTemplate(CurrentItem); - break; - } - - - DataBound(EventArgs.Empty); + break; } -
    - - @** BEGIN PAGER **@ - - @if (Items == null) - { - - } - else - { + + + DataBound(EventArgs.Empty); + } + + + + + + } -
    Loading...
    + + @** BEGIN PAGER **@ + @for (int i = 1; i <= Items.Count(); i++) - { + { if (i == Position) { - + } else { var thisPosition = i; - + } - } - } - + +
    @i@i@i@i
    +
    -
    + @if (FooterTemplate != null) + { + @FooterTemplate + } + else + { + @FooterText + } +
    diff --git a/src/BlazorWebFormsComponents/FormView.razor.cs b/src/BlazorWebFormsComponents/FormView.razor.cs index 5b222fef3..0889cf4bd 100644 --- a/src/BlazorWebFormsComponents/FormView.razor.cs +++ b/src/BlazorWebFormsComponents/FormView.razor.cs @@ -37,6 +37,42 @@ public FormView() [Parameter] public RenderFragment ItemTemplate { get; set; } + /// + /// Gets or sets the text displayed in the header row. + /// + [Parameter] + public string HeaderText { get; set; } + + /// + /// Gets or sets the custom header template. Takes precedence over HeaderText. + /// + [Parameter] + public RenderFragment HeaderTemplate { get; set; } + + /// + /// Gets or sets the text displayed in the footer row. + /// + [Parameter] + public string FooterText { get; set; } + + /// + /// Gets or sets the custom footer template. Takes precedence over FooterText. + /// + [Parameter] + public RenderFragment FooterTemplate { get; set; } + + /// + /// Gets or sets the text shown when the DataSource is empty or null. + /// + [Parameter] + public string EmptyDataText { get; set; } + + /// + /// Gets or sets the custom template for the empty state. Takes precedence over EmptyDataText. + /// + [Parameter] + public RenderFragment EmptyDataTemplate { get; set; } + public ItemType CurrentItem { get; set; } public FormViewMode CurrentMode { get; private set; } diff --git a/src/BlazorWebFormsComponents/GridView.razor b/src/BlazorWebFormsComponents/GridView.razor index 0d714395c..f4e4935b2 100644 --- a/src/BlazorWebFormsComponents/GridView.razor +++ b/src/BlazorWebFormsComponents/GridView.razor @@ -10,7 +10,18 @@
    @column.HeaderText@column.HeaderText@column.HeaderText
    + @EmptyDataText
    + + + @for (int i = 0; i < TotalPages; i++) + { + var pageIndex = i; + @if (i == PageIndex) + { + + } + else + { + + } + } + +
    @(i + 1)@(pageIndex + 1)
    +
    } diff --git a/src/BlazorWebFormsComponents/GridView.razor.cs b/src/BlazorWebFormsComponents/GridView.razor.cs index 378b5be8d..fa3652a8f 100644 --- a/src/BlazorWebFormsComponents/GridView.razor.cs +++ b/src/BlazorWebFormsComponents/GridView.razor.cs @@ -1,7 +1,11 @@ using BlazorWebFormsComponents.DataBinding; +using BlazorWebFormsComponents.Enums; using BlazorWebFormsComponents.Interfaces; using Microsoft.AspNetCore.Components; +using System; using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; namespace BlazorWebFormsComponents { @@ -28,9 +32,59 @@ public partial class GridView : DataBoundComponent, IRowColl [Parameter] public string DataKeyNames { get; set; } /// - /// The css class of the GridView + /// Gets or sets the index of the row to edit. -1 means no row is being edited. /// - [Parameter] public string CssClass { get; set; } + [Parameter] public int EditIndex { get; set; } = -1; + + /// + /// Gets or sets the style applied to the row being edited. + /// + [Parameter] public TableItemStyle EditRowStyle { get; set; } + + /// + /// Enables or disables sorting for the GridView + /// + [Parameter] public bool AllowSorting { get; set; } + + /// + /// The current sort direction + /// + [Parameter] public SortDirection SortDirection { get; set; } = SortDirection.Ascending; + + /// + /// The current sort expression (column name) + /// + [Parameter] public string SortExpression { get; set; } + + /// + /// Fires before sort is applied. Can be cancelled. + /// + [Parameter] public EventCallback Sorting { get; set; } + + /// + /// Fires after sort is applied + /// + [Parameter] public EventCallback Sorted { get; set; } + + /// + /// Gets or sets whether paging is enabled. + /// + [Parameter] public bool AllowPaging { get; set; } + + /// + /// Gets or sets the number of items to display per page. + /// + [Parameter] public int PageSize { get; set; } = 10; + + /// + /// Gets or sets the current page index (zero-based). + /// + [Parameter] public int PageIndex { get; set; } + + /// + /// Occurs after the page index has changed. + /// + [Parameter] public EventCallback PageIndexChanged { get; set; } /// public List> ColumnList { get; set; } = new List>(); @@ -66,6 +120,138 @@ protected override void OnInitialized() [Parameter] public EventCallback OnRowCommand { get; set; } + /// + /// Occurs when a row's Edit button is clicked, but before the row enters edit mode. + /// + [Parameter] public EventCallback RowEditing { get; set; } + + /// + /// Occurs when a row's Update button is clicked, but before the row is updated. + /// + [Parameter] public EventCallback RowUpdating { get; set; } + + /// + /// Occurs when a row's Delete button is clicked, but before the row is deleted. + /// + [Parameter] public EventCallback RowDeleting { get; set; } + + /// + /// Occurs when a row's Cancel button is clicked, but before the row exits edit mode. + /// + [Parameter] public EventCallback RowCancelingEdit { get; set; } + + /// + /// Gets the total number of pages based on item count and page size. + /// + public int TotalPages => Items != null && AllowPaging && PageSize > 0 + ? (int)Math.Ceiling((double)Items.Count() / PageSize) + : 1; + + /// + /// Gets the items for the current page, or all items if paging is disabled. + /// + protected IEnumerable PagedItems + { + get + { + if (Items == null) return Enumerable.Empty(); + if (!AllowPaging) return Items; + return Items.Skip(PageIndex * PageSize).Take(PageSize); + } + } + + /// + /// Navigates to the specified page index. + /// + protected async Task GoToPage(int newPageIndex) + { + if (Items == null) return; + + var oldPageIndex = PageIndex; + var totalPages = TotalPages; + var startRowIndex = newPageIndex * PageSize; + + var args = new PageChangedEventArgs(newPageIndex, oldPageIndex, totalPages, startRowIndex); + + PageIndex = args.NewPageIndex; + await PageIndexChanged.InvokeAsync(args); + StateHasChanged(); + } + + /// + /// Initiates a sort operation for the specified sort expression + /// + internal async Task Sort(string sortExpression) + { + var newDirection = (sortExpression == SortExpression && SortDirection == SortDirection.Ascending) + ? SortDirection.Descending + : SortDirection.Ascending; + + var args = new GridViewSortEventArgs(sortExpression, newDirection); + await Sorting.InvokeAsync(args); + if (args.Cancel) return; + + SortExpression = args.SortExpression; + SortDirection = args.SortDirection; + await Sorted.InvokeAsync(args); + StateHasChanged(); + } + + /// + /// Puts the specified row into edit mode. + /// + internal async Task EditRow(int rowIndex) + { + var args = new GridViewEditEventArgs(rowIndex); + await RowEditing.InvokeAsync(args); + if (args.Cancel) return; + EditIndex = args.NewEditIndex; + StateHasChanged(); + } + + /// + /// Fires the RowUpdating event for the specified row and exits edit mode. + /// + internal async Task UpdateRow(int rowIndex) + { + var args = new GridViewUpdateEventArgs(rowIndex); + await RowUpdating.InvokeAsync(args); + if (args.Cancel) return; + EditIndex = -1; + StateHasChanged(); + } + + /// + /// Fires the RowDeleting event for the specified row. + /// + internal async Task DeleteRow(int rowIndex) + { + var args = new GridViewDeleteEventArgs(rowIndex); + await RowDeleting.InvokeAsync(args); + } + + /// + /// Cancels edit mode for the specified row. + /// + internal async Task CancelEdit(int rowIndex) + { + var args = new GridViewCancelEditEventArgs(rowIndex); + await RowCancelingEdit.InvokeAsync(args); + if (args.Cancel) return; + EditIndex = -1; + StateHasChanged(); + } + + /// + /// Gets whether the auto-generated command column should be displayed. + /// + internal bool ShowCommandColumn => RowEditing.HasDelegate || RowUpdating.HasDelegate || RowDeleting.HasDelegate || RowCancelingEdit.HasDelegate; + + /// + /// Gets the total column count including the auto-generated command column. + /// + internal int TotalColumnCount => ColumnList.Count + (ShowCommandColumn ? 1 : 0); + /// public void AddColumn(IColumn column) { diff --git a/src/BlazorWebFormsComponents/GridViewCancelEditEventArgs.cs b/src/BlazorWebFormsComponents/GridViewCancelEditEventArgs.cs new file mode 100644 index 000000000..ff1cd7c02 --- /dev/null +++ b/src/BlazorWebFormsComponents/GridViewCancelEditEventArgs.cs @@ -0,0 +1,15 @@ +using System; + +namespace BlazorWebFormsComponents +{ + public class GridViewCancelEditEventArgs : EventArgs + { + public int RowIndex { get; } + public bool Cancel { get; set; } + + public GridViewCancelEditEventArgs(int rowIndex) + { + RowIndex = rowIndex; + } + } +} diff --git a/src/BlazorWebFormsComponents/GridViewDeleteEventArgs.cs b/src/BlazorWebFormsComponents/GridViewDeleteEventArgs.cs new file mode 100644 index 000000000..0ae7dfc2d --- /dev/null +++ b/src/BlazorWebFormsComponents/GridViewDeleteEventArgs.cs @@ -0,0 +1,15 @@ +using System; + +namespace BlazorWebFormsComponents +{ + public class GridViewDeleteEventArgs : EventArgs + { + public int RowIndex { get; } + public bool Cancel { get; set; } + + public GridViewDeleteEventArgs(int rowIndex) + { + RowIndex = rowIndex; + } + } +} diff --git a/src/BlazorWebFormsComponents/GridViewEditEventArgs.cs b/src/BlazorWebFormsComponents/GridViewEditEventArgs.cs new file mode 100644 index 000000000..dcba8a958 --- /dev/null +++ b/src/BlazorWebFormsComponents/GridViewEditEventArgs.cs @@ -0,0 +1,15 @@ +using System; + +namespace BlazorWebFormsComponents +{ + public class GridViewEditEventArgs : EventArgs + { + public int NewEditIndex { get; set; } + public bool Cancel { get; set; } + + public GridViewEditEventArgs(int newEditIndex) + { + NewEditIndex = newEditIndex; + } + } +} diff --git a/src/BlazorWebFormsComponents/GridViewRow.razor b/src/BlazorWebFormsComponents/GridViewRow.razor index 2346a661e..a7e502654 100644 --- a/src/BlazorWebFormsComponents/GridViewRow.razor +++ b/src/BlazorWebFormsComponents/GridViewRow.razor @@ -2,14 +2,41 @@ @inherits BaseRow @using Interfaces - + @foreach (IColumn column in Columns) { @if (DataItem is ButtonField) (DataItem as ButtonField).DataItemIndex = DataItemIndex; - @column.Render(DataItem) + @if (IsEditing) + { + @column.RenderEdit(DataItem) + } + else + { + @column.Render(DataItem) + } + + } + @if (GridView != null && (GridView.RowEditing.HasDelegate || GridView.RowUpdating.HasDelegate || GridView.RowDeleting.HasDelegate || GridView.RowCancelingEdit.HasDelegate)) + { + + @if (IsEditing) + { + Update + @((MarkupString)" ") + Cancel + } + else + { + Edit + @if (GridView.RowDeleting.HasDelegate) + { + @((MarkupString)" ") + Delete + } + } } diff --git a/src/BlazorWebFormsComponents/GridViewRow.razor.cs b/src/BlazorWebFormsComponents/GridViewRow.razor.cs index 7164dbc67..e592eaebb 100644 --- a/src/BlazorWebFormsComponents/GridViewRow.razor.cs +++ b/src/BlazorWebFormsComponents/GridViewRow.razor.cs @@ -23,5 +23,20 @@ public partial class GridViewRow : BaseRow /// The columns of the Row /// [Parameter] public List> Columns { get; set; } + + /// + /// Whether this row is in edit mode + /// + [Parameter] public bool IsEditing { get; set; } + + /// + /// Style applied when the row is in edit mode + /// + [Parameter] public TableItemStyle EditRowStyle { get; set; } + + /// + /// Reference to the parent GridView + /// + [Parameter] public GridView GridView { get; set; } } } diff --git a/src/BlazorWebFormsComponents/GridViewSortEventArgs.cs b/src/BlazorWebFormsComponents/GridViewSortEventArgs.cs new file mode 100644 index 000000000..40fa2278d --- /dev/null +++ b/src/BlazorWebFormsComponents/GridViewSortEventArgs.cs @@ -0,0 +1,18 @@ +using BlazorWebFormsComponents.Enums; +using System; + +namespace BlazorWebFormsComponents +{ + public class GridViewSortEventArgs : EventArgs + { + public string SortExpression { get; set; } + public SortDirection SortDirection { get; set; } + public bool Cancel { get; set; } + + public GridViewSortEventArgs(string sortExpression, SortDirection sortDirection) + { + SortExpression = sortExpression; + SortDirection = sortDirection; + } + } +} diff --git a/src/BlazorWebFormsComponents/GridViewUpdateEventArgs.cs b/src/BlazorWebFormsComponents/GridViewUpdateEventArgs.cs new file mode 100644 index 000000000..01fe9d59a --- /dev/null +++ b/src/BlazorWebFormsComponents/GridViewUpdateEventArgs.cs @@ -0,0 +1,15 @@ +using System; + +namespace BlazorWebFormsComponents +{ + public class GridViewUpdateEventArgs : EventArgs + { + public int RowIndex { get; } + public bool Cancel { get; set; } + + public GridViewUpdateEventArgs(int rowIndex) + { + RowIndex = rowIndex; + } + } +} diff --git a/src/BlazorWebFormsComponents/HyperLink.razor b/src/BlazorWebFormsComponents/HyperLink.razor index 7d6ff5a4a..67f8dd632 100644 --- a/src/BlazorWebFormsComponents/HyperLink.razor +++ b/src/BlazorWebFormsComponents/HyperLink.razor @@ -3,13 +3,13 @@ @if (Visible) { - @if (NavigationUrl == null) + @if (NavigateUrl == null) { @Text } else { - @Text + @Text } } diff --git a/src/BlazorWebFormsComponents/HyperLink.razor.cs b/src/BlazorWebFormsComponents/HyperLink.razor.cs index 79c825226..4ed15461c 100644 --- a/src/BlazorWebFormsComponents/HyperLink.razor.cs +++ b/src/BlazorWebFormsComponents/HyperLink.razor.cs @@ -1,11 +1,19 @@ -using Microsoft.AspNetCore.Components; +using System; +using Microsoft.AspNetCore.Components; namespace BlazorWebFormsComponents { public partial class HyperLink : BaseStyledComponent { [Parameter] - public string NavigationUrl { get; set; } + public string NavigateUrl { get; set; } + + [Obsolete("Use NavigateUrl instead")] + public string NavigationUrl + { + get => NavigateUrl; + set => NavigateUrl = value; + } [Parameter] public string Target { get; set; } = string.Empty; diff --git a/src/BlazorWebFormsComponents/Image.razor b/src/BlazorWebFormsComponents/Image.razor index 5ea365dbb..c5222ca36 100644 --- a/src/BlazorWebFormsComponents/Image.razor +++ b/src/BlazorWebFormsComponents/Image.razor @@ -1,41 +1,49 @@ -@using System.Text -@inherits BaseWebFormsComponent -@{ - var element = new StringBuilder($"\"{0}\"", +} - if (!string.IsNullOrEmpty(ToolTip)) - { - element.AppendFormat(" title=\"{0}\"", ToolTip); - } +@code { + private string GetAltText() + { + if (!string.IsNullOrEmpty(AlternateText)) + return AlternateText; + if (GenerateEmptyAlternateText) + return ""; + return null; + } - if(ImageAlign != Enums.ImageAlign.NotSet) - { - element.AppendFormat(" align=\"{0}\"", ImageAlign.ToString().ToLower()); - } + private string GetId() + { + return !string.IsNullOrEmpty(ClientID) ? ClientID : null; + } - element.Append(" />"); -} + private string GetCssClassOrNull() + { + return !string.IsNullOrEmpty(CssClass) ? CssClass : null; + } -@if (Visible) -{ - @((MarkupString)element.ToString()) + private string GetTitle() + { + return !string.IsNullOrEmpty(ToolTip) ? ToolTip : null; + } + + private string GetLongDesc() + { + return DescriptionUrl; + } + + private string GetAlign() + { + return ImageAlign != Enums.ImageAlign.NotSet ? ImageAlign.ToString().ToLower() : null; + } } diff --git a/src/BlazorWebFormsComponents/Image.razor.cs b/src/BlazorWebFormsComponents/Image.razor.cs index b8afdff64..c94f233b0 100644 --- a/src/BlazorWebFormsComponents/Image.razor.cs +++ b/src/BlazorWebFormsComponents/Image.razor.cs @@ -4,7 +4,7 @@ namespace BlazorWebFormsComponents { - public partial class Image : BaseWebFormsComponent, IImageComponent + public partial class Image : BaseStyledComponent, IImageComponent { [Parameter] public string AlternateText { get; set; } diff --git a/src/BlazorWebFormsComponents/Interfaces/ICalendarStyleContainer.cs b/src/BlazorWebFormsComponents/Interfaces/ICalendarStyleContainer.cs new file mode 100644 index 000000000..38892f68d --- /dev/null +++ b/src/BlazorWebFormsComponents/Interfaces/ICalendarStyleContainer.cs @@ -0,0 +1,18 @@ +namespace BlazorWebFormsComponents.Interfaces +{ + /// + /// Interface for components that contain Calendar-specific TableItemStyle properties. + /// + public interface ICalendarStyleContainer + { + TableItemStyle DayStyle { get; } + TableItemStyle TitleStyle { get; } + TableItemStyle DayHeaderStyle { get; } + TableItemStyle TodayDayStyle { get; } + TableItemStyle SelectedDayStyle { get; } + TableItemStyle OtherMonthDayStyle { get; } + TableItemStyle WeekendDayStyle { get; } + TableItemStyle NextPrevStyle { get; } + TableItemStyle SelectorStyle { get; } + } +} diff --git a/src/BlazorWebFormsComponents/Interfaces/IColumn.cs b/src/BlazorWebFormsComponents/Interfaces/IColumn.cs index 83727d1f2..b764ec816 100644 --- a/src/BlazorWebFormsComponents/Interfaces/IColumn.cs +++ b/src/BlazorWebFormsComponents/Interfaces/IColumn.cs @@ -12,10 +12,20 @@ public interface IColumn /// string HeaderText { get; set; } + /// + /// The sort expression for the column + /// + string SortExpression { get; set; } + /// /// The parent IColumnCollection where the IColumn resides /// IColumnCollection ParentColumnsCollection { get; set; } RenderFragment Render(ItemType item); + + /// + /// Renders the column in edit mode. Falls back to Render if not overridden. + /// + RenderFragment RenderEdit(ItemType item); } } diff --git a/src/BlazorWebFormsComponents/Label.razor b/src/BlazorWebFormsComponents/Label.razor index fa0eaff9a..249d14b78 100644 --- a/src/BlazorWebFormsComponents/Label.razor +++ b/src/BlazorWebFormsComponents/Label.razor @@ -1,6 +1,25 @@ -@inherits BaseWebFormsComponent +@inherits BaseStyledComponent @if (Visible) { - @Text + @if (!string.IsNullOrEmpty(AssociatedControlID)) + { + + } + else + { + @Text + } +} + +@code { + private string GetCssClassOrNull() + { + return !string.IsNullOrEmpty(CssClass) ? CssClass : null; + } + + private string GetAccessKeyOrNull() + { + return !string.IsNullOrEmpty(AccessKey) ? AccessKey : null; + } } diff --git a/src/BlazorWebFormsComponents/Label.razor.cs b/src/BlazorWebFormsComponents/Label.razor.cs index c4fb56ebf..244267655 100644 --- a/src/BlazorWebFormsComponents/Label.razor.cs +++ b/src/BlazorWebFormsComponents/Label.razor.cs @@ -3,9 +3,12 @@ namespace BlazorWebFormsComponents { - public partial class Label : BaseWebFormsComponent, ITextComponent + public partial class Label : BaseStyledComponent, ITextComponent { [Parameter] public string Text { get; set; } + + [Parameter] + public string AssociatedControlID { get; set; } } } diff --git a/src/BlazorWebFormsComponents/ListBox.razor b/src/BlazorWebFormsComponents/ListBox.razor index c399ba722..5627fadd4 100644 --- a/src/BlazorWebFormsComponents/ListBox.razor +++ b/src/BlazorWebFormsComponents/ListBox.razor @@ -1,7 +1,7 @@ @using BlazorWebFormsComponents.DataBinding @using BlazorWebFormsComponents.Enums @typeparam TItem -@inherits DataBoundComponent +@inherits BaseListControl @if (Visible) { diff --git a/src/BlazorWebFormsComponents/ListBox.razor.cs b/src/BlazorWebFormsComponents/ListBox.razor.cs index 9582c23b2..f169ed13c 100644 --- a/src/BlazorWebFormsComponents/ListBox.razor.cs +++ b/src/BlazorWebFormsComponents/ListBox.razor.cs @@ -13,14 +13,8 @@ namespace BlazorWebFormsComponents /// Represents a list box control that allows the user to select one or more items from a list. /// /// The type of items in the data source. - public partial class ListBox : DataBoundComponent, IStyle + public partial class ListBox : BaseListControl { - /// - /// Gets or sets the collection of list items in the ListBox. - /// - [Parameter] - public ListItemCollection StaticItems { get; set; } = new(); - /// /// Gets or sets the selected value. /// @@ -57,18 +51,6 @@ public partial class ListBox : DataBoundComponent, IStyle [Parameter] public EventCallback SelectedIndexChanged { get; set; } - /// - /// Gets or sets the field of the data source that provides the text content of the list items. - /// - [Parameter] - public string DataTextField { get; set; } - - /// - /// Gets or sets the field of the data source that provides the value of each list item. - /// - [Parameter] - public string DataValueField { get; set; } - /// /// Gets or sets the number of rows displayed in the ListBox control. /// @@ -105,36 +87,6 @@ public partial class ListBox : DataBoundComponent, IStyle public IEnumerable SelectedItems => GetItems().Where(i => SelectedValues.Contains(i.Value)); - // IStyle implementation - [Parameter] - public WebColor BackColor { get; set; } - - [Parameter] - public WebColor BorderColor { get; set; } - - [Parameter] - public BorderStyle BorderStyle { get; set; } - - [Parameter] - public Unit BorderWidth { get; set; } - - [Parameter] - public string CssClass { get; set; } - - [Parameter] - public FontInfo Font { get; set; } = new FontInfo(); - - [Parameter] - public WebColor ForeColor { get; set; } - - [Parameter] - public Unit Height { get; set; } - - [Parameter] - public Unit Width { get; set; } - - protected string Style => this.ToStyle().NullIfEmpty(); - private bool IsSelected(string value) { if (SelectionMode == ListSelectionMode.Multiple) @@ -169,35 +121,5 @@ private async Task HandleChange(ChangeEventArgs e) await OnSelectedIndexChanged.InvokeAsync(e); } - private IEnumerable GetItems() - { - // Return static Items first - foreach (var item in StaticItems) - { - yield return item; - } - - // Then data-bound items - if (Items != null) - { - foreach (var dataItem in Items) - { - yield return new ListItem - { - Text = GetPropertyValue(dataItem, DataTextField), - Value = GetPropertyValue(dataItem, DataValueField) - }; - } - } - } - - private string GetPropertyValue(TItem item, string propertyName) - { - if (string.IsNullOrEmpty(propertyName)) - return item?.ToString() ?? string.Empty; - - var prop = typeof(TItem).GetProperty(propertyName); - return prop?.GetValue(item)?.ToString() ?? string.Empty; - } } } diff --git a/src/BlazorWebFormsComponents/ListView.razor.cs b/src/BlazorWebFormsComponents/ListView.razor.cs index 728fbc435..2b49f7cf7 100644 --- a/src/BlazorWebFormsComponents/ListView.razor.cs +++ b/src/BlazorWebFormsComponents/ListView.razor.cs @@ -48,7 +48,7 @@ public ListView() /// Style is not applied by this control /// [Parameter, Obsolete("Style is not applied by this control")] - public string Style { get; set; } + public new string Style { get; set; } [Parameter] public RenderFragment ChildContent { get; set; } diff --git a/src/BlazorWebFormsComponents/LoginControls/ChangePassword.razor b/src/BlazorWebFormsComponents/LoginControls/ChangePassword.razor index d6c83deaf..e4b767dc3 100644 --- a/src/BlazorWebFormsComponents/LoginControls/ChangePassword.razor +++ b/src/BlazorWebFormsComponents/LoginControls/ChangePassword.razor @@ -1,4 +1,4 @@ -@inherits BaseWebFormsComponent +@inherits BaseStyledComponent @using BlazorWebFormsComponents.Validations; @using Microsoft.AspNetCore.Components.Forms; @@ -32,7 +32,7 @@ } else { - +
    @@ -172,7 +172,7 @@ } else { - +
    @@ -199,3 +199,10 @@ } + +@code { + private string GetCssClassOrNull() + { + return !string.IsNullOrEmpty(CssClass) ? CssClass : null; + } +} diff --git a/src/BlazorWebFormsComponents/LoginControls/ChangePassword.razor.cs b/src/BlazorWebFormsComponents/LoginControls/ChangePassword.razor.cs index e202858fb..b5ff0fb1e 100644 --- a/src/BlazorWebFormsComponents/LoginControls/ChangePassword.razor.cs +++ b/src/BlazorWebFormsComponents/LoginControls/ChangePassword.razor.cs @@ -7,7 +7,7 @@ namespace BlazorWebFormsComponents.LoginControls { - public partial class ChangePassword : BaseWebFormsComponent + public partial class ChangePassword : BaseStyledComponent { #region Obsolete @@ -238,6 +238,7 @@ protected override void HandleUnknownAttributes() HyperLinkStyle.FromUnknownAttributes(AdditionalAttributes, "HyperLinkStyle-"); } + this.SetFontsFromAttributes(AdditionalAttributes); base.HandleUnknownAttributes(); } diff --git a/src/BlazorWebFormsComponents/LoginControls/CreateUserWizard.razor b/src/BlazorWebFormsComponents/LoginControls/CreateUserWizard.razor index f097e01c4..c72027ada 100644 --- a/src/BlazorWebFormsComponents/LoginControls/CreateUserWizard.razor +++ b/src/BlazorWebFormsComponents/LoginControls/CreateUserWizard.razor @@ -1,4 +1,4 @@ -@inherits BaseWebFormsComponent +@inherits BaseStyledComponent @using BlazorWebFormsComponents.Validations; @using Microsoft.AspNetCore.Components.Forms; @@ -32,7 +32,7 @@ } else { - +
    @if (DisplaySideBar) @@ -188,7 +188,7 @@ } else { -
    +
    @@ -215,3 +215,10 @@ } + +@code { + private string GetCssClassOrNull() + { + return !string.IsNullOrEmpty(CssClass) ? CssClass : null; + } +} diff --git a/src/BlazorWebFormsComponents/LoginControls/CreateUserWizard.razor.cs b/src/BlazorWebFormsComponents/LoginControls/CreateUserWizard.razor.cs index 3d4edd46c..65e12a813 100644 --- a/src/BlazorWebFormsComponents/LoginControls/CreateUserWizard.razor.cs +++ b/src/BlazorWebFormsComponents/LoginControls/CreateUserWizard.razor.cs @@ -7,7 +7,7 @@ namespace BlazorWebFormsComponents.LoginControls { - public partial class CreateUserWizard : BaseWebFormsComponent + public partial class CreateUserWizard : BaseStyledComponent { #region Obsolete @@ -241,6 +241,7 @@ protected override void HandleUnknownAttributes() HyperLinkStyle.FromUnknownAttributes(AdditionalAttributes, "HyperLinkStyle-"); } + this.SetFontsFromAttributes(AdditionalAttributes); base.HandleUnknownAttributes(); } diff --git a/src/BlazorWebFormsComponents/LoginControls/Login.razor b/src/BlazorWebFormsComponents/LoginControls/Login.razor index e17f86e12..f072d9b5d 100644 --- a/src/BlazorWebFormsComponents/LoginControls/Login.razor +++ b/src/BlazorWebFormsComponents/LoginControls/Login.razor @@ -1,4 +1,4 @@ -@inherits BaseWebFormsComponent +@inherits BaseStyledComponent @using BlazorWebFormsComponents.Validations; @using Microsoft.AspNetCore.Components.Forms; @@ -29,7 +29,7 @@ @if (VisibleWhenLoggedIn || !UserAuthenticated) { - +
    @@ -158,7 +158,10 @@ public ForwardRef> UsernameInput = new ForwardRef>(); public ForwardRef> PasswordInput = new ForwardRef>(); - + private string GetCssClassOrNull() + { + return !string.IsNullOrEmpty(CssClass) ? CssClass : null; + } public class LoginModel diff --git a/src/BlazorWebFormsComponents/LoginControls/Login.razor.cs b/src/BlazorWebFormsComponents/LoginControls/Login.razor.cs index e244736e4..faacc5df1 100644 --- a/src/BlazorWebFormsComponents/LoginControls/Login.razor.cs +++ b/src/BlazorWebFormsComponents/LoginControls/Login.razor.cs @@ -7,7 +7,7 @@ namespace BlazorWebFormsComponents.LoginControls { - public partial class Login : BaseWebFormsComponent + public partial class Login : BaseStyledComponent { #region Obsolete Attributes / Properties @@ -184,6 +184,7 @@ protected override void HandleUnknownAttributes() } + this.SetFontsFromAttributes(AdditionalAttributes); base.HandleUnknownAttributes(); } diff --git a/src/BlazorWebFormsComponents/Menu.razor b/src/BlazorWebFormsComponents/Menu.razor index e27c86f42..fcd84553a 100644 --- a/src/BlazorWebFormsComponents/Menu.razor +++ b/src/BlazorWebFormsComponents/Menu.razor @@ -1,8 +1,9 @@ @inherits BaseWebFormsComponent @using BlazorComponentUtilities +@using BlazorWebFormsComponents.Enums
    -
      +
        @ChildContent @ChildNodesRenderFragment @@ -29,6 +30,10 @@ width: auto; } + @($"#{ID} ul.horizontal > li") { + display: inline-block; + } + @($"#{ID} ul.dynamic") { @DynamicMenuStyle?.ToStyle().ToString() z-index: 1; diff --git a/src/BlazorWebFormsComponents/Menu.razor.cs b/src/BlazorWebFormsComponents/Menu.razor.cs index 960201fb4..7e1b7550c 100644 --- a/src/BlazorWebFormsComponents/Menu.razor.cs +++ b/src/BlazorWebFormsComponents/Menu.razor.cs @@ -4,6 +4,7 @@ using System.Threading.Tasks; using System.Xml; using BlazorComponentUtilities; +using BlazorWebFormsComponents.Enums; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Rendering; using Microsoft.JSInterop; @@ -30,6 +31,9 @@ public partial class Menu : BaseWebFormsComponent [Parameter] public int DisappearAfter { get; set; } + [Parameter] + public Orientation Orientation { get; set; } = Orientation.Vertical; + [Parameter] public int StaticDisplayLevels { get; set; } @@ -110,7 +114,7 @@ protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) { - await JS.InvokeVoidAsync("bwfc.Page.AddScriptElement", $"{StaticFilesLocation}Menu/Menu.js", $"new Sys.WebForms.Menu({{ element: '{ID}', disappearAfter: {DisappearAfter}, orientation: 'vertical', tabIndex: 0, disabled: false }});"); + await JS.InvokeVoidAsync("bwfc.Page.AddScriptElement", $"{StaticFilesLocation}Menu/Menu.js", $"new Sys.WebForms.Menu({{ element: '{ID}', disappearAfter: {DisappearAfter}, orientation: '{Orientation.ToString().ToLower()}', tabIndex: 0, disabled: false }});"); } } diff --git a/src/BlazorWebFormsComponents/RadioButton.razor.cs b/src/BlazorWebFormsComponents/RadioButton.razor.cs index e9311d623..4b02efb13 100644 --- a/src/BlazorWebFormsComponents/RadioButton.razor.cs +++ b/src/BlazorWebFormsComponents/RadioButton.razor.cs @@ -1,3 +1,4 @@ +using BlazorWebFormsComponents.Validations; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Web; using System; @@ -10,6 +11,15 @@ public partial class RadioButton : BaseStyledComponent private string _inputId => !string.IsNullOrEmpty(ClientID) ? ClientID : _generatedInputId; private readonly string _generatedInputId = Guid.NewGuid().ToString("N"); + [Parameter] + public bool CausesValidation { get; set; } = true; + + [Parameter] + public string ValidationGroup { get; set; } + + [CascadingParameter(Name = "ValidationGroupCoordinator")] + protected ValidationGroupCoordinator Coordinator { get; set; } + [Parameter] public bool Checked { get; set; } @@ -36,6 +46,12 @@ public partial class RadioButton : BaseStyledComponent private async Task HandleChange(ChangeEventArgs e) { Checked = true; + + if (CausesValidation && Coordinator != null) + { + Coordinator.ValidateGroup(ValidationGroup); + } + await CheckedChanged.InvokeAsync(Checked); await OnCheckedChanged.InvokeAsync(e); } diff --git a/src/BlazorWebFormsComponents/RadioButtonList.razor b/src/BlazorWebFormsComponents/RadioButtonList.razor index e49cb2aa8..e7385a564 100644 --- a/src/BlazorWebFormsComponents/RadioButtonList.razor +++ b/src/BlazorWebFormsComponents/RadioButtonList.razor @@ -1,7 +1,7 @@ @using BlazorWebFormsComponents.DataBinding @using BlazorWebFormsComponents.Enums @typeparam TItem -@inherits DataBoundComponent +@inherits BaseListControl @if (Visible) { diff --git a/src/BlazorWebFormsComponents/RadioButtonList.razor.cs b/src/BlazorWebFormsComponents/RadioButtonList.razor.cs index 04dd58ce7..68e757638 100644 --- a/src/BlazorWebFormsComponents/RadioButtonList.razor.cs +++ b/src/BlazorWebFormsComponents/RadioButtonList.razor.cs @@ -13,28 +13,10 @@ namespace BlazorWebFormsComponents /// Represents a list control that displays a group of radio buttons for single selection. /// /// The type of items in the data source. - public partial class RadioButtonList : DataBoundComponent, IStyle + public partial class RadioButtonList : BaseListControl { private string _groupName = Guid.NewGuid().ToString("N"); - /// - /// Gets or sets the collection of static list items in the RadioButtonList. - /// - [Parameter] - public ListItemCollection StaticItems { get; set; } = new(); - - /// - /// Gets or sets the field of the data source that provides the text content of the list items. - /// - [Parameter] - public string DataTextField { get; set; } - - /// - /// Gets or sets the field of the data source that provides the value of each list item. - /// - [Parameter] - public string DataValueField { get; set; } - /// /// Gets or sets the number of columns to display in the list control. /// @@ -113,36 +95,6 @@ public partial class RadioButtonList : DataBoundComponent, IStyle /// public ListItem SelectedItem => GetItems().FirstOrDefault(i => i.Value == SelectedValue); - // IStyle implementation - [Parameter] - public WebColor BackColor { get; set; } - - [Parameter] - public WebColor BorderColor { get; set; } - - [Parameter] - public BorderStyle BorderStyle { get; set; } - - [Parameter] - public Unit BorderWidth { get; set; } - - [Parameter] - public string CssClass { get; set; } - - [Parameter] - public FontInfo Font { get; set; } = new FontInfo(); - - [Parameter] - public WebColor ForeColor { get; set; } - - [Parameter] - public Unit Height { get; set; } - - [Parameter] - public Unit Width { get; set; } - - protected string Style => this.ToStyle().NullIfEmpty(); - private async Task HandleChange(ListItem item, ChangeEventArgs e) { SelectedValue = item.Value; @@ -155,37 +107,6 @@ private async Task HandleChange(ListItem item, ChangeEventArgs e) await OnSelectedIndexChanged.InvokeAsync(e); } - private IEnumerable GetItems() - { - // Return static Items first - foreach (var item in StaticItems) - { - yield return item; - } - - // Then data-bound items - if (Items != null) - { - foreach (var dataItem in Items) - { - yield return new ListItem - { - Text = GetPropertyValue(dataItem, DataTextField), - Value = GetPropertyValue(dataItem, DataValueField) - }; - } - } - } - - private string GetPropertyValue(TItem item, string propertyName) - { - if (string.IsNullOrEmpty(propertyName)) - return item?.ToString() ?? string.Empty; - - var prop = typeof(TItem).GetProperty(propertyName); - return prop?.GetValue(item)?.ToString() ?? string.Empty; - } - private string GetInputId(int index) => $"{_groupName}_{index}"; } } diff --git a/src/BlazorWebFormsComponents/TemplateField.razor.cs b/src/BlazorWebFormsComponents/TemplateField.razor.cs index 2dd386859..864281486 100644 --- a/src/BlazorWebFormsComponents/TemplateField.razor.cs +++ b/src/BlazorWebFormsComponents/TemplateField.razor.cs @@ -12,9 +12,23 @@ public partial class TemplateField : BaseColumn /// [Parameter] public RenderFragment ItemTemplate { get; set; } + /// + /// The template to display when the row is in edit mode. + /// + [Parameter] public RenderFragment EditItemTemplate { get; set; } + public override RenderFragment Render(ItemType item) { return ItemTemplate(item); } + + public override RenderFragment RenderEdit(ItemType item) + { + if (EditItemTemplate != null) + { + return EditItemTemplate(item); + } + return Render(item); + } } } diff --git a/src/BlazorWebFormsComponents/TextBox.razor.cs b/src/BlazorWebFormsComponents/TextBox.razor.cs index f55da7429..724fd623f 100644 --- a/src/BlazorWebFormsComponents/TextBox.razor.cs +++ b/src/BlazorWebFormsComponents/TextBox.razor.cs @@ -1,5 +1,6 @@ using BlazorWebFormsComponents.Enums; using BlazorWebFormsComponents.Interfaces; +using BlazorWebFormsComponents.Validations; using Microsoft.AspNetCore.Components; using System; using System.Collections.Generic; @@ -8,6 +9,15 @@ namespace BlazorWebFormsComponents { public partial class TextBox : BaseStyledComponent, ITextComponent { + [Parameter] + public bool CausesValidation { get; set; } = true; + + [Parameter] + public string ValidationGroup { get; set; } + + [CascadingParameter(Name = "ValidationGroupCoordinator")] + protected ValidationGroupCoordinator Coordinator { get; set; } + [Parameter] public string Text { get; set; } = string.Empty; diff --git a/src/BlazorWebFormsComponents/TreeView.razor.cs b/src/BlazorWebFormsComponents/TreeView.razor.cs index 45cc78b28..ee0a0f928 100644 --- a/src/BlazorWebFormsComponents/TreeView.razor.cs +++ b/src/BlazorWebFormsComponents/TreeView.razor.cs @@ -10,7 +10,7 @@ namespace BlazorWebFormsComponents { - public partial class TreeView : BaseDataBoundComponent, IStyle + public partial class TreeView : BaseDataBoundComponent { [Parameter] @@ -37,20 +37,6 @@ public partial class TreeView : BaseDataBoundComponent, IStyle [Parameter] public bool UseAccessibilityFeatures { get; set; } = false; - #region IHasStyle - - [Parameter] public WebColor BackColor { get; set; } - [Parameter] public WebColor BorderColor { get; set; } - [Parameter] public BorderStyle BorderStyle { get; set; } - [Parameter] public Unit BorderWidth { get; set; } - [Parameter] public string CssClass { get; set; } - [Parameter] public WebColor ForeColor { get; set; } - [Parameter] public Unit Height { get; set; } - [Parameter] public Unit Width { get; set; } - [Parameter] public FontInfo Font { get; set; } = new FontInfo(); - - #endregion - #region Events protected override async Task OnAfterRenderAsync(bool firstRender) diff --git a/src/BlazorWebFormsComponents/Validations/AspNetValidationSummary.razor b/src/BlazorWebFormsComponents/Validations/AspNetValidationSummary.razor index 17b591a1c..0ec22f79a 100644 --- a/src/BlazorWebFormsComponents/Validations/AspNetValidationSummary.razor +++ b/src/BlazorWebFormsComponents/Validations/AspNetValidationSummary.razor @@ -1,11 +1,15 @@ @using BlazorWebFormsComponents.Enums @inherits BaseStyledComponent -@if (Enabled && Visible) +@if (Enabled && Visible && ShowSummary) {
        @if (IsValid) { + @if (!string.IsNullOrEmpty(HeaderText)) + { + @HeaderText + } @switch (DisplayMode) { case BulletListDisplayMode b: diff --git a/src/BlazorWebFormsComponents/Validations/AspNetValidationSummary.razor.cs b/src/BlazorWebFormsComponents/Validations/AspNetValidationSummary.razor.cs index 8623c8ad0..33634a8eb 100644 --- a/src/BlazorWebFormsComponents/Validations/AspNetValidationSummary.razor.cs +++ b/src/BlazorWebFormsComponents/Validations/AspNetValidationSummary.razor.cs @@ -16,9 +16,32 @@ public partial class AspNetValidationSummary : BaseStyledComponent, IDisposable [Parameter] public ValidationSummaryDisplayMode DisplayMode { get; set; } = ValidationSummaryDisplayMode.BulletList; - public bool IsValid => CurrentEditContext.GetValidationMessages().Any(); + [Parameter] public string HeaderText { get; set; } - public IEnumerable ValidationMessages => CurrentEditContext.GetValidationMessages().Select(x => x.Split(',')[1]); + [Parameter] public bool ShowSummary { get; set; } = true; + + [Parameter] public string ValidationGroup { get; set; } + + public bool IsValid => FilteredMessages.Any(); + + public IEnumerable ValidationMessages => FilteredMessages.Select(x => x.Split('\x1F')[0].Split(',')[1]); + + private IEnumerable FilteredMessages + { + get + { + var messages = CurrentEditContext.GetValidationMessages(); + if (!string.IsNullOrEmpty(ValidationGroup)) + { + messages = messages.Where(x => + { + var parts = x.Split('\x1F'); + return parts.Length > 1 && parts[1] == ValidationGroup; + }); + } + return messages; + } + } public AspNetValidationSummary() { diff --git a/src/BlazorWebFormsComponents/Validations/BaseValidator.razor b/src/BlazorWebFormsComponents/Validations/BaseValidator.razor index 397a4e3de..33a79cde5 100644 --- a/src/BlazorWebFormsComponents/Validations/BaseValidator.razor +++ b/src/BlazorWebFormsComponents/Validations/BaseValidator.razor @@ -1,9 +1,9 @@ @inherits BaseStyledComponent @typeparam Type -@if (Enabled && Visible && !IsValid) +@if (Enabled && Visible) { - + @if (string.IsNullOrWhiteSpace(Text)) { @ErrorMessage diff --git a/src/BlazorWebFormsComponents/Validations/BaseValidator.razor.cs b/src/BlazorWebFormsComponents/Validations/BaseValidator.razor.cs index bde38b2ec..15037ad60 100644 --- a/src/BlazorWebFormsComponents/Validations/BaseValidator.razor.cs +++ b/src/BlazorWebFormsComponents/Validations/BaseValidator.razor.cs @@ -6,6 +6,7 @@ using System.Linq.Expressions; using System.Reflection; using System.Threading.Tasks; +using Microsoft.JSInterop; namespace BlazorWebFormsComponents.Validations { @@ -25,9 +26,19 @@ public abstract partial class BaseValidator : BaseStyledComponent, IValida [Parameter] public string ValidationGroup { get; set; } [Parameter] public HorizontalAlign HorizontalAlign { get; set; } [Parameter] public VerticalAlign VerticalAlign { get; set; } + [Parameter] public ValidatorDisplay Display { get; set; } = ValidatorDisplay.Static; + [Parameter] public bool SetFocusOnError { get; set; } public abstract bool Validate(string value); + protected string DisplayStyle => Display switch + { + ValidatorDisplay.None => "display:none;", + ValidatorDisplay.Dynamic when IsValid => "display:none;", + ValidatorDisplay.Static when IsValid => "visibility:hidden;", + _ => "" + }; + protected StyleBuilder CalculatedStyle => this.ToStyle(); protected override void OnInitialized() @@ -100,7 +111,12 @@ private void EventHandler(object sender, ValidationRequestedEventArgs eventArgs) { IsValid = false; // Text is for validator, ErrorMessage is for validation summary - _messageStore.Add(fieldIdentifier, Text + "," + ErrorMessage); + _messageStore.Add(fieldIdentifier, Text + "," + ErrorMessage + "\x1F" + (ValidationGroup ?? "")); + + if (SetFocusOnError) + { + _ = JsRuntime.InvokeVoidAsync("bwfc.Validation.SetFocus", fieldIdentifier.FieldName); + } } CurrentEditContext.NotifyValidationStateChanged();