1. Introduction ✨
In the process of Winform development, it is often necessary to implement data display functionality. Previously, the GridControl control was always used. Today, I want to introduce how to use the Table component from Ant Design Blazor for data display in a Winform Blazor Hybrid project through an example.
2. Effect ✨
Let's first take a look at the implemented effect:


3. Specific Implementation ✨
For how to use Ant Design Blazor in a Winform Blazor Hybrid project, please refer to my previous article.
Introduce the Table component of Ant Design Blazor:
<table
TItem="IData"
DataSource="@datas"
OnRowClick="OnRowClick"
@ref="antTableRef"
>
<PropertyColumn Property="c=>c.StationName"> </PropertyColumn>
<PropertyColumn Property="c=>c.Weather"> </PropertyColumn>
<PropertyColumn Property="c=>c.Tem_Low"> </PropertyColumn>
<PropertyColumn Property="c=>c.Tem_High"> </PropertyColumn>
<PropertyColumn Property="c=>c.Wind"> </PropertyColumn>
<PropertyColumn Property="c=>c.Visibility_Low"> </PropertyColumn>
<PropertyColumn Property="c=>c.Visibility_High"> </PropertyColumn>
<PropertyColumn Property="c=>c.Fog"> </PropertyColumn>
<PropertyColumn Property="c=>c.Haze"> </PropertyColumn>
<PropertyColumn Property="c=>c.Date"> </PropertyColumn>
</table>
Where:
TItem represents the type of a single item in DataSource. Starting from version 0.16.0, Table supports regular classes, records, interfaces, and abstract classes as types for DataSource.
Here, my TItem is set to an interface called IData, defined as follows:
public interface IData
{
[DisplayName("Station Name")]
public string? StationName { get; set; }
[DisplayName("Weather")]
public string? Weather { get; set; }
[DisplayName("Min Temperature /℃")]
public string? Tem_Low { get; set; }
[DisplayName("Max Temperature /℃")]
public string? Tem_High { get; set; }
[DisplayName("Wind Direction & Force")]
public string? Wind { get; set; }
[DisplayName("Min Visibility /km")]
public string? Visibility_Low { get; set; }
[DisplayName("Max Visibility /km")]
public string? Visibility_High { get; set; }
[DisplayName("Fog")]
public string? Fog { get; set; }
[DisplayName("Haze")]
public string? Haze { get; set; }
[DisplayName("Date")]
public DateTime? Date { get; set; }
}
The [DisplayName("Station Name")] is a metadata annotation for an attribute or member, used to provide a more user-friendly display name. Ant Design Blazor automatically uses this to display the name.
DataSource represents the data source of the table, of type IEnumerable:

Here, DataSource="@datas" means that I assign a data source named datas to the DataSource property of the Table component.
The definition of datas is as follows:
WeatherData[] datas = Array.Empty<WeatherData>();
WeatherData is a custom class that implements the IData interface:
public class WeatherData : IData
{
[SugarColumn(IsPrimaryKey = true, IsIdentity = true)]
public int Id { get; set; }
public string? StationName { get; set; }
public string? Weather { get; set; }
public string? Tem_Low { get; set; }
public string? Tem_High { get; set; }
public string? Wind { get; set; }
public string? Visibility_Low { get; set; }
public string? Visibility_High { get; set; }
public string? Fog { get; set; }
public string? Haze { get; set; }
public DateTime? Date { get; set; }
}
}
At this point, you might wonder: just now TItem represented the type of a single item in DataSource, but now the DataSource is WeatherData[], so the type of a single item is WeatherData, not the IData set earlier. Is this acceptable?
Through the following simple example, you may understand:
public interface IFlyable
{
void Fly();
}
public class Bird : IFlyable
{
public void Fly()
{
Console.WriteLine("The bird is flying.");
}
}
class Program
{
// Main method
static void Main()
{
Bird myBird = new Bird();
IFlyable flyableObject = myBird; // Type conversion
// Call interface method
flyableObject.Fly();
}
}
An IFlyable interface and a Bird class are defined; the Bird class implements the IFlyable interface. In the Main function, an instance of Bird is created, and then the object is implicitly converted to the interface type. Through the interface, the implementation class's method is called, and the output is:
The bird is flying.
This shows that in C#, when a class implements an interface, instances of that class can be implicitly converted to the interface type. Here, WeatherData is implicitly converted to IData.
The same applies to DataSource. Although the official documentation states the type as IEnumerable, here we use WeatherData[] and it works because Array implements the IEnumerable interface, as shown below:

OnRowClick represents the row click event, of type EventCallback<RowData>. In this example, it is not actually used.
In Blazor, @ref is a directive used to reference HTML elements or component instances within a Blazor component. By using @ref, you can obtain a reference to a DOM element or child component in the Blazor component and then operate on it or access its properties and methods.
Here, I added @ref="antTableRef" to the Table component and added the following in the code area:
Table<IData>? antTableRef;
Thus successfully referencing the Table component instance.
<PropertyColumn> represents a property column, i.e., the column to display. Its Property attribute specifies the property to bind, of type Expression<Func<TItem, TProp>>.
You might wonder: what exactly is Expression<Func<TItem, TProp>>?
Expression<Func<TItem, TProp>> is an expression tree in C#, used to represent a lambda expression that takes a parameter of type TItem and returns a value of type TProp.
Breaking it down, Expression<T> is a class representing the tree structure of a lambda expression, where T is a delegate type. For detailed learning, refer to the official documentation:

