Drawing in WinForms Blazor Hybrid
A few days ago, I introduced using Blazor Hybrid in WinForms, and also mentioned that with Blazor's UI, our WinForms programs can be designed more beautifully. Next, I want to illustrate this with an example of drawing in WinForms Blazor Hybrid, hoping it will be helpful to you.
Effect
Before we start, let me show you the effect, as shown below:


Specific Implementation
If you are interested in the specific implementation, you can continue reading.
1. Introduce Ant Design Blazor
All the components used in this application come from Ant Design Blazor.
In this article, I will only introduce the implementation of the drawing part. First, you need to introduce Ant Design Blazor into the project.
Install the NuGet package reference, as shown below:

If you need to draw, you also need to reference the AntDesign.Charts package.
Register the relevant services in the project's Form1.cs:
services.AddAntDesign();
As shown below:

Introduce static styles and script files:
<link href="_content/AntDesign/css/ant-design-blazor.css" rel="stylesheet" />
<script src="_content/AntDesign/js/ant-design-blazor.js"></script>
In the WinForms Blazor Hybrid project, introduce them in wwwroot/index.html, as shown below:

Here, I have also introduced AntDesign.Charts.
Add the namespace in _Imports.razor:
@using AntDesign
To dynamically display popup components, you need to add an <AntContainer /> component in App.razor.
According to the official documentation, in WinForms Blazor Hybrid, you can add it in the razor component used as the main page. Here, I added it in Index.razor as shown below:

Now you can use the components of Ant Design Blazor.
2. Page Design
The design of the drawing page is as follows:

First, choose a layout that you like. I chose this one from the official website, as shown below:

Modify the icon and name yourself. Now the first question in front of us is: how to implement page switching on click?
Each MenuItem has a Key property, as shown below:

Here, each Key is unique. Clicking different MenuItems will trigger a click event, and the click event uses a lambda expression to call the same method, but with different parameters.
Now let's look at this method:
int selectedMenuItem = 1;
private void NavigateToContent(int menuItemNumber)
{
selectedMenuItem = menuItemNumber;
}
It's very simple, just pass the parameter to selectedMenuItem.
Then in the content area, use a switch case:
<Content Class="site-layout-background" Style="margin: 24px 16px;padding: 24px;min-height: 450px;">
@switch(selectedMenuItem)
{
case 1:
<GetData></GetData>
break;
case 2:
<QueryData></QueryData>
break;
case 3:
<Painting></Painting>
break;
case 4:
<Export></Export>
break;
}
</Content>
Then different components can be displayed according to different selectedMenuItem values.
Now let's look at the design of the <Painting></Painting> component.
The page code of the <Painting></Painting> component is as follows:
<div>
<GridRow>
<GridCol Span="8">
<Space Direction="DirectionVHType.Vertical">
<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>
<Text Strong>Drawing Indicators:</Text>
</SpaceItem>
<SpaceItem>
<div>
<AntDesign.CheckboxGroup
Options="@ckeckAllOptions"
@bind-Value="selectedValues"
/>
</div>
</SpaceItem>
<SpaceItem>
<button type="@ButtonType.Primary" OnClick="Painting_Clicked">
Draw
</button>
</SpaceItem>
</Space>
</GridCol>
<GridCol Span="12">
<AntDesign.Charts.Line
Data="@Data1"
Config="Config1"
@ref="lineChartRef"
/>
</GridCol>
</GridRow>
</div>
3. Fill Station Names
When we open this component, different station names are already available, as shown below:

How is this achieved?
First, use the <AutoComplete> component, as shown below:
<AutoComplete
@bind-Value="@value"
Options="@options"
OnSelectionChange="OnSelectionChange"
OnActiveChange="OnActiveChange"
Placeholder="input here"
Style="width:150px"
/>
List<string> options = new List<string>();
protected override void OnInitialized()
{
options = weatherServer.GetDifferentStations();
}
In Blazor, OnInitialized is a lifecycle method used to execute some logic when the component is initialized. Specifically, the OnInitialized method is a virtual method defined in the Microsoft.AspNetCore.Components.ComponentBase class. You can override it in derived components to perform custom operations during component initialization.
Here, we use a three-layer architecture, consisting of the UI layer, business logic layer, and data access layer.
weatherServer is a custom service. To use this service, you need to add the statement at the beginning:
@inject IWeatherServer weatherServer;
In Blazor, @inject is a directive used to inject services into Razor pages or components. Through @inject, you can bring dependency injection services into Blazor pages or components to use these services within them.
Of course, to use the service, you must register it first:
services.AddSingleton<IWeatherServer,WeatherServer>();
services.AddSingleton<DataServer>();
Here, one is the service for the business logic layer, and the other is the service for data access.
IWeatherServer is the interface of the business logic layer. The benefits of using interfaces are as follows:
Implement Multiple Inheritance:
C# classes only support single inheritance, but a class can implement multiple interfaces. Interfaces provide a way for a class to acquire and implement functionality from different dimensions. A class can implement multiple interfaces, thereby having each set of members defined by the interface.
Implement Specifications:
Interfaces define a set of specifications that require implementing classes to provide specific members. This helps enforce that implementation classes follow certain programming conventions and standards, thereby improving code consistency and readability.
Provide Abstraction and Flexibility:
Interfaces themselves do not provide specific implementations, only define the contract of members. This makes interfaces a powerful abstraction tool, allowing you to describe the capabilities of a class without exposing the specific implementation.
Interfaces also provide a way to extend and modify the behavior of a class without changing the implementation of the class itself.
Implement Dependency Injection:
The combination of interfaces and dependency injection makes it easier to achieve replaceability and testability in applications. Through the dependency injection framework, you can inject different implementations at runtime, thereby achieving low coupling between modules.
Define Public Contracts:
Interfaces provide a way to define public contracts, allowing multiple implementations to work together in the system regardless of their specific types. This is very useful for plugin systems, extensibility, and modular design.
Allow Polymorphism:
Through interfaces, you can leverage the polymorphism mechanism in C#. When you reference an object's interface type, you can actually reference the derived type of the object at runtime, thus achieving polymorphic behavior.
Define Event Contracts: Interfaces can contain event declarations to define the event contract that a class should provide. This helps standardize the use and handling of events.
I use interfaces here mainly to clarify what functions the service actually implements, because the concrete implementation class will have a lot of code, making it hard to see clearly.
For example, the interfaces related to drawing are as follows:
public List<string> GetDifferentStations();
public List<WeatherData> GetDataByCondition(Condition condition);
Then implement them in the implementation class:
public List<string> GetDifferentStations()
{
return dataService.GetDifferentStations();
}
public List<WeatherData> GetDataByCondition(Condition condition)
{
return dataService.GetDataByCondition(condition);
}
The business logic layer does not directly interact with the database; it uses the data access service:
public List<string> GetDifferentStations()
{
return db.Queryable<WeatherData>().Select(x => x.StationName ?? "").Distinct().ToList();
}
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();
}
Here, the database uses SQLite, and the ORM uses SQLSugar. I won't go into detail about how to set it up here; you can check the official website or read previous articles.
4. Implementation of Drawing
The code is as follows:
async void Painting_Clicked()
{
if (Date1 != null && Date2 != null && value != null && selectedValues != null)
{
if(Data1?.Length > 0)
{
Data1 = new object[0];
}
if (plotDatas.Count > 0)
{
plotDatas.Clear();
}
var cofig = new MessageConfig()
{
Content = "Drawing...",
Duration = 0
};
var task = _message.Loading(cofig);
var condition = new Condition();
condition.StartDate = (DateTime)Date1;
condition.EndDate = (DateTime)Date2;
condition.StationName = value;
for(int i = 0;i < selectedValues.Length;i ++)
{
switch (selectedValues[i])
{
case "Tem_Low":
var result1 = weatherServer.GetDataByCondition(condition).Select(x => new PlotData
{
Date = x.Date,
Type = "Tem_Low",
Value = Convert.ToDouble(x.Tem_Low)
}).ToList();
plotDatas.AddRange(result1);
break;
case "Tem_High":
var result2 = weatherServer.GetDataByCondition(condition).Select(x => new PlotData
{
Date = x.Date,
Type = "Tem_High",
Value = Convert.ToDouble(x.Tem_High)
}).ToList();
plotDatas.AddRange(result2);
break;
case "Visibility_Low":
var result3 = weatherServer.GetDataByCondition(condition).Select(x => new PlotData
{
Date = x.Date,
Type = "Visibility_Low",
Value = Convert.ToDouble(x.Visibility_Low)
}).ToList();
plotDatas.AddRange(result3);
break;
case "Visibility_High":
var result4 = weatherServer.GetDataByCondition(condition).Select(x => new PlotData
{
Date = x.Date,
Type = "Visibility_High",
Value = Convert.ToDouble(x.Visibility_High)
}).ToList();
plotDatas.AddRange(result4);
break;
}
}
// Project the array of custom types into an array of object[]
Data1 = plotDatas.Select(p => new { date = p.Date, type = p.Type, value = p.Value }).ToArray();
// Update chart data
await lineChartRef.ChangeData(Data1);
task.Start();
}
else
{
await _message.Error("Please check if the start date, end date, station name, and drawing indicators are all selected!!!");
}
}
To draw multiple line charts in AntDesign.Charts, the official location is as follows:

Create a custom drawing data class:
public class PlotData
{
public DateTime? Date { get; set; }
public string? Type { get; set; }
public double Value { get; set; }
}
Then create a list of drawing data classes:
List<PlotData> plotDatas = new List<PlotData>();
Create a custom condition class:
public class Condition
{
public DateTime StartDate{ get; set; }
public DateTime EndDate { get; set; }
public string? StationName { get; set; }
}
Then when I click, if all items are not null, create a condition object:
var condition = new Condition();
condition.StartDate = (DateTime)Date1;
condition.EndDate = (DateTime)Date2;
condition.StationName = value;
This object contains the start time, end time, and station name we selected.
Then iterate through selectedValues:
for(int i = 0;i < selectedValues.Length;i ++)
selectedValues is of type string[]?.
string[]? selectedValues;
Indicates the selected values in the multi-select box.
static CheckboxOption[] ckeckAllOptions = new CheckboxOption[]{
new CheckboxOption{ Label="Minimum Temperature (℃)",Value="Tem_Low" },
new CheckboxOption{ Label="Maximum Temperature (℃)", Value="Tem_High" },
new CheckboxOption{ Label="Minimum Visibility (km)", Value="Visibility_Low"},
new CheckboxOption{ Label="Maximum Visibility (km)", Value="Visibility_High" },
};
Each selected Label has a corresponding value.
switch (selectedValues[i])
{
case "Tem_Low":
var result1 = weatherServer.GetDataByCondition(condition).Select(x => new PlotData
{
Date = x.Date,
Type = "Tem_Low",
Value = Convert.ToDouble(x.Tem_Low)
}).ToList();
plotDatas.AddRange(result1);
break;
case "Tem_High":
var result2 = weatherServer.GetDataByCondition(condition).Select(x => new PlotData
{
Date = x.Date,
Type = "Tem_High",
Value = Convert.ToDouble(x.Tem_High)
}).ToList();
plotDatas.AddRange(result2);
break;
case "Visibility_Low":
var result3 = weatherServer.GetDataByCondition(condition).Select(x => new PlotData
{
Date = x.Date,
Type = "Visibility_Low",
Value = Convert.ToDouble(x.Visibility_Low)
}).ToList();
plotDatas.AddRange(result3);
break;
case "Visibility_High":
var result4 = weatherServer.GetDataByCondition(condition).Select(x => new PlotData
{
Date = x.Date,
Type = "Visibility_High",
Value = Convert.ToDouble(x.Visibility_High)
}).ToList();
plotDatas.AddRange(result4);
break;
}
If the value is Tem_Low, then our drawing data is:
var result2 = weatherServer.GetDataByCondition(condition).Select(x => new PlotData
{
Date = x.Date,
Type = "Tem_High",
Value = Convert.ToDouble(x.Tem_High)
}).ToList();
Here, the implementation of weatherServer.GetDataByCondition(condition) is as follows:
public List<WeatherData> GetDataByCondition(Condition condition)
{
return dataService.GetDataByCondition(condition);
}
And the implementation of dataService.GetDataByCondition(condition) 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();
}
Finally, we obtain List<WeatherData> that meets the date and station name requirements, and then use the Select method to construct PlotData objects:
Select(x => new PlotData
{
Date = x.Date,
Type = "Tem_High",
Value = Convert.ToDouble(x.Tem_High)
}).ToList();
Then add them to plotDatas:
plotDatas.AddRange(result1);
After iterating through selectedValues, we get all the required drawing data, one item for each selected indicator. Then we need to map to an array of type object[]:
object[]? Data1;
// Project the array of custom types into an array of object[]
Data1 = plotDatas.Select(p => new { date = p.Date, type = p.Type, value = p.Value }).ToArray();
I am also puzzled about this. The AntLineChart and other components of Ant Design Charts Blazor usually use an array of type object[] as the data source for charts. This is because JavaScript itself is a weakly typed language, and Blazor communicates with JavaScript through JavaScript Interop. This is ChatGPT's explanation, and you can use it as a reference.
Then update the chart:
// Update chart data
await lineChartRef.ChangeData(Data1);
Drawing settings:
LineConfig Config1 = new LineConfig
{
Padding = "auto",
XField = "date",
YField = "value",
SeriesField = "type",
Smooth = true
};
Then you can achieve drawing.
Summary
This is my first attempt to write a small case using WinForms Blazor Hybrid. I have just started to understand Blazor Hybrid. There may be shortcomings, and I ask for your understanding. Finally, I hope it will be helpful to you.