Drawing in winform blazor hybrid
A few days ago, I introduced to you the use of blazor hybrid in winform, and I also said that with blazor's ui, our winform program design can be more beautiful. Next, I want to use an example of drawing in winform blazor hybrid to illustrate it, hoping to be helpful to you.
effect
Before starting, let's show you the effect, as follows:


specific implementation
If you are interested in the specific implementation, you can continue reading.
1. Introduce ant design blazor
All components used in this application come from ant design blazor.
In this article, I only introduce the implementation of the drawing part. First, we need to introduce ant design blazor into the project.
Install the NuGet package reference as follows:

If you need to draw, you also need to quote the AntDesign.Charts package.
在项目的 Form1.cs 中注册相关服务:
services.AddAntDesign();
As follows:

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>
The winform blazor hybrid project is introduced at wwwroot/index.html as follows:

Here I also introduced AntDesign.Charts.
在 _Imports.razor 中加入命名空间:
@using AntDesign
为了动态地显示弹出组件,需要在 App.razor 中添加一个 <AntContainer /> 组件。
This is what the official website says. In the winform blazor hybrid, you can add it to the razor as the homepage. Here is Index.razor as follows:

You can now use the components of ant design blazor.
2. Page design
The design of the drawing page is as follows:

The first step is to choose a layout you like. I chose this one on the official website, as follows:

If you modify the icon and name yourself, the first question in front of you now is how to click to switch pages?
Each MenuItem has a Key attribute, as follows:

Every Key here is unique. Clicking on different MenuItems triggers a click event, and the click event uses a lambda expression to call the same method, but with different parameters.
Now take a look at this method:
int selectedMenuItem = 1;
private void NavigateToContent(int menuItemNumber)
{
selectedMenuItem = menuItemNumber;
}
It's simple, just pass the parameters to selectedMenuItem.
Then in the content area, use the 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>
You can then display different components based on different selectedMenuItem values.
现在来看看<Painting></Painting>组件的设计。
<Painting></Painting>组件的页面代码如下:
<div>
<GridRow>
<GridCol Span="8">
<Space Direction="DirectionVHType.Vertical">
<SpaceItem>
<Text Strong>开始日期:</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>结束日期:</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>站名:</Text>
</SpaceItem>
<SpaceItem>
<AutoComplete
@bind-Value="@value"
Options="@options"
OnSelectionChange="OnSelectionChange"
OnActiveChange="OnActiveChange"
Placeholder="input here"
Style="width:150px"
/>
</SpaceItem>
<SpaceItem>
<Text Strong>绘图指标:</Text>
</SpaceItem>
<SpaceItem>
<div>
<AntDesign.CheckboxGroup
Options="@ckeckAllOptions"
@bind-Value="selectedValues"
/>
</div>
</SpaceItem>
<SpaceItem>
<button type="@ButtonType.Primary" OnClick="Painting_Clicked">
绘图
</button>
</SpaceItem>
</Space>
</GridCol>
<GridCol Span="12">
<AntDesign.Charts.Line
Data="@Data1"
Config="Config1"
@ref="lineChartRef"
/>
</GridCol>
</GridRow>
</div>
3. Fill in the station name
When we opened this component, there were different station names, as follows:

