Skip to content

Milestone 4: Chart Component (Chart.js + Blazor JS Interop)#4

Closed
csharpfritz wants to merge 38 commits into
devfrom
milestone4/chart-component
Closed

Milestone 4: Chart Component (Chart.js + Blazor JS Interop)#4
csharpfritz wants to merge 38 commits into
devfrom
milestone4/chart-component

Conversation

@csharpfritz

Copy link
Copy Markdown
Owner

Milestone 4: Chart Component

Summary

Implements the Web Forms Chart control as a Blazor component using Chart.js v4.x via JS interop. This is the project's first JavaScript interop component. Brings the component count from 50/53 (94%) to 51/53 (96%).

What's New

Chart Component Hierarchy

  • Chart.razor — Main component inheriting BaseStyledComponent. Renders <canvas> with JS interop to Chart.js.
  • ChartSeries.razor — Child component for data series with ChartType, XValueMember, YValueMembers parameters.
  • ChartArea.razor — Chart area configuration with AxisX, AxisY parameters.
  • ChartLegend.razor — Legend configuration with Docking, Enabled parameters.
  • ChartTitle.razor — Title configuration with Text, Docking parameters.
  • Axis.cs / DataPoint.cs — Supporting POCOs.
  • ChartConfigBuilder.cs — Pure static class mapping component state → Chart.js JSON config (highly testable).
  • ChartJsInterop.cs — C# IJSRuntime wrapper with lazy module loading.

JS Interop Infrastructure

  • chart-interop.js — ES module bridge: createChart, updateChart, destroyChart.
  • chart.min.js — Chart.js v4.4.8 placeholder stub (needs real bundle for production).

Supported Chart Types (Phase 1)

Column, Line, Bar, Pie, Area, Doughnut, Scatter, StackedColumn — covering 90%+ of real-world usage.

Enums

SeriesChartType (35 values for API fidelity), ChartPalette (12 palettes), Docking, ChartDashStyle.

HTML Output Exception

Web Forms Chart renders <img src=\"/ChartImg.axd?...\">. Blazor Chart renders <canvas>. Justified: server-side image generation is architecturally impossible in Blazor; migration value is in the API, not the HTML.

Testing

  • 866 bUnit tests (140 new Chart tests) — all passing
  • 124 integration tests (19 new Chart tests) — all passing

Bug Fixes (Boy Scout Rule)

  • Fixed ASP.NET structured log console error filter in integration tests (was causing 97 test failures)
  • Fixed external resource loading error filter (placeholder images)
  • Removed duplicate FileUpload route (Pages/ControlSamples/FileUpload/Default.razor)
  • Fixed ChangePassword and CreateUserWizard test assertions for EditForm interactive rendering

Documentation

  • docs/DataControls/Chart.md — Full component documentation with migration notes
  • docs/Migration/DeferredControls.md — Updated Chart status
  • Updated mkdocs.yml, README.md, status.md

Sample Pages

8 sample pages under ControlSamples/Chart/:
Index (Column), Line, Bar, Pie, Area, Doughnut, Scatter, StackedColumn

Design Decisions

  • Chart.js selected over D3 (wrong abstraction), ApexCharts (2x bundle), Plotly (3-4MB)
  • CascadingParameter "ParentChart" pattern for child registration (follows MultiView/View)
  • BaseStyledComponent base class (Web Forms Chart inherits WebControl)
  • IAsyncDisposable for JS interop cleanup

Copilot AI and others added 21 commits February 9, 2026 17:08
Co-authored-by: csharpfritz <78577+csharpfritz@users.noreply.github.com>
Co-authored-by: csharpfritz <78577+csharpfritz@users.noreply.github.com>
Co-authored-by: csharpfritz <78577+csharpfritz@users.noreply.github.com>
Co-authored-by: csharpfritz <78577+csharpfritz@users.noreply.github.com>
…ends#333)

- Create CalendarSelectionMode enum (None, Day, DayWeek, DayWeekMonth)
- Refactor Calendar.SelectionMode from string to CalendarSelectionMode enum
- Remove .GetAwaiter().GetResult() blocking call in CreateDayRenderArgs
- Add Caption, CaptionAlign, UseAccessibleHeader properties
- Update tests and samples to use enum values
- Calendar: date selection, selection modes, styling, day/title formats, events

- FileUpload: basic upload, file type filtering, multiple files, disabled, styled

- ImageMap: navigate/postback/mixed hot spot modes, rectangle/circle/polygon shapes

- Updated NavMenu and ComponentList with links to all three new components
…ents into dev

# Conflicts:
#	docs/EditorControls/FileUpload.md
…Friends#338 merge

