Skip to content

Commit 1bdbb1f

Browse files
csharpfritzCopilot
andcommitted
Fix BWFC data control template emission for ListView, FormView, GridView
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 19b0fe7 commit 1bdbb1f

10 files changed

Lines changed: 291 additions & 7 deletions

File tree

.squad/agents/bishop/history.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,7 @@
3535
- Deprecate PowerShell scripts — CLI is canonical (2026-05-06T09:16)
3636
- Audit ALL BWFC Width/Height attributes for string support (2026-05-06T15:12)
3737
- NEVER replace ListView/FormView/GridView — must use BWFC components (2026-05-07T09:24)
38+
39+
## Learnings
40+
- **2026-05-07T13:17:32-04:00:** ListView `GroupTemplate` and `LayoutTemplate` emission is more reliable when the CLI emits explicit `Context` names (`items`, `groups`) instead of leaving raw `@context` placeholders. That keeps generated placeholder markup readable and removes one common manual repair step on Wingtip fixtures.
41+
- **2026-05-07T13:17:32-04:00:** Typed `GridView` columns must inherit the parent grid `ItemType`; leaving `TemplateField` at `ItemType="object"` breaks migrated template expressions like `@Item.Quantity`. A dedicated post-attribute-strip pass that rewrites child BWFC column generics to the grid row type fixes this deterministically.
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Bishop decision — fix BWFC data-control template emission
2+
3+
**Date:** 2026-05-07T13:17:32-04:00
4+
**By:** Bishop
5+
**Requested by:** Jeffrey T. Fritz
6+
7+
## Decision
8+
Add a dedicated CLI markup transform to propagate typed `GridView ItemType` values down into child BWFC column components, and extend template-context normalization so `ListView` group/layout placeholders emit explicit fragment contexts.
9+
10+
## What changed
11+
1. Added `GridViewColumnItemTypeTransform` at Order 705 after `AttributeStripTransform`.
12+
2. Rewrote typed `GridView` child `BoundField`, `TemplateField`, `HyperLinkField`, and `ButtonField` tags from `ItemType="object"` to the parent grid item type.
13+
3. Extended `TemplateContextTransform` so `GroupTemplate` emits `Context="items"` and `LayoutTemplate` emits `Context="groups"` when no explicit context exists.
14+
4. Added CLI tests covering Wingtip-style `ShoppingCart` and `ProductList` emission.
15+
5. Updated CLI docs to document the new transform behavior.
16+
17+
## Why
18+
Run 40 showed Layer 1 output still needed manual structural cleanup on flagship BWFC data controls. The worst failure mode was typed `GridView` templates compiling against `object`, which makes generated expressions like `@Item.Quantity` invalid. Explicit ListView placeholder contexts also make the generated template structure trustworthy and easier to inspect.
19+
20+
## Validation
21+
- `dotnet test tests\BlazorWebFormsComponents.Cli.Tests --nologo`
22+
- `dotnet run --project src\BlazorWebFormsComponents.Cli -- convert -i samples\WingtipToys\WingtipToys\ProductList.aspx -o .\bishop-output --overwrite`
23+
- `dotnet run --project src\BlazorWebFormsComponents.Cli -- convert -i samples\WingtipToys\WingtipToys\ProductDetails.aspx -o .\bishop-output --overwrite`
24+
- `dotnet run --project src\BlazorWebFormsComponents.Cli -- convert -i samples\WingtipToys\WingtipToys\ShoppingCart.aspx -o .\bishop-output --overwrite`
25+
26+
## Consequences
27+
Layer 1 now emits cleaner BWFC ListView placeholders and correctly typed GridView columns for Wingtip-style pages, reducing manual repair on the benchmark path. FormView item templates continue to emit valid typed fragments, and the CLI suite now guards this contract.

docs/cli/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ webforms-to-blazor convert \
117117
The tool applies an ordered transform pipeline and then a semantic pattern catalog:
118118

119119
1. **Directives** (5) — Page, Master, Control, Register, Import directives
120-
2. **Markup** (20) — Controls, expressions, master-page script cleanup, display-expression cleanup, templates, validator typing, data binding
120+
2. **Markup** (21) — Controls, expressions, master-page script cleanup, display-expression cleanup, templates, validator typing, typed GridView columns, data binding
121121
3. **Code-Behind** (26) — Using statements, HttpUtility/EF modernization, base classes, lifecycle, event handlers, compile-surface stubs, markup-driven safety stubs
122122

123123
See **[Transform Reference](transforms.md)** for the flat transform list and **[Semantic Pattern Catalog](semantic-pattern-catalog.md)** for the bounded semantic pass that runs afterward.

docs/cli/transforms.md

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,12 @@ This page documents the flat markup and code-behind transforms applied by the `w
2727
| 615 | ValidatorGenericTypeTransform | Markup | Markup | Add explicit `Type="string"` / `InputType="string"` defaults for generic BWFC validators |
2828
| 620 | TemplateFieldChildComponentsTransform | Markup | Markup | Wrap TemplateField style children in `<ChildComponents>` |
2929
| 700 | AttributeStripTransform | Markup | Markup | Remove `runat="server"`, preserve BWFC `ItemType`, add generic fallbacks |
30+
| 705 | GridViewColumnItemTypeTransform | Markup | Markup | Propagate typed `GridView ItemType` values to `BoundField` / `TemplateField` child columns |
3031
| 710 | EventWiringTransform | Markup | Markup | Convert `OnClick="X"``OnClick="@X"` |
3132
| 720 | UrlReferenceTransform | Markup | Markup | Convert `~/` paths to `/` |
3233
| 750 | ComponentRefMarkupTransform | Markup | Markup | Convert control IDs to `@ref`-compatible component references |
3334
| 800 | TemplatePlaceholderTransform | Markup | Markup | Replace template placeholder elements with the layout/group render fragment context |
34-
| 805 | TemplateContextTransform | Markup | Markup | Add `Context="Item"` to typed item templates and rewrite `@context``@Item` |
35+
| 805 | TemplateContextTransform | Markup | Markup | Add explicit item/group/layout template contexts for typed data-control fragments |
3536
| 810 | AttributeNormalizeTransform | Markup | Markup | Normalize attribute values (booleans, enums, units) |
3637
| 820 | DataSourceIdTransform | Markup | Markup | Replace DataSourceID with Items binding |
3738
| 30 | GetRouteUrlTransform | Code-Behind | Code-Behind | Flag `Page.GetRouteUrl()` calls |
@@ -462,6 +463,41 @@ Also:
462463

463464
---
464465

466+
### 15a. GridViewColumnItemTypeTransform (Order: 705)
467+
468+
**Propagates a typed `GridView` item type down into child BWFC column components.**
469+
470+
When a migrated `GridView` is strongly typed, `TemplateField`, `BoundField`, `HyperLinkField`, and `ButtonField` children must compile against that same row type. This transform rewrites `ItemType="object"` fallbacks on those column tags to the parent grid's concrete `ItemType`, while leaving object-typed grids alone.
471+
472+
**Example:**
473+
```html
474+
<!-- Before -->
475+
<GridView ItemType="CartItem" AutoGenerateColumns="false">
476+
<Columns>
477+
<BoundField ItemType="object" DataField="ProductID" />
478+
<TemplateField ItemType="object" HeaderText="Quantity">
479+
<ItemTemplate Context="Item">
480+
<TextBox Text="@Item.Quantity" />
481+
</ItemTemplate>
482+
</TemplateField>
483+
</Columns>
484+
</GridView>
485+
486+
<!-- After -->
487+
<GridView ItemType="CartItem" AutoGenerateColumns="false">
488+
<Columns>
489+
<BoundField ItemType="CartItem" DataField="ProductID" />
490+
<TemplateField ItemType="CartItem" HeaderText="Quantity">
491+
<ItemTemplate Context="Item">
492+
<TextBox Text="@Item.Quantity" />
493+
</ItemTemplate>
494+
</TemplateField>
495+
</Columns>
496+
</GridView>
497+
```
498+
499+
---
500+
465501
### 16. EventWiringTransform (Order: 710)
466502

467503
**Converts Web Forms event handler syntax to Blazor.**
@@ -539,21 +575,27 @@ This transform targets placeholder tags such as `itemPlaceholder`, `groupPlaceho
539575

540576
### 19. TemplateContextTransform (Order: 805)
541577

542-
**Adds `Context="Item"` to typed item templates and rewrites generated `@context` references to `@Item`.**
578+
**Adds explicit contexts for typed item templates plus named placeholder contexts for `ListView` group/layout fragments.**
543579

544-
This keeps BWFC data controls aligned with Web Forms naming so generated `<ListView>`, `<FormView>`, `<GridView>`, `<DataList>`, and `<Repeater>` templates stay on the component model instead of being flattened into manual HTML.
580+
This keeps BWFC data controls aligned with Web Forms naming so generated `<ListView>`, `<FormView>`, `<GridView>`, `<DataList>`, and `<Repeater>` templates stay on the component model instead of being flattened into manual HTML. It also upgrades placeholder-only `GroupTemplate` / `LayoutTemplate` blocks to named contexts such as `items` and `groups`, which makes emitted `ListView` markup structurally valid and easier to read.
545581

546582
**Example:**
547583
```html
548584
<!-- Before -->
549585
<ItemTemplate>
550586
<TextBox Text="@context.Name" />
551587
</ItemTemplate>
588+
<GroupTemplate>
589+
<tr>@context</tr>
590+
</GroupTemplate>
552591

553592
<!-- After -->
554593
<ItemTemplate Context="Item">
555594
<TextBox Text="@Item.Name" />
556595
</ItemTemplate>
596+
<GroupTemplate Context="items">
597+
<tr>@items</tr>
598+
</GroupTemplate>
557599
```
558600

559601
---

src/BlazorWebFormsComponents.Cli/Program.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ private static ServiceProvider BuildServiceProvider()
6060
services.AddSingleton<IMarkupTransform, ValidatorGenericTypeTransform>();
6161
services.AddSingleton<IMarkupTransform, TemplateFieldChildComponentsTransform>();
6262
services.AddSingleton<IMarkupTransform, AttributeStripTransform>();
63+
services.AddSingleton<IMarkupTransform, GridViewColumnItemTypeTransform>();
6364
services.AddSingleton<IMarkupTransform, EventWiringTransform>();
6465
services.AddSingleton<IMarkupTransform, ComponentRefMarkupTransform>();
6566
services.AddSingleton<IMarkupTransform, UrlReferenceTransform>();
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
using System.Text.RegularExpressions;
2+
using BlazorWebFormsComponents.Cli.Pipeline;
3+
4+
namespace BlazorWebFormsComponents.Cli.Transforms.Markup;
5+
6+
/// <summary>
7+
/// Propagates a typed GridView's ItemType to child column components so TemplateField and
8+
/// other generic BWFC columns compile against the row item type instead of falling back to object.
9+
/// Object-typed grids are left unchanged.
10+
/// </summary>
11+
public sealed class GridViewColumnItemTypeTransform : IMarkupTransform
12+
{
13+
public string Name => "GridViewColumnItemType";
14+
public int Order => 705;
15+
16+
private static readonly Regex GridViewRegex = new(
17+
@"(?<open><GridView\b[^>]*\bItemType=""(?<itemType>[^""]+)""[^>]*>)(?<inner>.*?)(?<close></GridView>)",
18+
RegexOptions.Compiled | RegexOptions.Singleline);
19+
20+
private static readonly Regex ColumnTagRegex = new(
21+
@"<(?<tag>BoundField|TemplateField|HyperLinkField|ButtonField)\b(?<attrs>[^>]*?)(?<close>\s*/?>)",
22+
RegexOptions.Compiled | RegexOptions.Singleline);
23+
24+
private static readonly Regex ItemTypeRegex = new(
25+
@"\bItemType=""(?<itemType>[^""]+)""",
26+
RegexOptions.Compiled);
27+
28+
public string Apply(string content, FileMetadata metadata)
29+
{
30+
var previous = string.Empty;
31+
do
32+
{
33+
previous = content;
34+
content = GridViewRegex.Replace(content, RewriteGridView);
35+
} while (!string.Equals(previous, content, StringComparison.Ordinal));
36+
37+
return content;
38+
}
39+
40+
private static string RewriteGridView(Match match)
41+
{
42+
var gridItemType = match.Groups["itemType"].Value;
43+
if (string.Equals(gridItemType, "object", StringComparison.Ordinal))
44+
{
45+
return match.Value;
46+
}
47+
48+
var rewrittenInner = ColumnTagRegex.Replace(match.Groups["inner"].Value, column => RewriteColumn(column, gridItemType));
49+
return match.Groups["open"].Value + rewrittenInner + match.Groups["close"].Value;
50+
}
51+
52+
private static string RewriteColumn(Match match, string gridItemType)
53+
{
54+
var attrs = match.Groups["attrs"].Value;
55+
var close = match.Groups["close"].Value;
56+
var itemTypeMatch = ItemTypeRegex.Match(attrs);
57+
58+
if (!itemTypeMatch.Success)
59+
{
60+
return $"<{match.Groups["tag"].Value} ItemType=\"{gridItemType}\"{attrs}{close}";
61+
}
62+
63+
if (!string.Equals(itemTypeMatch.Groups["itemType"].Value, "object", StringComparison.Ordinal))
64+
{
65+
return match.Value;
66+
}
67+
68+
var rewrittenAttrs = ItemTypeRegex.Replace(attrs, $"ItemType=\"{gridItemType}\"", 1);
69+
return $"<{match.Groups["tag"].Value}{rewrittenAttrs}{close}";
70+
}
71+
}

src/BlazorWebFormsComponents.Cli/Transforms/Markup/TemplateContextTransform.cs

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
namespace BlazorWebFormsComponents.Cli.Transforms.Markup;
55

66
/// <summary>
7-
/// Adds Context="Item" to typed item templates and rewrites generated @context references
8-
/// inside those templates to @Item so migrated BWFC data controls preserve Web Forms naming.
7+
/// Adds explicit template contexts for BWFC data-control fragments so generated markup stays
8+
/// structurally valid and readable for item, group, and layout templates.
99
/// </summary>
1010
public sealed class TemplateContextTransform : IMarkupTransform
1111
{
@@ -16,15 +16,30 @@ public sealed class TemplateContextTransform : IMarkupTransform
1616
@"<(ItemTemplate|AlternatingItemTemplate|EditItemTemplate|InsertItemTemplate|SelectedItemTemplate)(?![^>]*\bContext=)([^>]*)>(.*?)</\1>",
1717
RegexOptions.Compiled | RegexOptions.Singleline);
1818

19+
private static readonly Regex ListViewPlaceholderTemplateBlockRegex = new(
20+
@"<(GroupTemplate|LayoutTemplate)(?![^>]*\bContext=)([^>]*)>(.*?)</\1>",
21+
RegexOptions.Compiled | RegexOptions.Singleline);
22+
1923
public string Apply(string content, FileMetadata metadata)
2024
{
21-
return ItemTemplateBlockRegex.Replace(content, static match =>
25+
content = ItemTemplateBlockRegex.Replace(content, static match =>
2226
{
2327
var rewrittenInner = match.Groups[3].Value
2428
.Replace("@context", "@Item", StringComparison.Ordinal)
2529
.Replace("context.", "Item.", StringComparison.Ordinal);
2630

2731
return $"<{match.Groups[1].Value} Context=\"Item\"{match.Groups[2].Value}>{rewrittenInner}</{match.Groups[1].Value}>";
2832
});
33+
34+
return ListViewPlaceholderTemplateBlockRegex.Replace(content, static match =>
35+
{
36+
var contextName = string.Equals(match.Groups[1].Value, "GroupTemplate", StringComparison.Ordinal)
37+
? "items"
38+
: "groups";
39+
var rewrittenInner = match.Groups[3].Value
40+
.Replace("@context", $"@{contextName}", StringComparison.Ordinal);
41+
42+
return $"<{match.Groups[1].Value} Context=\"{contextName}\"{match.Groups[2].Value}>{rewrittenInner}</{match.Groups[1].Value}>";
43+
});
2944
}
3045
}

tests/BlazorWebFormsComponents.Cli.Tests/TestHelpers.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,7 @@ public static MigrationPipeline CreateDefaultPipeline()
209209
new TemplateFieldChildComponentsTransform(),
210210
// Order 700-750: Attributes & refs
211211
new AttributeStripTransform(),
212+
new GridViewColumnItemTypeTransform(),
212213
new EventWiringTransform(),
213214
new ComponentRefMarkupTransform(),
214215
new UrlReferenceTransform(),
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
using BlazorWebFormsComponents.Cli.Pipeline;
2+
using BlazorWebFormsComponents.Cli.Transforms.Markup;
3+
4+
namespace BlazorWebFormsComponents.Cli.Tests.TransformUnit;
5+
6+
public class GridViewColumnItemTypeTransformTests
7+
{
8+
private readonly GridViewColumnItemTypeTransform _transform = new();
9+
10+
private static FileMetadata TestMetadata => new()
11+
{
12+
SourceFilePath = "test.aspx",
13+
OutputFilePath = "test.razor",
14+
FileType = FileType.Page,
15+
OriginalContent = string.Empty
16+
};
17+
18+
[Fact]
19+
public void UsesParentGridViewItemTypeForObjectColumns()
20+
{
21+
var input = """
22+
<GridView ItemType="CartItem" AutoGenerateColumns="false">
23+
<Columns>
24+
<BoundField ItemType="object" DataField="ProductID" />
25+
<TemplateField ItemType="object" HeaderText="Quantity">
26+
<ItemTemplate Context="Item">
27+
<TextBox Text="@Item.Quantity" />
28+
</ItemTemplate>
29+
</TemplateField>
30+
</Columns>
31+
</GridView>
32+
""";
33+
34+
var result = _transform.Apply(input, TestMetadata);
35+
36+
Assert.Contains("<BoundField ItemType=\"CartItem\" DataField=\"ProductID\" />", result);
37+
Assert.Contains("<TemplateField ItemType=\"CartItem\" HeaderText=\"Quantity\">", result);
38+
Assert.DoesNotContain("ItemType=\"object\"", result);
39+
Assert.Contains("</GridView>", result);
40+
}
41+
42+
[Fact]
43+
public void LeavesObjectTypedGridViewColumnsUntouched()
44+
{
45+
var input = """
46+
<GridView ItemType="object" AutoGenerateColumns="false">
47+
<Columns>
48+
<BoundField ItemType="object" DataField="ProductName" />
49+
</Columns>
50+
</GridView>
51+
""";
52+
53+
var result = _transform.Apply(input, TestMetadata);
54+
55+
Assert.Equal(input, result);
56+
}
57+
58+
[Fact]
59+
public void PipelineRewritesWingtipShoppingCartColumnsToTypedFields()
60+
{
61+
var pipeline = TestHelpers.CreateDefaultPipeline();
62+
var metadata = TestMetadata;
63+
64+
var result = pipeline.TransformMarkup("""
65+
<asp:GridView ID="CartList" runat="server" AutoGenerateColumns="False" ItemType="WingtipToys.Models.CartItem">
66+
<Columns>
67+
<asp:BoundField DataField="ProductID" HeaderText="ID" />
68+
<asp:TemplateField HeaderText="Quantity">
69+
<ItemTemplate>
70+
<asp:TextBox ID="PurchaseQuantity" runat="server" Text="<%#: Item.Quantity %>"></asp:TextBox>
71+
</ItemTemplate>
72+
</asp:TemplateField>
73+
</Columns>
74+
</asp:GridView>
75+
""", metadata);
76+
77+
Assert.Contains("<GridView id=\"CartList\" AutoGenerateColumns=\"false\" ItemType=\"CartItem\">", result);
78+
Assert.Contains("<BoundField ItemType=\"CartItem\" DataField=\"ProductID\" HeaderText=\"ID\" />", result);
79+
Assert.Contains("<TemplateField ItemType=\"CartItem\" HeaderText=\"Quantity\">", result);
80+
Assert.Contains("<ItemTemplate Context=\"Item\">", result);
81+
Assert.Contains("Text=\"@Item.Quantity\"", result);
82+
Assert.DoesNotContain("ItemType=\"object\"", result);
83+
Assert.Equal(1, CountOccurrences(result, "<Columns>"));
84+
Assert.Contains("</GridView>", result);
85+
}
86+
87+
private static int CountOccurrences(string source, string target)
88+
{
89+
var count = 0;
90+
var index = 0;
91+
while ((index = source.IndexOf(target, index, StringComparison.Ordinal)) >= 0)
92+
{
93+
count++;
94+
index += target.Length;
95+
}
96+
97+
return count;
98+
}
99+
}

tests/BlazorWebFormsComponents.Cli.Tests/TransformUnit/TemplateContextTransformTests.cs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,4 +52,28 @@ public void PipelineAddsContextForGridTemplateField()
5252
Assert.Contains("<TemplateField ItemType=\"object\">", result);
5353
Assert.Contains("<ItemTemplate Context=\"Item\">@Item.Name</ItemTemplate>", result);
5454
}
55+
56+
[Fact]
57+
public void PipelineAddsNamedContextForGroupTemplatePlaceholder()
58+
{
59+
var pipeline = TestHelpers.CreateDefaultPipeline();
60+
var metadata = TestMetadata;
61+
62+
var result = pipeline.TransformMarkup("<asp:ListView ItemType=\"Widget\"><GroupTemplate><tr id=\"itemPlaceholder\" runat=\"server\"></tr></GroupTemplate></asp:ListView>", metadata);
63+
64+
Assert.Contains("<GroupTemplate Context=\"items\">", result);
65+
Assert.Contains("@items", result);
66+
}
67+
68+
[Fact]
69+
public void PipelineAddsNamedContextForLayoutTemplatePlaceholder()
70+
{
71+
var pipeline = TestHelpers.CreateDefaultPipeline();
72+
var metadata = TestMetadata;
73+
74+
var result = pipeline.TransformMarkup("<asp:ListView ItemType=\"Widget\"><LayoutTemplate><div id=\"groupPlaceholder\" runat=\"server\"></div></LayoutTemplate></asp:ListView>", metadata);
75+
76+
Assert.Contains("<LayoutTemplate Context=\"groups\">", result);
77+
Assert.Contains("@groups", result);
78+
}
5579
}

0 commit comments

Comments
 (0)