How is this achieved?
首先使用<AutoComplete>自动完成这个组件,如下所示:
<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();
}
在 Blazor 中,OnInitialized 是一个生命周期方法,用于在组件初始化时执行一些逻辑。具体而言,OnInitialized 方法是 Microsoft.AspNetCore.Components.ComponentBase 类中定义的一个虚拟方法,你可以在派生的组件中覆盖它,以在组件初始化的时候执行一些自定义的操作。
A three-layer architecture is adopted here, which is divided into ui layer, business logic layer, and database access layer.
其中的weatherServer是我自定义的服务,使用这个服务,需要在开头添加语句:
@inject IWeatherServer weatherServer;
在 Blazor 中,@inject 是用于在 Razor 页面或组件中注入服务的指令。通过 @inject,你可以将依赖注入服务引入到 Blazor 页面或组件中,以便在其中使用这些服务。
Of course, to use the service, you must first register the service:
services.AddSingleton<IWeatherServer,WeatherServer>();
services.AddSingleton<DataServer>();
Here, one is a service for business logic and the other is a service for data access.
其中IWeatherServer是业务逻辑层的接口,使用接口的好处,大家可以参考一下:
** Achieve multiple inheritance: **
Classes in C#only support single inheritance, but one class can implement multiple interfaces. Interfaces provide a way to allow a class to acquire and implement functionality in different dimensions. A class can implement multiple interfaces, thus having a set of members defined for each interface.
** Implementation specifications: **
Interfaces define a set of specifications that require implementation classes to provide specific members. This helps enforce implementation classes to follow certain programming specifications and standards, thereby improving code consistency and readability.
** Provides abstraction and flexibility: **
The interface itself does not provide a specific implementation, but only defines the contracts of the members. This makes interfaces a powerful abstraction tool that allows you to describe the capabilities of classes without exposing specific implementation.
Interfaces also provide a way to extend and modify class behavior without changing the implementation of the class itself.
** Implement dependency injection: **
The combination of interfaces and dependency injection makes it easier to implement substitutability and testability in applications. With a dependency injection framework, you can inject different implementations at runtime, achieving low coupling between modules.
** Define public contract: **
Interfaces provide a way to define common contracts so that multiple implementations can work together in the system, regardless of their specific type. This is very useful for plug-in systems, extensibility and modular design.
** Polymorphism allowed: **
Through the interface, you can take advantage of the polymorphism mechanism in C#. When you reference an object's interface type, you can actually reference the object's derived type at run time, thereby achieving polymorphic behavior.
** Define event contracts: ** The interface can contain event declarations to define the event contracts that the class should provide. This helps standardize the use and handling of events.
I use interfaces here mainly to clarify what functions the service implements, because there will be a lot of code in the specific implementation class, which is difficult to read clearly.
For example, the interfaces related to drawing are as follows:
public List<string> GetDifferentStations();
public List<WeatherData> GetDataByCondition(Condition condition);
Then implement it 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 interact directly with the database, but uses database access services:
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();
}
The database here uses SQLite, and the ORM uses SQLSugar. I won't elaborate on how to set it up here. You can check the official website or historical articles.
4. Realization 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 = "正在画图中...",
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;
}
}
// 将自定义类型的数组投影为 object[] 类型的数组
Data1 = plotDatas.Select(p => new { date = p.Date, type = p.Type, value = p.Value }).ToArray();
// 更新图表数据
await lineChartRef.ChangeData(Data1);
task.Start();
}
else
{
await _message.Error("请查看开始日期、结束日期、站名与绘图指标是否都已选择!!!");
}
}
Draw multiple line charts in AntDesign.Charts, and the official website locations are 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 the items are not empty, create a conditional 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 string[]? Type.
string[]? selectedValues;
Represents the value selected in the multi-selection box.
static CheckboxOption[] ckeckAllOptions = new CheckboxOption[]{
new CheckboxOption{ Label="最低温度(℃)",Value="Tem_Low" },
new CheckboxOption{ Label="最高温度(℃)", Value="Tem_High" },
new CheckboxOption{ Label="最低可见度(km)", Value="Visibility_Low"},
new CheckboxOption{ Label="最高可见度(km)", Value="Visibility_High" },
};
The 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;
}
如果值为Tem_Low,那么我们的画图数据就是:
var result2 = weatherServer.GetDataByCondition(condition).Select(x => new PlotData
{
Date = x.Date,
Type = "Tem_High",
Value = Convert.ToDouble(x.Tem_High)
}).ToList();
Here, the first implementation of weatherServer.GetDataByCondition(condition) is as follows:
public List<WeatherData> GetDataByCondition(Condition condition)
{
return dataService.GetDataByCondition(condition);
}
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();
}
最终获得了满足日期与站名要求的List<WeatherData>,然后再使用 Select 方法构造 PlotData 对象:
Select(x => new PlotData
{
Date = x.Date,
Type = "Tem_High",
Value = Convert.ToDouble(x.Tem_High)
}).ToList();
Then add it to plotDatas:
plotDatas.AddRange(result1);
In this way, after traversing selectedValues, we get all the drawing data we need. There are a few items selected, and then we need to map them to an array of type object[]:
object[]? Data1;
// 将自定义类型的数组投影为 object[] 类型的数组
Data1 = plotDatas.Select(p => new { date = p.Date, type = p.Type, value = p.Value }).ToArray();
这里我也很迷惑,Ant Design Charts Blazor 的 AntLineChart 等组件通常使用 object[] 类型的数组作为图表的数据源。这是因为 JavaScript 本身是一种弱类型语言,而 Blazor 通过 JavaScript Interop 进行与 JavaScript 的通信,这是 ChatGPT 的解释,大家可以参考一下。
Then update the chart:
// 更新图表数据
await lineChartRef.ChangeData(Data1);
Drawing settings:
LineConfig Config1 = new LineConfig
{
Padding = "auto",
XField = "date",
YField = "value",
SeriesField = "type",
Smooth = true
};
Then you can draw.
summary
This is the first time I have tried to use winform blazor hybrid to write a small case. Blazor hybrid has just begun to understand it. Please forgive me for the shortcomings, and I hope it will help you in the end.