Func<TItem, TProp> is a generic delegate type representing a method with one input parameter and one output parameter. You can also refer to the official documentation for detailed learning:

Here's a simple example to illustrate:
Expression<Func<Person, string>> getNameExpression = person => person.Name;
getNameExpression represents a Lambda expression. What kind of Lambda? One where the input parameter type is Person (corresponding to person) and the output type is string (corresponding to person.Name).
Therefore, the following code:
<PropertyColumn Property="c=>c.StationName"> </PropertyColumn>
is understandable: the type of Property is a Lambda expression c=>c.StationName where the input parameter type is TItem (which is IData here) and the output type is TProp (which is string here).
After understanding the above, let's look at this part of the code:

The code is as follows:
<GridRow>
<Space>
<SpaceItem>
<Text Strong>Start Date:</Text>
</SpaceItem>
<SpaceItem>
<DatePicker TValue="DateTime?" Format="yyyy/MM/dd" Mask="yyyy/dd/MM"
Placeholder="@("yyyy/dd/MM")" @bind-Value = "Date1"/>
</SpaceItem>
<SpaceItem>
<Text Strong>End Date:</Text>
</SpaceItem>
<SpaceItem>
<DatePicker TValue="DateTime?" Format="yyyy/MM/dd" Mask="yyyy/dd/MM"
Placeholder="@("yyyy/dd/MM")" @bind-Value = "Date2"/>
</SpaceItem>
<SpaceItem>
<Text Strong>Station Name:</Text>
</SpaceItem>
<SpaceItem>
<AutoComplete
@bind-Value="@value"
Options="@options"
OnSelectionChange="OnSelectionChange"
OnActiveChange="OnActiveChange"
Placeholder="input here"
Style="width:150px"
/>
</SpaceItem>
<SpaceItem>
<button type="@ButtonType.Primary" OnClick="QueryButton_Clicked">
Query
</button>
</SpaceItem>
</Space>
</GridRow>
Station name auto-complete:
<AutoComplete
@bind-Value="@value"
Options="@options"
OnSelectionChange="OnSelectionChange"
OnActiveChange="OnActiveChange"
Placeholder="input here"
Style="width:150px"
/>
The implementation of this was introduced in the previous article, so it won't be repeated here.
Both date picker components use data binding:
<SpaceItem>
<DatePicker TValue="DateTime?" Format="yyyy/MM/dd" Mask="yyyy/dd/MM"
Placeholder="@("yyyy/dd/MM")" @bind-Value = "Date1"/>
</SpaceItem>
<SpaceItem>
<DatePicker TValue="DateTime?" Format="yyyy/MM/dd" Mask="yyyy/dd/MM"
Placeholder="@("yyyy/dd/MM")" @bind-Value = "Date2"/>
</SpaceItem>
Where:
TValue represents the type of the value, set to DateTime?.
@bind-Value performs data binding, binding the value of the date picker component to Date1 and Date2:
DateTime? Date1;
DateTime? Date2;
Query button:
<button type="@ButtonType.Primary" OnClick="QueryButton_Clicked">Query</button>
Click event code:
async void QueryButton_Clicked()
{
if (Date1 != null && Date2 != null && value != null)
{
var cofig = new MessageConfig()
{
Content = "Updating...",
Duration = 0
};
var task = _message.Loading(cofig);
var condition = new Condition();
condition.StartDate = (DateTime)Date1;
condition.EndDate = (DateTime)Date2;
condition.StationName = value;
datas = weatherServer.GetDataByCondition(condition).ToArray();
StateHasChanged();
task.Start();
}
else
{
await _message.Error("Please check if the start date, end date, and station name are all selected!!!");
}
}
When the condition is met, a Condition object is created, containing the start date, end date, and station name. The Condition class is defined as follows:
public class Condition
{
public DateTime StartDate{ get; set; }
public DateTime EndDate { get; set; }
public string? StationName { get; set; }
}
Then the GetDataByCondition method in the business logic layer's weatherServer is called:
datas = weatherServer.GetDataByCondition(condition).ToArray();
The GetDataByCondition method in weatherServer is as follows:
public List<WeatherData> GetDataByCondition(Condition condition)
{
return dataService.GetDataByCondition(condition);
}
Since database read/write is involved, the GetDataByCondition method in the data access layer's dataService is called.
The GetDataByCondition method in the data access layer's dataService is as follows:
public List<WeatherData> GetDataByCondition(Condition condition)
{
return db.Queryable<WeatherData>()
.Where(x => x.Date >= condition.StartDate &&
x.Date < condition.EndDate.AddDays(1) &&
x.StationName == condition.StationName).ToList();
}
When re-querying:
StateHasChanged();
Calling this method causes the component to update. In Blazor, StateHasChanged is a method used to notify the Blazor framework to re-render the component and its child components. The UI rendering of a Blazor component is based on the component's state; when the state changes, StateHasChanged must be called to trigger a re-render.
var cofig = new MessageConfig()
{
Content = "Updating...",
Duration = 0
};
var task = _message.Loading(cofig);
task.Start();
This provides a user information prompt.
4. Summary ✨
Through a complete example, it has been demonstrated that in Winform, besides using GridControl for data display, you can also use the Table component from Ant Design Blazor.