The FileUpload PR (FritzAndFriends#338) inadvertently reverted Sprint 1 gate review
entries from agent histories (beast, cyclops, forge, jubilee, rogue)
and downgraded the FileUpload InputFile decision in decisions.md.

Restored from commit f85aa42 (docs(ai-team): Sprint 1 gate review results).
Creates .agent.md files for all 6 team agents (Beast, Cyclops, Forge,
Jubilee, Rogue, Scribe) so they appear in GitHub Copilot's agent picker.
Content sourced from existing .ai-team/agents/*/charter.md files.
Squad is the single Copilot agent that delegates to the specialized
agents defined in .ai-team/agents/. Individual agent files were
incorrectly created  the correct pattern is one coordinator agent
(squad.agent.md) that routes work to Forge, Cyclops, Beast, Jubilee,
Rogue, and Scribe based on task type.
Session: 2026-02-10-sprint2-complete
Requested by: Jeffrey T. Fritz

Changes:
- Logged Sprint 2 session (4 components shipped with docs, samples, tests)
- Merged Sprint 2 design review decision from inbox
- Removed duplicate FileUpload InputFile decision from inbox (already consolidated)
- Appended Sprint 2 completion decision to decisions.md
- Propagated cross-agent updates to all 5 agent histories
…ents into dev

# Conflicts:
#	docs/UtilityFeatures/PageService.md
#	samples/AfterBlazorServerSide/Components/Pages/ControlSamples/Calendar/Index.razor
Session: 2026-02-11-sprint3-planning
Requested by: Jeffrey T. Fritz

Changes:
- Logged session to .ai-team/log/2026-02-11-sprint3-planning.md
- Merged 3 decisions from inbox into decisions.md
- Updated status.md to reflect 48/53 components complete
- Sprint 3 scope: DetailsView + PasswordRecovery
- Propagated cross-agent updates to all agent history files
Session: 2026-02-12-sprint3-execution
Requested by: Jeffrey T. Fritz

Changes:
- Logged Sprint 3 execution session
- Merged 7 decisions from inbox into decisions.md
- Sprint 3 gate review: DetailsView + PasswordRecovery APPROVED
- Propagated cross-agent updates to Beast, Colossus, Cyclops, Rogue, Jubilee
- status.md updated to 50/53 (94%)
Session: 2026-02-12-milestone4-planning
Requested by: Jeffrey T. Fritz

Changes:
- Logged session to .ai-team/log/2026-02-12-milestone4-planning.md
- Merged decisions from inbox (Chart.js evaluation, milestone plan, milestones directive)
- Propagated milestone 4 updates to 5 agent history files
Session: 2026-02-12-milestone4-planning
Requested by: Scribe (automatic)

Changes:
- Summarized Forge history.md (exceeded ~12KB threshold)
- Preserved all team updates and key patterns
- Add Chart, ChartSeries, ChartArea, ChartLegend, ChartTitle components
- Add JS interop infrastructure (chart-interop.js, ChartJsInterop.cs)
- Add ChartConfigBuilder for component state -> Chart.js JSON config
- Add SeriesChartType, ChartPalette, Docking, ChartDashStyle enums
- Add DataPoint and Axis POCOs
- Add 140 bUnit tests for Chart components (866 total, all passing)
- Add 8 Chart sample pages (Column, Line, Bar, Pie, Area, Doughnut, Scatter, StackedColumn)
- Add 19 Chart integration tests
- Add Chart documentation and update mkdocs.yml, README, status.md
- Fix integration test console error filter for ASP.NET structured logs
- Fix integration test filter for external resource loading errors
- Fix duplicate FileUpload route (delete old Pages/ControlSamples/FileUpload/Default.razor)
- Fix ChangePassword and CreateUserWizard test assertions for EditForm rendering
- All tests green: 866 bUnit + 124 integration tests
Comment on lines +101 to +114
foreach (var s in seriesList)
{
if (s.Points != null && s.Points.Count > 0)
{
foreach (var p in s.Points)
{
if (!string.IsNullOrEmpty(p.Label))
labels.Add(p.Label);
else if (p.XValue != null)
labels.Add(p.XValue);
}
break;
}
}

Check notice

Code scanning / CodeQL

Missed opportunity to use Where Note

This foreach loop
implicitly filters its target sequence
- consider filtering the sequence explicitly using '.Where(...)'.

Copilot Autofix

AI 4 months ago

In general, to fix this kind of issue, we should replace a foreach loop that scans a sequence and conditionally processes only elements that satisfy a predicate with an explicit LINQ filter such as .Where(predicate) (and possibly .FirstOrDefault(), .Select(...), etc., as appropriate). This makes the code’s intent—“work with the first series that has points”—explicit and reduces nesting.

Here, the loop from lines 101–114 finds the first ChartSeriesConfig whose Points collection is non‑null and non‑empty, then builds labels from that series’ points and breaks. We can preserve this behaviour by:

  1. Selecting that series explicitly using LINQ:
    var firstSeriesWithPoints = seriesList
        .FirstOrDefault(s => s.Points != null && s.Points.Count > 0);
  2. If such a series exists, building the labels from firstSeriesWithPoints.Points in a simple foreach without extra conditionals or break.

Concretely in BuildDataSection within src/BlazorWebFormsComponents/ChartConfigBuilder.cs, replace the foreach (var s in seriesList) block that builds labels with:

  • A var firstSeriesWithPoints = ...FirstOrDefault(...) statement.
  • An if (firstSeriesWithPoints != null) block iterating over its Points and adding their labels/XValues to the labels list.

No new imports are needed: System.Linq is already imported, and FirstOrDefault is a standard LINQ method. The rest of the method, including the use of labels and datasets, remains unchanged.

Suggested changeset 1
src/BlazorWebFormsComponents/ChartConfigBuilder.cs

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/src/BlazorWebFormsComponents/ChartConfigBuilder.cs b/src/BlazorWebFormsComponents/ChartConfigBuilder.cs
--- a/src/BlazorWebFormsComponents/ChartConfigBuilder.cs
+++ b/src/BlazorWebFormsComponents/ChartConfigBuilder.cs
@@ -98,18 +98,21 @@
 
 		// Collect labels from the first series that has them
 		var labels = new List<object>();
-		foreach (var s in seriesList)
+		var firstSeriesWithPoints = seriesList
+			.FirstOrDefault(s => s.Points != null && s.Points.Count > 0);
+
+		if (firstSeriesWithPoints != null)
 		{
-			if (s.Points != null && s.Points.Count > 0)
+			foreach (var p in firstSeriesWithPoints.Points)
 			{
-				foreach (var p in s.Points)
+				if (!string.IsNullOrEmpty(p.Label))
 				{
-					if (!string.IsNullOrEmpty(p.Label))
-						labels.Add(p.Label);
-					else if (p.XValue != null)
-						labels.Add(p.XValue);
+					labels.Add(p.Label);
 				}
-				break;
+				else if (p.XValue != null)
+				{
+					labels.Add(p.XValue);
+				}
 			}
 		}
 
EOF
@@ -98,18 +98,21 @@

// Collect labels from the first series that has them
var labels = new List<object>();
foreach (var s in seriesList)
var firstSeriesWithPoints = seriesList
.FirstOrDefault(s => s.Points != null && s.Points.Count > 0);

if (firstSeriesWithPoints != null)
{
if (s.Points != null && s.Points.Count > 0)
foreach (var p in firstSeriesWithPoints.Points)
{
foreach (var p in s.Points)
if (!string.IsNullOrEmpty(p.Label))
{
if (!string.IsNullOrEmpty(p.Label))
labels.Add(p.Label);
else if (p.XValue != null)
labels.Add(p.XValue);
labels.Add(p.Label);
}
break;
else if (p.XValue != null)
{
labels.Add(p.XValue);
}
}
}

Copilot is powered by AI and may make mistakes. Always verify output.
Unable to commit as this autofix suggestion is now outdated
Comment on lines +135 to +148
if (s.ChartType == SeriesChartType.Point)
{
dataset["data"] = s.Points.Select(p => new Dictionary<string, object>
{
["x"] = p.XValue ?? 0,
["y"] = p.YValues?.Length > 0 ? p.YValues[0] : 0
}).ToList();
}
else
{
dataset["data"] = s.Points
.Select(p => p.YValues?.Length > 0 ? p.YValues[0] : 0.0)
.ToList();
}

Check notice

Code scanning / CodeQL

Missed ternary opportunity Note

Both branches of this 'if' statement write to the same variable - consider using '?' to express intent better.

Copilot Autofix

AI 4 months ago

In general, to fix this pattern you keep a single assignment and replace the if/else with a conditional (? :) that chooses between the two values that were previously assigned in each branch. This keeps the behavior identical while making it clearer that the only difference is which value is used.

For this specific case in src/BlazorWebFormsComponents/ChartConfigBuilder.cs, lines 135–148, we should replace:

  • The entire if (s.ChartType == SeriesChartType.Point) { ... } else { ... } block

with a single assignment:

  • dataset["data"] = s.ChartType == SeriesChartType.Point ? <point-expression> : <other-expression>;

The two existing expressions (s.Points.Select(...dictionary...) and s.Points.Select(...double...)) will become the true/false branches of the ternary. We must preserve the .ToList() calls and the existing dictionary and selection logic exactly, only restructuring into a conditional expression. No new imports or helper methods are needed; everything can be expressed inline where the if/else currently sits.

Suggested changeset 1
src/BlazorWebFormsComponents/ChartConfigBuilder.cs

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/src/BlazorWebFormsComponents/ChartConfigBuilder.cs b/src/BlazorWebFormsComponents/ChartConfigBuilder.cs
--- a/src/BlazorWebFormsComponents/ChartConfigBuilder.cs
+++ b/src/BlazorWebFormsComponents/ChartConfigBuilder.cs
@@ -132,20 +132,15 @@
 			// Data values
 			if (s.Points != null && s.Points.Count > 0)
 			{
-				if (s.ChartType == SeriesChartType.Point)
-				{
-					dataset["data"] = s.Points.Select(p => new Dictionary<string, object>
+				dataset["data"] = s.ChartType == SeriesChartType.Point
+					? s.Points.Select(p => new Dictionary<string, object>
 					{
 						["x"] = p.XValue ?? 0,
 						["y"] = p.YValues?.Length > 0 ? p.YValues[0] : 0
-					}).ToList();
-				}
-				else
-				{
-					dataset["data"] = s.Points
+					}).ToList()
+					: s.Points
 						.Select(p => p.YValues?.Length > 0 ? p.YValues[0] : 0.0)
 						.ToList();
-				}
 			}
 
 			// Color from series or palette
EOF
@@ -132,20 +132,15 @@
// Data values
if (s.Points != null && s.Points.Count > 0)
{
if (s.ChartType == SeriesChartType.Point)
{
dataset["data"] = s.Points.Select(p => new Dictionary<string, object>
dataset["data"] = s.ChartType == SeriesChartType.Point
? s.Points.Select(p => new Dictionary<string, object>
{
["x"] = p.XValue ?? 0,
["y"] = p.YValues?.Length > 0 ? p.YValues[0] : 0
}).ToList();
}
else
{
dataset["data"] = s.Points
}).ToList()
: s.Points
.Select(p => p.YValues?.Length > 0 ? p.YValues[0] : 0.0)
.ToList();
}
}

// Color from series or palette
Copilot is powered by AI and may make mistakes. Always verify output.
Unable to commit as this autofix suggestion is now outdated
if (area.AxisX != null)
{
var xAxis = BuildAxisConfig(area.AxisX);
if (scales.ContainsKey("x"))

Check notice

Code scanning / CodeQL

Inefficient use of ContainsKey Note

Inefficient use of 'ContainsKey' and
indexer
.

Copilot Autofix

AI 4 months ago

In general, to fix this issue you should replace patterns of if (dict.ContainsKey(key)) { var v = dict[key]; ... } with if (dict.TryGetValue(key, out var v)) { ... }, which combines the existence check and retrieval into a single dictionary operation.

For this specific code in src/BlazorWebFormsComponents/ChartConfigBuilder.cs, update both the "x" and "y" scale-handling blocks to use TryGetValue on the scales dictionary. Instead of checking scales.ContainsKey("x") and then accessing scales["x"], call scales.TryGetValue("x", out var existingObj), verify that existingObj is a Dictionary<string, object>, and then merge the axis configuration into that dictionary. If the key does not exist (or does not hold the expected type), fall back to assigning scales["x"] = xAxis as before. Do the same for the "y" axis. This preserves existing behavior, but ensures only one dictionary lookup in the common path. No new imports or types are required; you only need to introduce local variables for the out values and type checks.

Suggested changeset 1
src/BlazorWebFormsComponents/ChartConfigBuilder.cs

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/src/BlazorWebFormsComponents/ChartConfigBuilder.cs b/src/BlazorWebFormsComponents/ChartConfigBuilder.cs
--- a/src/BlazorWebFormsComponents/ChartConfigBuilder.cs
+++ b/src/BlazorWebFormsComponents/ChartConfigBuilder.cs
@@ -227,9 +227,8 @@
 			if (area.AxisX != null)
 			{
 				var xAxis = BuildAxisConfig(area.AxisX);
-				if (scales.ContainsKey("x"))
+				if (scales.TryGetValue("x", out var existingObj) && existingObj is Dictionary<string, object> existing)
 				{
-					var existing = (Dictionary<string, object>)scales["x"];
 					foreach (var kvp in xAxis)
 						existing[kvp.Key] = kvp.Value;
 				}
@@ -242,9 +240,8 @@
 			if (area.AxisY != null)
 			{
 				var yAxis = BuildAxisConfig(area.AxisY);
-				if (scales.ContainsKey("y"))
+				if (scales.TryGetValue("y", out var existingObj) && existingObj is Dictionary<string, object> existing)
 				{
-					var existing = (Dictionary<string, object>)scales["y"];
 					foreach (var kvp in yAxis)
 						existing[kvp.Key] = kvp.Value;
 				}
EOF
@@ -227,9 +227,8 @@
if (area.AxisX != null)
{
var xAxis = BuildAxisConfig(area.AxisX);
if (scales.ContainsKey("x"))
if (scales.TryGetValue("x", out var existingObj) && existingObj is Dictionary<string, object> existing)
{
var existing = (Dictionary<string, object>)scales["x"];
foreach (var kvp in xAxis)
existing[kvp.Key] = kvp.Value;
}
@@ -242,9 +240,8 @@
if (area.AxisY != null)
{
var yAxis = BuildAxisConfig(area.AxisY);
if (scales.ContainsKey("y"))
if (scales.TryGetValue("y", out var existingObj) && existingObj is Dictionary<string, object> existing)
{
var existing = (Dictionary<string, object>)scales["y"];
foreach (var kvp in yAxis)
existing[kvp.Key] = kvp.Value;
}
Copilot is powered by AI and may make mistakes. Always verify output.
Unable to commit as this autofix suggestion is now outdated
if (area.AxisY != null)
{
var yAxis = BuildAxisConfig(area.AxisY);
if (scales.ContainsKey("y"))

Check notice

Code scanning / CodeQL

Inefficient use of ContainsKey Note

Inefficient use of 'ContainsKey' and
indexer
.

Copilot Autofix

AI 4 months ago

In general, to fix this issue you replace the sequence “check with ContainsKey, then read with indexer” with a single call to TryGetValue, which both checks for existence and retrieves the value in one dictionary lookup. You then operate on the retrieved value if the call succeeds, or handle the absence of the key in the else branch.

In this file, there are two such patterns: one for the "x" key (lines 230–235) and one for the "y" key (lines 245–249). For each, we can change:

  • if (scales.ContainsKey("x")) { var existing = (Dictionary<string, object>)scales["x"]; ... }
  • if (scales.ContainsKey("y")) { var existing = (Dictionary<string, object>)scales["y"]; ... }

to use TryGetValue:

  • if (scales.TryGetValue("x", out var xScaleObj)) { var existing = (Dictionary<string, object>)xScaleObj; ... }
  • if (scales.TryGetValue("y", out var yScaleObj)) { var existing = (Dictionary<string, object>)yScaleObj; ... }

The cast to Dictionary<string, object> still happens (and will still throw if the stored value is of the wrong type), preserving existing behavior. No new methods, imports, or additional definitions are required; we only change the conditional and how we fetch the existing scale dictionary. All changes are localized to src/BlazorWebFormsComponents/ChartConfigBuilder.cs in the shown region.

Suggested changeset 1
src/BlazorWebFormsComponents/ChartConfigBuilder.cs

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/src/BlazorWebFormsComponents/ChartConfigBuilder.cs b/src/BlazorWebFormsComponents/ChartConfigBuilder.cs
--- a/src/BlazorWebFormsComponents/ChartConfigBuilder.cs
+++ b/src/BlazorWebFormsComponents/ChartConfigBuilder.cs
@@ -227,9 +227,9 @@
 			if (area.AxisX != null)
 			{
 				var xAxis = BuildAxisConfig(area.AxisX);
-				if (scales.ContainsKey("x"))
+				if (scales.TryGetValue("x", out var xScaleObj))
 				{
-					var existing = (Dictionary<string, object>)scales["x"];
+					var existing = (Dictionary<string, object>)xScaleObj;
 					foreach (var kvp in xAxis)
 						existing[kvp.Key] = kvp.Value;
 				}
@@ -242,9 +241,9 @@
 			if (area.AxisY != null)
 			{
 				var yAxis = BuildAxisConfig(area.AxisY);
-				if (scales.ContainsKey("y"))
+				if (scales.TryGetValue("y", out var yScaleObj))
 				{
-					var existing = (Dictionary<string, object>)scales["y"];
+					var existing = (Dictionary<string, object>)yScaleObj;
 					foreach (var kvp in yAxis)
 						existing[kvp.Key] = kvp.Value;
 				}
EOF
@@ -227,9 +227,9 @@
if (area.AxisX != null)
{
var xAxis = BuildAxisConfig(area.AxisX);
if (scales.ContainsKey("x"))
if (scales.TryGetValue("x", out var xScaleObj))
{
var existing = (Dictionary<string, object>)scales["x"];
var existing = (Dictionary<string, object>)xScaleObj;
foreach (var kvp in xAxis)
existing[kvp.Key] = kvp.Value;
}
@@ -242,9 +241,9 @@
if (area.AxisY != null)
{
var yAxis = BuildAxisConfig(area.AxisY);
if (scales.ContainsKey("y"))
if (scales.TryGetValue("y", out var yScaleObj))
{
var existing = (Dictionary<string, object>)scales["y"];
var existing = (Dictionary<string, object>)yScaleObj;
foreach (var kvp in yAxis)
existing[kvp.Key] = kvp.Value;
}
Copilot is powered by AI and may make mistakes. Always verify output.
Unable to commit as this autofix suggestion is now outdated
csharpfritz and others added 8 commits February 14, 2026 09:22
Session: 2026-02-14-chart-implementation
Requested by: Jeffrey T. Fritz

Changes:
- Logged session: Chart data binding and Playwright tests shipped
- Merged 7 decision files from inbox (Chart visual tests, data binding, implementation, samples)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Fix ChartSeries.ToConfig() to support Items/XValueMember/YValueMembers data binding
- Add 12 data binding unit tests (152 Chart tests total)
- Create 4 new sample pages: DataBinding, MultiSeries, Styling, ChartAreas
- Add 38 Playwright integration tests for Chart appearance verification
- All 143 integration tests and 152 Chart unit tests pass

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- ChartAreas, DataBinding, MultiSeries, Styling routes now tested
- All 42 Chart integration tests pass
- Completes Forge's gate review requirements

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…randing

- Upgraded Bootstrap 4.3.1 to 5.3.3
- Fixed pl-4 -> ps-4 utility class in NavMenu.razor
- Created ComponentCatalog.cs with 36 components across 6 categories
- Added GetByCategory, GetByRoute, Search helper methods
- Created logo.svg with migration concept (Web Forms -> Blazor)
- Added brand-colors.css with custom properties
- Updated App.razor with favicon and brand stylesheet

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Redesigned MainLayout.razor with modern Bootstrap 5 navbar
  - Fixed-top navbar with logo, search placeholder, external links
  - Collapsible sidebar with hamburger menu on mobile
  - Footer with GitHub link and MIT License
  - data-bs-theme attribute for future dark mode support
- Refactored NavMenu.razor to use ComponentCatalog
  - Data-driven rendering with collapsible category sections
  - Tooltips on component links with descriptions
  - Current page highlighting and auto-expand categories
  - Badge showing component count per category
- Created SamplePageTemplate.razor with card-based layout
  - Title, Description, DemoContent, CodeExample sections
  - Consistent styling using brand colors
- Fixed Menu_Renders_WithItems test (ul/li vs table)

All 147 integration tests pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Implemented client-side search with Fuse.js fuzzy matching
  - SearchBox.razor with debounced search (200ms)
  - Keyboard navigation (up/down/enter/escape)
  - Match highlighting in results
  - Category badges and truncated descriptions
- Redesigned homepage with hero section and card-based catalog
  - Gradient hero with CTA buttons (Get Started, View on GitHub)
  - 6 category cards with component counts and links
  - Responsive grid (3/2/1 columns for desktop/tablet/mobile)
- Fixed sidebar layout issues
  - Removed redundant header text
  - Categories expanded by default on desktop
  - Mobile nav collapse on link click

All 147 integration tests pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Added 4 screenshots showcasing the sample site:
- Homepage with component catalog
- GridView with interactive data binding
- Chart component with Chart.js
- Fuzzy search feature

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Fixed route mismatches between ComponentCatalog and actual page routes:
- MasterPage: /ControlSamples/MasterPage -> /control-samples/masterpage
- FormView: /ControlSamples/FormView -> /ControlSamples/FormView/Simple
- DataList: SimpleFlow subpage -> Flow (matches actual route)
- Validation controls: Removed /Validations/ prefix from routes
- Login controls: Removed /LoginControls/ prefix from routes
- LoginStatus: Removed NotAuthenticated subpage (has separate route)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The global footer in App.razor was spanning full width and appearing
behind the fixed-position sidebar. Added inline styles in head to
ensure the footer respects sidebar width with responsive handling.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Comment on lines +1328 to +1335
if (yValue != null)
{
point.YValues = new[] { Convert.ToDouble(yValue) };
}
else
{
point.YValues = Array.Empty<double>();
}

Check notice

Code scanning / CodeQL

Missed ternary opportunity Note test

Both branches of this 'if' statement write to the same variable - consider using '?' to express intent better.

Copilot Autofix

AI 4 months ago

In general, to fix this kind of issue you replace an if/else where both branches assign to the same variable with a single assignment whose right-hand side is a ternary expression. That keeps the logic equivalent but shorter and more expressive.

Here, we focus only on the block:

if (yValue != null)
{
	point.YValues = new[] { Convert.ToDouble(yValue) };
}
else
{
	point.YValues = Array.Empty<double>();
}

The best fix is to assign point.YValues once, using a ternary on yValue != null to pick between the two alternatives, while ensuring Convert.ToDouble(yValue) is still evaluated only when yValue is non-null. The resulting line:

point.YValues = yValue != null ? new[] { Convert.ToDouble(yValue) } : Array.Empty<double>();

keeps behavior identical: if yValue is non-null, it converts it to double and wraps it in a single-element array; otherwise, it sets YValues to an empty array. No new methods, imports, or definitions are required, and only the shown lines inside src/BlazorWebFormsComponents.Test/ChartTests.cs need to be changed.

Suggested changeset 1
src/BlazorWebFormsComponents.Test/ChartTests.cs

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/src/BlazorWebFormsComponents.Test/ChartTests.cs b/src/BlazorWebFormsComponents.Test/ChartTests.cs
--- a/src/BlazorWebFormsComponents.Test/ChartTests.cs
+++ b/src/BlazorWebFormsComponents.Test/ChartTests.cs
@@ -1325,14 +1325,9 @@
 			{
 				var yProp = item.GetType().GetProperty(yValueMembers);
 				var yValue = yProp?.GetValue(item);
-				if (yValue != null)
-				{
-					point.YValues = new[] { Convert.ToDouble(yValue) };
-				}
-				else
-				{
-					point.YValues = Array.Empty<double>();
-				}
+				point.YValues = yValue != null
+					? new[] { Convert.ToDouble(yValue) }
+					: Array.Empty<double>();
 			}
 			else
 			{
EOF
@@ -1325,14 +1325,9 @@
{
var yProp = item.GetType().GetProperty(yValueMembers);
var yValue = yProp?.GetValue(item);
if (yValue != null)
{
point.YValues = new[] { Convert.ToDouble(yValue) };
}
else
{
point.YValues = Array.Empty<double>();
}
point.YValues = yValue != null
? new[] { Convert.ToDouble(yValue) }
: Array.Empty<double>();
}
else
{
Copilot is powered by AI and may make mistakes. Always verify output.
Comment on lines +113 to +121
if (Items != null && !string.IsNullOrEmpty(YValueMembers))
{
config.Points = ExtractDataPointsFromItems();
}
else
{
// Fall back to manually-specified Points
config.Points = Points;
}

Check notice

Code scanning / CodeQL

Missed ternary opportunity Note

Both branches of this 'if' statement write to the same variable - consider using '?' to express intent better.

Copilot Autofix

AI 4 months ago

In general, to fix this issue you replace an if/else statement where both branches only assign or return a value with a single assignment or return that uses the ternary (? :) operator. This expresses the conditional choice more directly and concisely.

Here, the if/else sets config.Points to either ExtractDataPointsFromItems() or Points. We should replace the whole if/else block with one assignment:

config.Points = (Items != null && !string.IsNullOrEmpty(YValueMembers))
	? ExtractDataPointsFromItems()
	: Points;

This preserves the existing condition and behavior. To retain the intent of the “Fall back to manually-specified Points” comment, we can move that comment onto the ternary’s false branch or just above the assignment. No new methods, imports, or other definitions are required. The change is localized to the ToConfig method in src/BlazorWebFormsComponents/ChartSeries.razor.cs around lines 112–121.

Suggested changeset 1
src/BlazorWebFormsComponents/ChartSeries.razor.cs

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/src/BlazorWebFormsComponents/ChartSeries.razor.cs b/src/BlazorWebFormsComponents/ChartSeries.razor.cs
--- a/src/BlazorWebFormsComponents/ChartSeries.razor.cs
+++ b/src/BlazorWebFormsComponents/ChartSeries.razor.cs
@@ -109,16 +109,10 @@
 			ChartArea = ChartArea
 		};
 
-		// If Items is provided, extract data points from it
-		if (Items != null && !string.IsNullOrEmpty(YValueMembers))
-		{
-			config.Points = ExtractDataPointsFromItems();
-		}
-		else
-		{
-			// Fall back to manually-specified Points
-			config.Points = Points;
-		}
+		// If Items is provided, extract data points from it; otherwise fall back to manually-specified Points
+		config.Points = (Items != null && !string.IsNullOrEmpty(YValueMembers))
+			? ExtractDataPointsFromItems()
+			: Points;
 
 		return config;
 	}
EOF
@@ -109,16 +109,10 @@
ChartArea = ChartArea
};

// If Items is provided, extract data points from it
if (Items != null && !string.IsNullOrEmpty(YValueMembers))
{
config.Points = ExtractDataPointsFromItems();
}
else
{
// Fall back to manually-specified Points
config.Points = Points;
}
// If Items is provided, extract data points from it; otherwise fall back to manually-specified Points
config.Points = (Items != null && !string.IsNullOrEmpty(YValueMembers))
? ExtractDataPointsFromItems()
: Points;

return config;
}
Copilot is powered by AI and may make mistakes. Always verify output.
Comment on lines +233 to +236
catch
{
return false;
}

Check notice

Code scanning / CodeQL

Generic catch clause Note

Generic catch clause.

Copilot Autofix

AI 4 months ago

In general, to fix generic catch clauses, you should replace them with one or more catch blocks that name the specific exception types you intend to handle, and allow unexpected exceptions to propagate. This avoids accidentally hiding serious problems while still handling the failure modes you care about.

For this specific method, we only want to treat conversion failures as “return false.” According to .NET documentation, Convert.ToDouble(object) can throw InvalidCastException, FormatException, and OverflowException for non-null inputs that cannot be converted. We should explicitly catch these three and return false in each case. Any other exception (e.g., OutOfMemoryException) should not be swallowed and will bubble up, which is safer.

Concretely, in src/BlazorWebFormsComponents/ChartSeries.razor.cs, in the TryConvertToDouble method’s default case, replace the bare catch block starting at line 233 with three specific catch blocks: one each for InvalidCastException, FormatException, and OverflowException, all returning false. No new using directives are needed because these exception types are already in System, which is imported at the top. This preserves the current behavior for ordinary conversion problems while complying with the CodeQL recommendation.

Suggested changeset 1
src/BlazorWebFormsComponents/ChartSeries.razor.cs

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/src/BlazorWebFormsComponents/ChartSeries.razor.cs b/src/BlazorWebFormsComponents/ChartSeries.razor.cs
--- a/src/BlazorWebFormsComponents/ChartSeries.razor.cs
+++ b/src/BlazorWebFormsComponents/ChartSeries.razor.cs
@@ -230,10 +230,18 @@
 					result = Convert.ToDouble(value);
 					return true;
 				}
-				catch
+				catch (InvalidCastException)
 				{
 					return false;
 				}
+				catch (FormatException)
+				{
+					return false;
+				}
+				catch (OverflowException)
+				{
+					return false;
+				}
 		}
 	}
 }
EOF
@@ -230,10 +230,18 @@
result = Convert.ToDouble(value);
return true;
}
catch
catch (InvalidCastException)
{
return false;
}
catch (FormatException)
{
return false;
}
catch (OverflowException)
{
return false;
}
}
}
}
Copilot is powered by AI and may make mistakes. Always verify output.
csharpfritz and others added 8 commits February 23, 2026 09:25
…eenshots

- Fix per-segment color assignment for pie/doughnut charts in ChartConfigBuilder
- Replace chart.min.js placeholder with real Chart.js v4.4.8 (201KB)
- Add Playwright-generated screenshots for all 8 chart types to docs/images/chart/
- Add Chart Type Gallery section to Chart.md documentation
- Remove palette limitation warnings (bug is now fixed)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Evaluate 5 Blazor-native approaches to replicate Web Forms Themes and
Skins functionality: CSS Custom Properties, CascadingValue ThemeProvider,
Generated CSS Isolation, DI-based Configuration, and Hybrid.

Recommend CascadingValue ThemeProvider as the primary approach due to
full Web Forms fidelity (Theme/StyleSheetTheme semantics, SkinID support,
all property types). Document is exploratory per Jeff's request.

- Create docs/Migration/ThemesAndSkins.md with full analysis
- Add to mkdocs.yml nav under Migration section
- Record decision in .ai-team/decisions/inbox/
- Update forge history with learnings

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Session: 2026-02-23-feature-audit
Requested by: Jeffrey T. Fritz

Changes:
- Logged session to .ai-team/log/2026-02-23-feature-audit.md
- Merged 8 decisions from inbox into decisions.md
- Consolidated overlapping decisions (AccessKey/ToolTip, Chart architecture, DataBoundComponent gap)
- Propagated updates to 6 agent history files

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…trategy

- Created 53 audit documents in planning-docs/ comparing every ASP.NET Web Forms
  control against its Blazor component implementation
- Created planning-docs/SUMMARY.md with aggregate gap analysis (66.3% feature match)
- Created docs/Migration/ThemesAndSkins.md with 5 evaluated migration approaches
  (recommended: CascadingValue ThemeProvider pattern)
- Updated mkdocs.yml nav with ThemesAndSkins entry
- Key findings: AccessKey/ToolTip universally missing, GridView weakest at 20.7%,
  DetailsView+PasswordRecovery stranded on unmerged sprint3 branch,
  7 base-class fixes would close ~180 gaps across the library

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Brings in sprint3/detailsview-passwordrecovery (FritzAndFriends#340):
- DetailsView component with paging, edit mode, command rows
- PasswordRecovery component with 3-step flow
- DataBinder/ViewState utility samples
- Local SVG placeholder images replacing external URLs
- bUnit and Playwright integration tests

Conflict resolution:
- DeferredControls.md: Kept Chart as Partially Implemented (HEAD)
  with dev's richer Substitution/Xml documentation
- ControlSampleTests.cs: Combined Chart tests (HEAD) + Utility
  tests (dev), kept comprehensive error filtering
- InteractiveComponentTests.cs: Took dev's thorough field-level
  assertions + all new DetailsView/PasswordRecovery tests

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Integrates remote work: Phase 1-3 UI overhaul (Bootstrap 5,
ComponentCatalog, sidebar), chart data binding samples, and
Playwright integration tests for new sample pages.

Conflict: NavMenu.razor  took remote's ComponentCatalog-driven
dynamic nav (UI overhaul) over local's static TreeNode list.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The UI overhaul added ComponentCatalog-driven NavMenu and SearchBox
components to AfterBlazorServerSide. Since AfterBlazorClientSide links
Layout files via content references, it needs:

- ComponentCatalog.cs linked as a compile item (NavMenu depends on it)
- @using AfterBlazorServerSide added to client _Imports.razor
- @using AfterBlazorServerSide.Components.Shared moved from
  MainLayout.razor to server-side _Imports.razor (avoids namespace
  resolution failure in client project where Shared components are
  not linked)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@csharpfritz csharpfritz force-pushed the dev branch 2 times, most recently from 97eedc5 to 1219f33 Compare February 25, 2026 17:25
@csharpfritz csharpfritz closed this Mar 1, 2026
@csharpfritz csharpfritz deleted the milestone4/chart-component branch March 1, 2026 20:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants