Skip to content

Commit ac59e5c

Browse files
Copilotcsharpfritz
andauthored
Add CascadingTypeParameter for automatic ItemType inference in GridView/DataGrid columns (#320)
* Initial plan * Add CascadingTypeParameter to GridView and DataGrid for automatic ItemType inference Co-authored-by: csharpfritz <78577+csharpfritz@users.noreply.github.com> * Update sample pages to demonstrate optional ItemType on columns Co-authored-by: csharpfritz <78577+csharpfritz@users.noreply.github.com> * Update documentation to explain ItemType cascading feature Co-authored-by: csharpfritz <78577+csharpfritz@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: csharpfritz <78577+csharpfritz@users.noreply.github.com>
1 parent 51d6b3d commit ac59e5c

9 files changed

Lines changed: 173 additions & 20 deletions

File tree

docs/DataControls/DataGrid.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ The DataGrid component is meant to emulate the asp:DataGrid control in markup an
1515
- The `ItemCommand.CommandSource` object will be populated with the `ButtonField` object
1616
- DataGrid uses the same column types as GridView (BoundField, ButtonField, etc.)
1717
- DataGrid is a legacy control superseded by GridView in ASP.NET 2.0, but is provided for compatibility
18+
- **ItemType cascading** - The `ItemType` parameter is automatically cascaded from the DataGrid to child columns. You only need to specify it once on the DataGrid, 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.
1819

1920
## Web Forms Features NOT Supported
2021

docs/DataControls/GridView.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ The GridView component is meant to emulate the asp:GridView control in markup an
99

1010
- The `RowCommand.CommandSource` object will be populated with the `ButtonField` object
1111
- **Context attribute** - When using `<TemplateField>`, add `Context="Item"` to access the current row item as `@Item` instead of Blazor's default `@context`
12+
- **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.
1213

1314
## Web Forms Declarative Syntax
1415

@@ -370,10 +371,14 @@ Currently, not every syntax element of Web Forms GridView is supported. In the m
370371
Visible=bool
371372
>
372373
<Columns>
374+
<!-- Note: ItemType is optional on columns and will be automatically
375+
inferred from the parent GridView. You can still explicitly
376+
specify ItemType on individual columns for clarity if desired. -->
373377
<BoundField
374378
DataField=string
375379
DataFormatString=string
376380
HeaderText=string
381+
ItemType=Type (optional, inferred from parent)
377382
Visible=bool
378383
>
379384
</BoundField>
@@ -384,13 +389,15 @@ Currently, not every syntax element of Web Forms GridView is supported. In the m
384389
DataTextField="string"
385390
DataTextFormatString="string"
386391
HeaderText="string"
392+
ItemType=Type (optional, inferred from parent)
387393
NavigateUrl="uri"
388394
Target="string|_blank|_parent|_search|_self|_top"
389395
Text="string"
390396
Visible="True|False">
391397
</HyperLinkField>
392398
<TemplateField
393399
HeaderText=string
400+
ItemType=Type (optional, inferred from parent)
394401
Visible=bool
395402
>
396403
<ItemTemplate Context="Item">

samples/AfterBlazorServerSide/Components/Pages/ControlSamples/DataGrid/Default.razor

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,16 @@
1111
SelectMethod="Customer.GetCustomers"
1212
EmptyDataText="No data available">
1313
<Columns>
14-
<BoundField ItemType="Customer" DataField="CustomerID" HeaderText="ID" />
15-
<BoundField ItemType="Customer" DataField="CompanyName" HeaderText="CompanyName" />
16-
<BoundField ItemType="Customer" DataField="FirstName" HeaderText="FirstName"/>
17-
<BoundField ItemType="Customer" DataField="LastName" HeaderText="LastName"/>
18-
<TemplateField ItemType="Customer">
14+
<BoundField DataField="CustomerID" HeaderText="ID" />
15+
<BoundField DataField="CompanyName" HeaderText="CompanyName" />
16+
<BoundField DataField="FirstName" HeaderText="FirstName"/>
17+
<BoundField DataField="LastName" HeaderText="LastName"/>
18+
<TemplateField>
1919
<ItemTemplate Context="Item">
2020
<button type="button">Click Me! @Item.FirstName</button>
2121
</ItemTemplate>
2222
</TemplateField>
23-
<ButtonField ItemType="Customer" ButtonType="ButtonType.Button" DataTextField="CompanyName" DataTextFormatString="{0}" CommandName="Customer"></ButtonField>
23+
<ButtonField ButtonType="ButtonType.Button" DataTextField="CompanyName" DataTextFormatString="{0}" CommandName="Customer"></ButtonField>
2424
</Columns>
2525
</DataGrid>
2626

@@ -30,9 +30,10 @@
3030
<code>
3131
&lt;DataGrid ItemType="Customer" AutoGenerateColumns="false" DataKeyField="CustomerID"&gt;<br />
3232
&nbsp;&nbsp;&lt;Columns&gt;<br />
33-
&nbsp;&nbsp;&nbsp;&nbsp;&lt;BoundField ItemType="Customer" DataField="CustomerID" HeaderText="ID" /&gt;<br />
34-
&nbsp;&nbsp;&nbsp;&nbsp;&lt;BoundField ItemType="Customer" DataField="CompanyName" HeaderText="CompanyName" /&gt;<br />
35-
&nbsp;&nbsp;&nbsp;&nbsp;&lt;TemplateField ItemType="Customer"&gt;<br />
33+
&nbsp;&nbsp;&nbsp;&nbsp;&lt;!-- Note: ItemType is now optional on columns and inferred from parent DataGrid --&gt;<br />
34+
&nbsp;&nbsp;&nbsp;&nbsp;&lt;BoundField DataField="CustomerID" HeaderText="ID" /&gt;<br />
35+
&nbsp;&nbsp;&nbsp;&nbsp;&lt;BoundField DataField="CompanyName" HeaderText="CompanyName" /&gt;<br />
36+
&nbsp;&nbsp;&nbsp;&nbsp;&lt;TemplateField&gt;<br />
3637
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&lt;ItemTemplate Context="Item"&gt;...&lt;/ItemTemplate&gt;<br />
3738
&nbsp;&nbsp;&nbsp;&nbsp;&lt;/TemplateField&gt;<br />
3839
&nbsp;&nbsp;&lt;/Columns&gt;<br />

samples/AfterBlazorServerSide/Components/Pages/ControlSamples/GridView/Default.razor

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,16 @@
1111
SelectMethod="Customer.GetCustomers"
1212
EmptyDataText="No data available">
1313
<Columns>
14-
<BoundField ItemType="Customer" DataField="CustomerID" HeaderText="ID" />
15-
<BoundField ItemType="Customer" DataField="CompanyName" HeaderText="CompanyName" />
16-
<BoundField ItemType="Customer" DataField="FirstName" HeaderText="FirstName"/>
17-
<BoundField ItemType="Customer" DataField="LastName" HeaderText="LastName"/>
18-
<TemplateField ItemType="Customer">
14+
<BoundField DataField="CustomerID" HeaderText="ID" />
15+
<BoundField DataField="CompanyName" HeaderText="CompanyName" />
16+
<BoundField DataField="FirstName" HeaderText="FirstName"/>
17+
<BoundField DataField="LastName" HeaderText="LastName"/>
18+
<TemplateField>
1919
<ItemTemplate Context="Item">
2020
<button type="button">Click Me! @Item.FirstName</button>
2121
</ItemTemplate>
2222
</TemplateField>
23-
<ButtonField ItemType="Customer" ButtonType="ButtonType.Button" DataTextField="CompanyName" DataTextFormatString="{0}" CommandName="Customer"></ButtonField>
23+
<ButtonField ButtonType="ButtonType.Button" DataTextField="CompanyName" DataTextFormatString="{0}" CommandName="Customer"></ButtonField>
2424
</Columns>
2525
</GridView>
2626

@@ -30,9 +30,10 @@
3030
<code>
3131
&lt;GridView ItemType="Customer" AutoGenerateColumns="false" DataKeyNames="CustomerID"&gt;<br />
3232
&nbsp;&nbsp;&lt;Columns&gt;<br />
33-
&nbsp;&nbsp;&nbsp;&nbsp;&lt;BoundField ItemType="Customer" DataField="CustomerID" HeaderText="ID" /&gt;<br />
34-
&nbsp;&nbsp;&nbsp;&nbsp;&lt;BoundField ItemType="Customer" DataField="CompanyName" HeaderText="CompanyName" /&gt;<br />
35-
&nbsp;&nbsp;&nbsp;&nbsp;&lt;TemplateField ItemType="Customer"&gt;<br />
33+
&nbsp;&nbsp;&nbsp;&nbsp;&lt;!-- Note: ItemType is now optional on columns and inferred from parent GridView --&gt;<br />
34+
&nbsp;&nbsp;&nbsp;&nbsp;&lt;BoundField DataField="CustomerID" HeaderText="ID" /&gt;<br />
35+
&nbsp;&nbsp;&nbsp;&nbsp;&lt;BoundField DataField="CompanyName" HeaderText="CompanyName" /&gt;<br />
36+
&nbsp;&nbsp;&nbsp;&nbsp;&lt;TemplateField&gt;<br />
3637
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&lt;ItemTemplate Context="Item"&gt;...&lt;/ItemTemplate&gt;<br />
3738
&nbsp;&nbsp;&nbsp;&nbsp;&lt;/TemplateField&gt;<br />
3839
&nbsp;&nbsp;&lt;/Columns&gt;<br />

samples/AfterBlazorServerSide/Components/Pages/ControlSamples/GridView/TemplateFields.razor

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,12 @@
77
AutoGenerateColumns="false"
88
ItemType="SharedSampleObjects.Models.Widget">
99
<Columns>
10-
<TemplateField HeaderText="Name" ItemType="SharedSampleObjects.Models.Widget">
10+
<TemplateField HeaderText="Name">
1111
<ItemTemplate Context="Item">
1212
<Label ID="lblName" Text="@Item.Name"></Label>
1313
</ItemTemplate>
1414
</TemplateField>
15-
<TemplateField HeaderText="Price" ItemType="SharedSampleObjects.Models.Widget">
15+
<TemplateField HeaderText="Price">
1616
<ItemTemplate Context="Item">
1717
<Label ID="lblPrice" Text="@Item.Price.ToString()"></Label>
1818
</ItemTemplate>
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
@code {
2+
[Fact]
3+
public void DataGrid_CascadedItemType_ColumnsInferTypeFromParent()
4+
{
5+
// Test that columns can omit ItemType when parent DataGrid has it
6+
// This uses the CascadingTypeParameter feature
7+
var cut = Render(@<DataGrid ItemType="Widget"
8+
AutoGenerateColumns="false"
9+
SelectMethod="GetWidgets">
10+
<Columns>
11+
<BoundField DataField="Id" HeaderText="ID" />
12+
<BoundField DataField="Name" HeaderText="Widget Name" />
13+
</Columns>
14+
</DataGrid>);
15+
16+
// Check headers
17+
var tableHeaders = cut.FindAll("th");
18+
tableHeaders.Count.ShouldBe(2, "Wrong number of TH elements");
19+
tableHeaders[0].TextContent.ShouldBe("ID");
20+
tableHeaders[1].TextContent.ShouldBe("Widget Name");
21+
22+
// Check first data row to see that it bound stuff correctly
23+
var firstDataRow = cut.FindAll("tr")[1]; // Row #0 is the header, so row #1 is the first data row
24+
var firstDataRowCells = firstDataRow.ChildNodes.OfType<AngleSharp.Dom.IElement>().ToList();
25+
var idCellHtml = firstDataRowCells[0].InnerHtml.Trim();
26+
idCellHtml.ShouldBe("1");
27+
var nameCellHtml = firstDataRowCells[1].InnerHtml.Trim();
28+
nameCellHtml.ShouldBe("First Widget");
29+
}
30+
31+
[Fact]
32+
public void DataGrid_CascadedItemType_BackwardCompatibility()
33+
{
34+
// Test backward compatibility - explicit ItemType on columns should still work
35+
var cut = Render(@<DataGrid ItemType="Widget"
36+
AutoGenerateColumns="false"
37+
SelectMethod="GetWidgets">
38+
<Columns>
39+
<BoundField ItemType="Widget" DataField="Id" HeaderText="ID" />
40+
<BoundField ItemType="Widget" DataField="Name" HeaderText="Widget Name" />
41+
</Columns>
42+
</DataGrid>);
43+
44+
var tableHeaders = cut.FindAll("th");
45+
tableHeaders.Count.ShouldBe(2);
46+
tableHeaders[0].TextContent.ShouldBe("ID");
47+
tableHeaders[1].TextContent.ShouldBe("Widget Name");
48+
}
49+
50+
IQueryable<Widget> GetWidgets(int maxRows, int startRowIndex, string sortByExpression, out int totalRowCount)
51+
{
52+
totalRowCount = Widget.SimpleWidgetList.Length;
53+
return Widget.SimpleWidgetList.AsQueryable();
54+
}
55+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
@code {
2+
[Fact]
3+
public void GridView_CascadedItemType_ColumnsInferTypeFromParent()
4+
{
5+
// Test that columns can omit ItemType when parent GridView has it
6+
// This uses the CascadingTypeParameter feature
7+
var cut = Render(@<GridView ItemType="Widget"
8+
AutoGenerateColumns="false"
9+
SelectMethod="GetWidgets">
10+
<Columns>
11+
<BoundField DataField="Id" HeaderText="ID" />
12+
<BoundField DataField="Name" HeaderText="Widget Name" />
13+
<BoundField DataField="LastUpdate.Year" HeaderText="Last Update Year" />
14+
</Columns>
15+
</GridView>);
16+
17+
// Check headers
18+
var tableHeaders = cut.FindAll("th");
19+
tableHeaders.Count.ShouldBe(3, "Wrong number of TH elements");
20+
tableHeaders[0].TextContent.ShouldBe("ID");
21+
tableHeaders[1].TextContent.ShouldBe("Widget Name");
22+
tableHeaders[2].TextContent.ShouldBe("Last Update Year");
23+
24+
// Check first data row to see that it bound stuff correctly
25+
var firstDataRow = cut.FindAll("tr")[1]; // Row #0 is the header, so row #1 is the first data row
26+
var firstDataRowCells = firstDataRow.ChildNodes.OfType<AngleSharp.Dom.IElement>().ToList();
27+
var idCellHtml = firstDataRowCells[0].InnerHtml.Trim();
28+
idCellHtml.ShouldBe("1");
29+
var nameCellHtml = firstDataRowCells[1].InnerHtml.Trim();
30+
nameCellHtml.ShouldBe("First Widget");
31+
var updateYearCellHtml = firstDataRowCells[2].InnerHtml.Trim();
32+
updateYearCellHtml.ShouldBe(Widget.SimpleWidgetList.First().LastUpdate.Year.ToString());
33+
}
34+
35+
[Fact]
36+
public void GridView_CascadedItemType_MixedWithExplicitTypeStillWorks()
37+
{
38+
// Test backward compatibility - explicit ItemType on columns should still work
39+
var cut = Render(@<GridView ItemType="Widget"
40+
AutoGenerateColumns="false"
41+
SelectMethod="GetWidgets">
42+
<Columns>
43+
<BoundField ItemType="Widget" DataField="Id" HeaderText="ID" />
44+
<BoundField DataField="Name" HeaderText="Widget Name" />
45+
</Columns>
46+
</GridView>);
47+
48+
var tableHeaders = cut.FindAll("th");
49+
tableHeaders.Count.ShouldBe(2);
50+
tableHeaders[0].TextContent.ShouldBe("ID");
51+
tableHeaders[1].TextContent.ShouldBe("Widget Name");
52+
}
53+
54+
[Fact]
55+
public void GridView_CascadedItemType_TemplateFieldWorks()
56+
{
57+
// Test that TemplateField also benefits from cascaded type
58+
var cut = Render(@<GridView ItemType="Widget"
59+
AutoGenerateColumns="false"
60+
SelectMethod="GetWidgets">
61+
<Columns>
62+
<BoundField DataField="Id" HeaderText="ID" />
63+
<TemplateField HeaderText="Custom">
64+
<ItemTemplate Context="Item">
65+
<span>@Item.Name</span>
66+
</ItemTemplate>
67+
</TemplateField>
68+
</Columns>
69+
</GridView>);
70+
71+
var tableHeaders = cut.FindAll("th");
72+
tableHeaders.Count.ShouldBe(2);
73+
tableHeaders[1].TextContent.ShouldBe("Custom");
74+
75+
// Check that template rendered correctly
76+
var firstDataRow = cut.FindAll("tr")[1];
77+
var cells = firstDataRow.ChildNodes.OfType<AngleSharp.Dom.IElement>().ToList();
78+
cells[1].InnerHtml.Trim().ShouldContain("First Widget");
79+
}
80+
81+
IQueryable<Widget> GetWidgets(int maxRows, int startRowIndex, string sortByExpression, out int totalRowCount)
82+
{
83+
totalRowCount = Widget.SimpleWidgetList.Length;
84+
return Widget.SimpleWidgetList.AsQueryable();
85+
}
86+
}

src/BlazorWebFormsComponents/DataGrid.razor

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
@using BlazorWebFormsComponents.DataBinding
22
@using Interfaces
33
@typeparam ItemType
4+
@attribute [CascadingTypeParameter(nameof(ItemType))]
45
@inherits DataBoundComponent<ItemType>
56
@if (ColumnList.Any())
67
{

src/BlazorWebFormsComponents/GridView.razor

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
@using BlazorWebFormsComponents.DataBinding
22
@using Interfaces
33
@typeparam ItemType
4+
@attribute [CascadingTypeParameter(nameof(ItemType))]
45
@inherits DataBoundComponent<ItemType>
56
@if (ColumnList.Any())
67
{

0 commit comments

Comments
 (0)