本课完成目标
- 规划一个完整 Windows Forms 桌面管理系统。
- 使用多窗体设计 Dashboard、Students、Settings 与 API 工具。
- 使用 Student 实体类表示领域数据。
- 通过 IStudentRepository 隔离数据访问。
- 使用 StudentRepository 执行参数化 SQL CRUD。
- 使用 StudentService 集中业务验证。
- 使用 Async/Await 异步读取数据库。
- 使用 DataGridView 显示、选择与刷新记录。
- 使用 SQL 与 LINQ 完成搜索、筛选与统计。
- 使用 appsettings.json 和用户偏好配置。
- 加入 JSON / Web API 工具作为扩展功能。
- 完成 Release 与 Publish 前最终检查。
36.1 最终项目要解决什么问题?
StudentDesktopManagementSystem2026 是一个小型但结构完整的学生资料管理系统。
总览
显示学生总数、平均成绩、最高成绩与及格人数。
学生管理
新增、读取、修改、删除、搜索、筛选和排序。
用户设置
记住课程、窗体大小和应用偏好。
网络资料
调用公开 API,展示 JSON 与 HttpClient 整合。
分层结构
UI、Service、Repository、Database 各负其责。
可发布
配置文件、用户资料与外部数据库依赖都有明确处理。
36.2 系统整体架构
重要原则:
Form 不直接写 SQL
Service 不直接读取 TextBox
Repository 不弹 MessageBox
Student 不负责数据库连接
36.3 项目结构
36.4 数据库结构
继续使用前面建立的:
StudentManagementDB
dbo.Students
如果需要重新建立:
CREATE TABLE dbo.Students
(
StudentID INT IDENTITY(1,1) PRIMARY KEY,
Name NVARCHAR(100) NOT NULL,
Course NVARCHAR(100) NOT NULL,
Score DECIMAL(5,2) NOT NULL
CHECK (Score BETWEEN 0 AND 100)
);
36.5 Student.vb
Public Class Student
Public Property StudentID As Integer
Public Property Name As String = ""
Public Property Course As String = ""
Public Property Score As Decimal
Public ReadOnly Property IsPass As Boolean
Get
Return Score >= 50D
End Get
End Property
End Class
这里加入了一个只读计算属性:
IsPass
它属于 Student 本身可以表达的状态,不需要数据库字段。
36.6 IStudentRepository.vb
Public Interface IStudentRepository
Function GetAllAsync() _
As Task(Of List(Of Student))
Function InsertAsync(
student As Student) _
As Task(Of Integer)
Function UpdateAsync(
student As Student) _
As Task(Of Integer)
Function DeleteAsync(
studentId As Integer) _
As Task(Of Integer)
Function SearchAsync(
keyword As String,
course As String) _
As Task(Of List(Of Student))
End Interface
36.7 StudentRepository:数据库实现
文件顶部:
Imports Microsoft.Data.SqlClient
类声明:
Public Class StudentRepository
Implements IStudentRepository
36.8 异步读取全部学生
Public Async Function GetAllAsync() _
As Task(Of List(Of Student)) _
Implements IStudentRepository.GetAllAsync
Const sql As String =
"SELECT StudentID, Name, Course, Score " &
"FROM dbo.Students " &
"ORDER BY StudentID;"
Dim students As New List(Of Student)()
Using connection As New SqlConnection(
DatabaseHelper.ConnectionString)
Using command As New SqlCommand(
sql,
connection)
Await connection.OpenAsync()
Using reader As SqlDataReader =
Await command.ExecuteReaderAsync()
While Await reader.ReadAsync()
students.Add(
New Student With {
.StudentID = reader.GetInt32(0),
.Name = reader.GetString(1),
.Course = reader.GetString(2),
.Score = reader.GetDecimal(3)
})
End While
End Using
End Using
End Using
Return students
End Function
36.9 参数化异步 INSERT
Public Async Function InsertAsync(
student As Student) _
As Task(Of Integer) _
Implements IStudentRepository.InsertAsync
Const sql As String =
"INSERT INTO dbo.Students " &
"(Name, Course, Score) " &
"VALUES (@Name, @Course, @Score);"
Using connection As New SqlConnection(
DatabaseHelper.ConnectionString)
Using command As New SqlCommand(
sql,
connection)
command.Parameters.Add(
"@Name",
SqlDbType.NVarChar,
100).Value =
student.Name
command.Parameters.Add(
"@Course",
SqlDbType.NVarChar,
100).Value =
student.Course
Dim scoreParameter As SqlParameter =
command.Parameters.Add(
"@Score",
SqlDbType.Decimal)
scoreParameter.Precision = 5
scoreParameter.Scale = 2
scoreParameter.Value =
student.Score
Await connection.OpenAsync()
Return Await command.ExecuteNonQueryAsync()
End Using
End Using
End Function
36.10 UPDATE 与 DELETE
Public Async Function UpdateAsync(
student As Student) _
As Task(Of Integer) _
Implements IStudentRepository.UpdateAsync
Const sql As String =
"UPDATE dbo.Students " &
"SET Name = @Name, " &
"Course = @Course, " &
"Score = @Score " &
"WHERE StudentID = @StudentID;"
Using connection As New SqlConnection(
DatabaseHelper.ConnectionString)
Using command As New SqlCommand(
sql,
connection)
command.Parameters.Add(
"@StudentID",
SqlDbType.Int).Value =
student.StudentID
command.Parameters.Add(
"@Name",
SqlDbType.NVarChar,
100).Value =
student.Name
command.Parameters.Add(
"@Course",
SqlDbType.NVarChar,
100).Value =
student.Course
Dim scoreParameter As SqlParameter =
command.Parameters.Add(
"@Score",
SqlDbType.Decimal)
scoreParameter.Precision = 5
scoreParameter.Scale = 2
scoreParameter.Value =
student.Score
Await connection.OpenAsync()
Return Await command.ExecuteNonQueryAsync()
End Using
End Using
End Function
Public Async Function DeleteAsync(
studentId As Integer) _
As Task(Of Integer) _
Implements IStudentRepository.DeleteAsync
Const sql As String =
"DELETE FROM dbo.Students " &
"WHERE StudentID = @StudentID;"
Using connection As New SqlConnection(
DatabaseHelper.ConnectionString)
Using command As New SqlCommand(
sql,
connection)
command.Parameters.Add(
"@StudentID",
SqlDbType.Int).Value =
studentId
Await connection.OpenAsync()
Return Await command.ExecuteNonQueryAsync()
End Using
End Using
End Function
36.11 搜索方法
综合姓名关键字与课程:
Public Async Function SearchAsync(
keyword As String,
course As String) _
As Task(Of List(Of Student)) _
Implements IStudentRepository.SearchAsync
Dim sql As String =
"SELECT StudentID, Name, Course, Score " &
"FROM dbo.Students " &
"WHERE Name LIKE @NamePattern "
If course <> "全部课程" Then
sql &=
"AND Course = @Course "
End If
sql &=
"ORDER BY Name;"
Dim students As New List(Of Student)()
Using connection As New SqlConnection(
DatabaseHelper.ConnectionString)
Using command As New SqlCommand(
sql,
connection)
command.Parameters.Add(
"@NamePattern",
SqlDbType.NVarChar,
100).Value =
"%" & keyword & "%"
If course <> "全部课程" Then
command.Parameters.Add(
"@Course",
SqlDbType.NVarChar,
100).Value =
course
End If
Await connection.OpenAsync()
Using reader As SqlDataReader =
Await command.ExecuteReaderAsync()
While Await reader.ReadAsync()
students.Add(
New Student With {
.StudentID = reader.GetInt32(0),
.Name = reader.GetString(1),
.Course = reader.GetString(2),
.Score = reader.GetDecimal(3)
})
End While
End Using
End Using
End Using
Return students
End Function
End Class
36.12 StudentService:业务规则
Public Class StudentService
Private ReadOnly repository As IStudentRepository
Public Sub New(
repository As IStudentRepository)
Me.repository =
repository
End Sub
Public Function Validate(
student As Student,
ByRef errorMessage As String) As Boolean
If String.IsNullOrWhiteSpace(
student.Name) Then
errorMessage =
"请输入学生姓名。"
Return False
End If
If String.IsNullOrWhiteSpace(
student.Course) Then
errorMessage =
"请输入课程名称。"
Return False
End If
If student.Score < 0D OrElse
student.Score > 100D Then
errorMessage =
"成绩必须介于 0 与 100。"
Return False
End If
errorMessage = ""
Return True
End Function
36.13 Service 异步 CRUD
Public Async Function GetStudentsAsync() _
As Task(Of List(Of Student))
Return Await repository.GetAllAsync()
End Function
Public Async Function AddStudentAsync(
student As Student,
errorMessage As Action(Of String)) _
As Task(Of Boolean)
Dim message As String = ""
If Not Validate(
student,
message) Then
errorMessage(message)
Return False
End If
Return Await repository.InsertAsync(
student) = 1
End Function
为了让本项目更容易理解,也可以使用上一课熟悉的 ByRef errorMessage 版本。这里展示的是另一种把错误回传给调用者的方法。
36.14 MainForm:系统主界面
MainForm 建议使用:
MenuStrip
ToolStrip
StatusStrip
Panel
DataGridView
Label Dashboard Cards
菜单:
File
├─ Refresh
└─ Exit
Students
├─ Add Student
├─ Edit Selected
└─ Delete Selected
Tools
├─ Settings
└─ API Viewer
Help
└─ About
| ID | Name | Course | Score | Pass |
|---|---|---|---|---|
| 1 | 李明 | Visual Basic 2026 | 88.00 | True |
| 4 | 李华 | Visual Basic 2026 | 93.00 | True |
| 5 | 张敏 | C# Programming | 67.00 | True |
36.15 MainForm 建立依赖
Public Class MainForm
Private ReadOnly studentService As StudentService
Private students As New List(Of Student)()
Public Sub New()
InitializeComponent()
Dim repository As IStudentRepository =
New StudentRepository()
studentService =
New StudentService(
repository)
End Sub
36.16 MainForm 异步加载
Private Async Sub MainForm_Load(
sender As Object,
e As EventArgs) Handles MyBase.Load
ConfigureGrid()
Await RefreshStudentsAsync()
End Sub
36.17 RefreshStudentsAsync()
Private Async Function RefreshStudentsAsync() As Task
UseWaitCursor =
True
lblStatus.Text =
"正在读取学生资料..."
Try
students =
Await studentService.GetStudentsAsync()
dgvStudents.DataSource =
Nothing
dgvStudents.DataSource =
students
UpdateDashboard()
lblStatus.Text =
$"已加载 {students.Count} 笔记录。"
Catch ex As Exception
MessageBox.Show(
ex.Message,
"读取失败",
MessageBoxButtons.OK,
MessageBoxIcon.Error)
lblStatus.Text =
"读取失败。"
Finally
UseWaitCursor =
False
End Try
End Function
36.18 用 LINQ 更新 Dashboard
Private Sub UpdateDashboard()
lblTotalStudents.Text =
students.Count.ToString()
If students.Count = 0 Then
lblAverageScore.Text =
"0.00"
lblHighestScore.Text =
"0.00"
lblPassCount.Text =
"0"
Exit Sub
End If
lblAverageScore.Text =
students.
Average(
Function(s) s.Score).
ToString("N2")
lblHighestScore.Text =
students.
Max(
Function(s) s.Score).
ToString("N2")
lblPassCount.Text =
students.
Count(
Function(s) s.IsPass).
ToString()
End Sub
36.19 StudentForm:新增与修改共用一个窗体
StudentForm 控件:
txtStudentID
txtName
cmbCourse
txtScore
btnSave
btnCancel
公开结果:
Public ReadOnly Property StudentResult As Student
36.20 StudentForm 核心代码
Public Class StudentForm
Public ReadOnly Property StudentResult As Student
Public Sub New(
student As Student)
InitializeComponent()
If student IsNot Nothing Then
txtStudentID.Text =
student.StudentID.ToString()
txtName.Text =
student.Name
cmbCourse.Text =
student.Course
txtScore.Text =
student.Score.ToString()
End If
End Sub
Private Sub btnSave_Click(
sender As Object,
e As EventArgs) Handles btnSave.Click
Dim score As Decimal
If Not Decimal.TryParse(
txtScore.Text,
score) Then
MessageBox.Show(
"请输入有效成绩。")
Exit Sub
End If
Dim studentId As Integer
Integer.TryParse(
txtStudentID.Text,
studentId)
StudentResult =
New Student With {
.StudentID = studentId,
.Name = txtName.Text.Trim(),
.Course = cmbCourse.Text.Trim(),
.Score = score
}
DialogResult =
DialogResult.OK
End Sub
End Class
36.21 新增学生
Private Async Sub mnuAddStudent_Click(
sender As Object,
e As EventArgs) Handles mnuAddStudent.Click
Using form As New StudentForm(
Nothing)
If form.ShowDialog(Me) <>
DialogResult.OK Then
Exit Sub
End If
Dim errorText As String = ""
If Not studentService.Validate(
form.StudentResult,
errorText) Then
MessageBox.Show(
errorText)
Exit Sub
End If
Await studentService.AddStudentAsync(
form.StudentResult,
Sub(message)
errorText = message
End Sub)
Await RefreshStudentsAsync()
End Using
End Sub
36.22 取得 DataGridView 当前学生
Private Function GetSelectedStudent() As Student
Return TryCast(
dgvStudents.CurrentRow?.
DataBoundItem,
Student)
End Function
36.23 修改与删除
修改时:
Dim selected As Student =
GetSelectedStudent()
If selected Is Nothing Then
MessageBox.Show(
"请先选择学生。")
Exit Sub
End If
然后打开 StudentForm,并把修改后的 Student 交给 Service。
删除时:
Dim selected As Student =
GetSelectedStudent()
If selected Is Nothing Then
Exit Sub
End If
Dim result =
MessageBox.Show(
$"确定删除 {selected.Name}?",
"确认删除",
MessageBoxButtons.YesNo,
MessageBoxIcon.Warning)
If result <> DialogResult.Yes Then
Exit Sub
End If
36.24 搜索与筛选
MainForm 加入:
txtSearch
cmbFilterCourse
btnSearch
btnShowAll
搜索按钮:
Private Async Sub btnSearch_Click(
sender As Object,
e As EventArgs) Handles btnSearch.Click
Dim repository As IStudentRepository =
New StudentRepository()
Dim results As List(Of Student) =
Await repository.SearchAsync(
txtSearch.Text.Trim(),
cmbFilterCourse.Text)
dgvStudents.DataSource =
Nothing
dgvStudents.DataSource =
results
lblStatus.Text =
$"搜索结果:{results.Count} 笔。"
End Sub
36.25 整合 Lesson 34 设置功能
appsettings.json:
{
"Application": {
"Name": "Student Desktop Management System 2026",
"DefaultCourse": "Visual Basic 2026"
},
"Api": {
"BaseUrl": "https://jsonplaceholder.typicode.com"
}
}
MainForm 标题:
Me.Text =
AppConfiguration.ApplicationName
SettingsForm 可以保存:
RememberLastCourse
LastCourse
WindowWidth
WindowHeight
36.26 整合 JSON 与 Web API
ApiViewerForm 使用 Lesson 33 的 ApiUser:
Public Class ApiUser
Public Property Id As Integer
Public Property Name As String
Public Property Username As String
Public Property Email As String
End Class
ApiUserService:
Imports System.Net.Http.Json
Public Class ApiUserService
Private ReadOnly httpClient As New HttpClient()
Public Async Function GetUsersAsync() _
As Task(Of List(Of ApiUser))
Dim url As String =
AppConfiguration.ApiBaseUrl &
"/users"
Dim users =
Await httpClient.GetFromJsonAsync(
Of List(Of ApiUser))(
url)
Return If(
users,
New List(Of ApiUser)())
End Function
End Class
36.27 多窗体协作
打开 Settings:
Using form As New SettingsForm()
form.ShowDialog(Me)
End Using
36.28 统一错误处理原则
让数据库异常继续往上抛,不直接 MessageBox。
在用户操作边界 Catch,并显示容易理解的错误信息。
例如:
Try
Await RefreshStudentsAsync()
Catch ex As Exception
MessageBox.Show(
ex.Message)
End Try
36.29 用户实际操作流程
读取 appsettings.json 与用户偏好。
Repository → Service → MainForm。
LINQ 计算总数、平均、最高分和及格人数。
StudentForm + Service + 参数化 SQL。
LIKE + Course 参数筛选。
JSON 保存偏好并调用远程 API。
36.30 建议按阶段完成,不要一次写完
Student、IStudentRepository、StudentRepository 编译成功。
验证与异步 CRUD 完成。
DataGridView 和 Dashboard 正常。
新增、修改、删除完整。
筛选、配置与用户偏好完成。
远程 API 与发布测试完成。
36.31 最终测试清单
36.32 发布最终项目
完成后:
Configuration = Release
Target = Folder
Deployment Mode = Self-contained
Target Runtime = win-x64
然后测试:
publish
└─ StudentDesktopManagementSystem2026.exe
36.33 GitHub 项目建议
建议仓库包含:
src\
database\
create-database.sql
README.md
.gitignore
README 可以说明:
项目功能
运行环境
数据库创建步骤
NuGet 包
配置方式
截图
发布说明
36.34 完成后可以怎样继续扩展?
课程管理
建立 Courses 表与 CourseRepository。
报表导出
CSV、JSON、HTML 或 PDF 报表。
用户登入
管理员与普通用户权限。
统计图表
课程人数、成绩分布与趋势。
服务化
把数据库访问移到 ASP.NET Core Web API。
AI 辅助功能
加入自然语言查询、摘要或学生表现分析。
36.35 综合项目常见问题
一开始就写所有功能
综合项目应该按 Milestone 完成,每一步都先 Rebuild 和测试。
Form1 又开始塞满 SQL
最终项目仍要坚持 Repository / Service 的职责分离。
异步方法调用忘记 Await
数据库与 API 的异步操作都要保持正确的 Async/Await 调用链。
只测试成功路径
还要测试无数据、错误输入、断网、数据库不可用和找不到配置文件。
36.36 最终挑战
- 把 SearchAsync 从 MainForm 的 Repository 调用移到 StudentService。
- 加入 CourseRepository 与课程数据库表。
- 建立 StudentDetailsForm,只读显示完整学生资料。
- 加入“Top 5 Students” LINQ 面板。
- 加入 CSV 或 JSON Export 功能。
- 把 API Viewer 改成从真实课程 API 读取资料。
- 建立 Self-contained win-x64 发布版本,并在另一台电脑测试。
最终复习
- 为什么 Form 不应该直接写 SQL?
- IStudentRepository 带来什么好处?
- StudentService 应该负责哪些事情?
- 参数化 SQL 为什么必须保留到最终项目?
- 为什么数据库读取适合使用 Async/Await?
- LINQ 在 Dashboard 中解决了哪些统计问题?
- appsettings.json 与 user-settings.json 有什么不同?
- Web API 为什么通常使用 JSON?
- 为什么 Self-contained 仍然不代表 LocalDB 已部署?
- 综合项目为什么应该按 Milestone 分阶段完成?
恭喜完成 36 课 Visual Basic 2026 中文教程
从第 1 课认识 Visual Basic 与 Windows Forms,到第 36 课完成一个分层、异步、数据库驱动并能够发布的桌面管理系统,你已经走过了变量、判断、循环、方法、类、集合、文件、资源、多窗体、数据库、CRUD、LINQ、接口、多态、Async/Await、JSON、Web API、配置与部署的完整学习路径。
下一步最重要的不是再背更多语法,而是继续扩展这个最终项目:增加真实功能、重构重复代码、编写测试、部署给别人使用,并从实际问题中继续学习。