最近通过WPF开发项目,为了对WPF知识点进行总结,所以利用业余时间,开发一个学生信息管理系统【Student Information Management System】。前四篇文章进行了框架搭建和模块划分,后台WebApi接口编写,以及课程管理模块,班级管理模块,学生管理模块的开发,本文在前四篇基础之上,继续深入开发学生信息管理系统的成绩管理和系统管理模块,通过本篇文章,将继续巩固之前的知识点,本文仅供学习分享使用,如有不足之处,还请指正。
涉及知识点
学生信息管理系统SIMS属于一个小型的完整系统开发,涉及的知识点比较,具体如下所示:
- WPF开发中TextBlock,TextBox,DataGrid,Combox,TabControl等控件的基础使用以及数据绑定等操作。
- MAH样式的使用,在本示例中MAH主要用于统一页面风格,提高用户体验。
- HttpClient使用,主要用于访问服务端提供的接口。
业务逻辑
前面几篇文章,由浅入深,逐步介绍了课程管理模块,班级管理模块,学生管理模块,今天继续介绍成绩管理模块,业务逻辑关系如下:
- 学生属于某一班级之学生,所以学生中包含班级信息。
- 班级中存在班长,同时班长又属于学生的一个实体。
- 成绩是某一学生的成绩,且一名学生有各门课程的成绩。所以成绩和学生有关,且和课程有关。
实体E-R图
学生表,成绩表,班级表,课程表,各个数据表之间的E-R图,如下所示:
由此可见,成绩表与课程和学生表,都有关联,所以放在最后。
成绩管理
成绩管理主要用于录入各个学生各个课程的成绩,包含成绩表的增删改查功能。
1. 成绩管理后台服务Service
IScoreAppService接口是对成绩管理的抽象,如下所示:
namespace SIMS.WebApi.Services.Score { public interface IScoreAppService { public PagedRequest<ScoreEntity> GetScores(string studentName,string courseName,int pageNum,int pageSize); /// <summary> /// 通过id查询成绩信息 /// </summary> /// <param name="id"></param> /// <returns></returns> public ScoreEntity GetScore(int id); /// <summary> /// 新增成绩 /// </summary> /// <param name="score"></param> /// <returns></returns> public int AddScore(ScoreEntity score); /// <summary> /// 修改成绩 /// </summary> /// <param name="score"></param> /// <returns></returns> public int UpdateScore(ScoreEntity score); /// <summary> /// 删除成绩 /// </summary> /// <param name="id"></param> public int DeleteScore(int id); } }
服务实现类ScoreAppService,是对接口的实现,具体如下所示:
namespace SIMS.WebApi.Services.Score { public class ScoreAppService : IScoreAppService { private DataContext dataContext; public ScoreAppService(DataContext dataContext) { this.dataContext = dataContext; } public int AddScore(ScoreEntity score) { var entity = this.dataContext.Scores.Add(score); this.dataContext.SaveChanges(); return 0; } public int DeleteScore(int id) { var entity = dataContext.Scores.FirstOrDefault(x => x.Id == id); if (entity != null) { dataContext.Scores.Remove(entity); dataContext.SaveChanges(); } return 0; } public ScoreEntity GetScore(int id) { var entity = dataContext.Scores.FirstOrDefault(r => r.Id == id); return entity; } /// <summary> /// 按条件查询成绩列表 /// </summary> /// <param name="studentName"></param> /// <param name="courseName"></param> /// <param name="pageNum"></param> /// <param name="pageSize"></param> /// <returns></returns> public PagedRequest<ScoreEntity> GetScores(string studentName, string courseName, int pageNum, int pageSize) { IQueryable<ScoreEntity> scores = null; if (!string.IsNullOrEmpty(studentName) && !string.IsNullOrEmpty(courseName)) { var students = this.dataContext.Students.Where(r => r.Name.Contains(studentName)); var courses = this.dataContext.Courses.Where(r => r.Name.Contains(courseName)); scores = this.dataContext.Scores.Where(r => students.Select(t => t.Id).Contains(r.StudentId)).Where(r => courses.Select(t => t.Id).Contains(r.CourseId)); } else if (!string.IsNullOrEmpty(studentName)) { var students = this.dataContext.Students.Where(r => r.Name.Contains(studentName)); scores = this.dataContext.Scores.Where(r => students.Select(t => t.Id).Contains(r.StudentId)); } else if (!string.IsNullOrEmpty(courseName)) { var courses = this.dataContext.Courses.Where(r => r.Name.Contains(courseName)); scores = this.dataContext.Scores.Where(r => courses.Select(t => t.Id).Contains(r.CourseId)); } else { scores = dataContext.Scores.Where(r => true).OrderBy(r => r.Id); } int count = scores.Count(); List<ScoreEntity> items; if (pageSize > 0) { items = scores.Skip((pageNum - 1) * pageSize).Take(pageSize).ToList(); } else { items = scores.ToList(); } return new PagedRequest<ScoreEntity>() { count = count, items = items }; } public int UpdateScore(ScoreEntity score) { dataContext.Scores.Update(score); dataContext.SaveChanges(); return 0; } } }
2. 成绩管理WebApi接口控制器
控制器是对数据服务的公开,每一个控制器的方法表示一个Action,即表示一个客户端可以访问的入口。具体如下所示:
namespace SIMS.WebApi.Controllers { /// <summary> /// 成绩控制器 /// </summary> [Route("api/[controller]/[action]")] [ApiController] public class ScoreController : ControllerBase { private readonly ILogger<ScoreController> logger; private readonly IScoreAppService scoreAppService; public ScoreController(ILogger<ScoreController> logger, IScoreAppService scoreAppService) { this.logger = logger; this.scoreAppService = scoreAppService; } /// <summary> /// 获取成绩信息 /// </summary> /// <param name="id"></param> /// <returns></returns> [HttpGet] public PagedRequest<ScoreEntity> GetScores(string? studentName, string? courseName, int pageNum, int pageSize) { return scoreAppService.GetScores(studentName, courseName, pageNum, pageSize); } /// <summary> /// 获取成绩信息 /// </summary> /// <param name="id"></param> /// <returns></returns> [HttpGet] public ScoreEntity GetScore(int id) { return scoreAppService.GetScore(id); } /// <summary> /// 新增成绩 /// </summary> /// <param name="score"></param> /// <returns></returns> [HttpPost] public int AddScore(ScoreEntity score) { return scoreAppService.AddScore(score); } /// <summary> /// 修改成绩 /// </summary> /// <param name="score"></param> /// <returns></returns> [HttpPut] public int UpdateScore(ScoreEntity score) { return scoreAppService.UpdateScore(score); } /// <summary> /// 删除成绩 /// </summary> /// <param name="id"></param> [HttpDelete] public int DeleteScore(int id) { return scoreAppService.DeleteScore(id); } } }
当服务运行起来后,Swagger还每一个控制器都进行归类,可以清晰的看到每一个接口对应的网址,成绩管理模块对应的接口如下所示:
3. 成绩管理客户端接口访问类HttpUtil
在学生信息系统开发的过程中,发现所有的接口访问都是通用的,所以对接口访问功能提取成一个HttpUtil基类【包括GET,POST,PUT,DELETE等功能】,其他具体业务再继承基类,并细化具体业务即可。ScoreHttpUtil代码如下所示:
namespace SIMS.Utils.Http { public class ScoreHttpUtil:HttpUtil { /// <summary> /// 通过id查询成绩信息 /// </summary> /// <param name="id"></param> /// <returns></returns> public static ScoreEntity GetScore(int id) { Dictionary<string, object> data = new Dictionary<string, object>(); data["id"] = id; var str = Get(UrlConfig.SCORE_GETSCORE, data); var socre = StrToObject<ScoreEntity>(str); return socre; } /// <summary> /// /// </summary> /// <param name="studentName"></param> /// <param name="courseName"></param> /// <param name="pageNum"></param> /// <param name="pageSize"></param> /// <returns></returns> public static PagedRequest<ScoreEntity> GetScores(string? studentName, string? courseName, int pageNum, int pageSize) { Dictionary<string, object> data = new Dictionary<string, object>(); data["courseName"] = courseName; data["studentName"] = studentName; data["pageNum"] = pageNum; data["pageSize"] = pageSize; var str = Get(UrlConfig.SCORE_GETSCORES, data); var socres = StrToObject<PagedRequest<ScoreEntity>>(str); return socres; } public static bool AddScore(ScoreEntity socre) { var ret = Post<ScoreEntity>(UrlConfig.SCORE_ADDSCORE, socre); return int.Parse(ret) == 0; } public static bool UpdateScore(ScoreEntity socre) { var ret = Put<ScoreEntity>(UrlConfig.SCORE_UPDATESCORE, socre); return int.Parse(ret) == 0; } public static bool DeleteScore(int Id) { Dictionary<string, string> data = new Dictionary<string, string>(); data["Id"] = Id.ToString(); var ret = Delete(UrlConfig.SCORE_DELETESCORE, data); return int.Parse(ret) == 0; } } }
4. 成绩管理客户端操作
经过前面四个部分的开发,客户端就可以与数据接口进行交互,展示数据到客户端。客户端所有的开发,均采用MVVM模式进行。
在成绩管理模块中,根据功能区分,主要包含两个View视图及对应的ViewModel。如下所示:
- Score视图,主要用于成绩的查询,以及新增,修改,删除的链接入口。
- AddEditScore视图,主要用于成绩信息的新增和修改,共用一个视图页面。
- 成绩课程不需要页面,所以没有对应视图。
4.1. Score视图
Score视图,主要是成绩的查询和新增,修改,删除的链接入口。涉及知识点如下:
- Score视图页面布局采用Grid方式和StackPanel混合布局,即整体布局采用Grid,细微布局采用StackPanel。
- 成绩采用分页列表的方式展示,需要用到DataGrid,及分页控件【WPF默认不提供分页控件,可自行编写分页控件】。
- 查询条件采用按钮Button和文本框TextBox等组成,关于基础控件的使用,不再详细论述,可参考其他文章。
- 在本系统的所有WPF视图中,均需要引入Prism和 MAH组件。
- Score视图中,所有的数据均采用Binding的方式与ViewModel进行交互。
Score视图具体代码,如下所示:
<UserControl x:Class="SIMS.ScoreModule.Views.Score" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:i="http://schemas.microsoft.com/xaml/behaviors" xmlns:prism="http://prismlibrary.com/" xmlns:local="clr-namespace:SIMS.ScoreModule.Views" mc:Ignorable="d" xmlns:mahApps="http://metro.mahapps.com/winfx/xaml/controls" xmlns:ctrls ="clr-namespace:SIMS.Utils.Controls;assembly=SIMS.Utils" prism:ViewModelLocator.AutoWireViewModel="True" d:DesignHeight="450" d:DesignWidth="800"> <UserControl.Resources> <ResourceDictionary> <ResourceDictionary.MergedDictionaries> <ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Controls.xaml" /> <ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Themes/Light.Blue.xaml" /> <ResourceDictionary> <Style x:Key="LinkButton" TargetType="Button"> <Setter Property="Background" Value="White"></Setter> <Setter Property="Cursor" Value="Hand"></Setter> <Setter Property="Margin" Value="3"></Setter> <Setter Property="MinWidth" Value="80"></Setter> <Setter Property="MinHeight" Value="25"></Setter> <Setter Property="BorderThickness" Value="0 0 0 0"></Setter> </Style> </ResourceDictionary> </ResourceDictionary.MergedDictionaries> </ResourceDictionary> </UserControl.Resources> <i:Interaction.Triggers> <i:EventTrigger EventName="Loaded"> <i:InvokeCommandAction Command="{Binding LoadedCommand}"></i:InvokeCommandAction> </i:EventTrigger> </i:Interaction.Triggers> <Grid> <Grid.RowDefinitions> <RowDefinition Height="Auto"></RowDefinition> <RowDefinition Height="Auto"></RowDefinition> <RowDefinition Height="*"></RowDefinition> <RowDefinition Height="Auto"></RowDefinition> </Grid.RowDefinitions> <TextBlock Text="成绩信息" FontSize="20" Background="AliceBlue" Margin="2"></TextBlock> <StackPanel Grid.Row="1" Orientation="Horizontal" VerticalAlignment="Center"> <TextBlock Text="学生名称" VerticalAlignment="Center" Margin="2"></TextBlock> <TextBox Margin="4" MinWidth="120" Height="30" Text="{Binding StudentName}" HorizontalContentAlignment="Stretch" mahApps:TextBoxHelper.ClearTextButton="True" mahApps:TextBoxHelper.Watermark="学生名称" mahApps:TextBoxHelper.WatermarkAlignment="Left" SpellCheck.IsEnabled="True" /> <TextBlock Text="课程名称" VerticalAlignment="Center" Margin="2"></TextBlock> <TextBox Margin="4" MinWidth="120" Height="30" Text="{Binding CourseName}" HorizontalContentAlignment="Stretch" mahApps:TextBoxHelper.ClearTextButton="True" mahApps:TextBoxHelper.Watermark="课程名称" mahApps:TextBoxHelper.WatermarkAlignment="Left" SpellCheck.IsEnabled="True" /> <Button Content="查询" Style="{DynamicResource MahApps.Styles.Button.Square.Accent}" Width="120" Height="30" Margin="3" Command="{Binding QueryCommand}"></Button> <Button Content="新增" Style="{DynamicResource MahApps.Styles.Button.Square.Accent}" Width="120" Height="30" Margin="3" Command="{Binding AddCommand}"></Button> </StackPanel> <DataGrid x:Name="dgScores" Grid.Row="2" Grid.Column="0" Margin="2" AutoGenerateColumns="False" CanUserAddRows="False" CanUserDeleteRows="False" ItemsSource="{Binding Scores}" RowHeaderWidth="0"> <DataGrid.Columns> <DataGridTextColumn Binding="{Binding Student.Name}" Header="学生" Width="*" /> <DataGridTextColumn Binding="{Binding Course.Name}" Header="课程" Width="*"/> <DataGridTextColumn Binding="{Binding Score}" Header="成绩" Width="*"/> <DataGridTextColumn Binding="{Binding CreateTime, StringFormat=yyyy-MM-dd HH:mm:ss}" Header="创建时间" Width="*"/> <DataGridTextColumn Binding="{Binding LastEditTime,StringFormat=yyyy-MM-dd HH:mm:ss}" Header="最后修改时间" Width="*"/> <DataGridTemplateColumn Header="操作" Width="*"> <DataGridTemplateColumn.CellTemplate> <DataTemplate> <StackPanel Orientation="Horizontal"> <Button Content="Edit" Style="{StaticResource LinkButton}" Command="{Binding RelativeSource={RelativeSource AncestorType=DataGrid, Mode=FindAncestor}, Path=DataContext.EditCommand}" CommandParameter="{Binding Id}"> <Button.Template> <ControlTemplate TargetType="Button"> <TextBlock TextDecorations="Underline" HorizontalAlignment="Center"> <ContentPresenter /> </TextBlock> </ControlTemplate> </Button.Template> </Button> <Button Content="Delete" Style="{StaticResource LinkButton}" Command="{Binding RelativeSource={RelativeSource AncestorType=DataGrid, Mode=FindAncestor}, Path=DataContext.DeleteCommand}" CommandParameter="{Binding Id}"> <Button.Template> <ControlTemplate TargetType="Button"> <TextBlock TextDecorations="Underline" HorizontalAlignment="Center"> <ContentPresenter /> </TextBlock> </ControlTemplate> </Button.Template> </Button> </StackPanel> </DataTemplate> </DataGridTemplateColumn.CellTemplate> </DataGridTemplateColumn> </DataGrid.Columns> </DataGrid> <ctrls:PageControl Grid.Row="3" DataContext="{Binding}" ></ctrls:PageControl> </Grid> </UserControl>
4.2. ScoreViewModel
ScoreViewModel是页面视图的业务逻辑处理,如处理客户端的点击的命令等内容。具体代码如下所示:
namespace SIMS.ScoreModule.ViewModels { public class ScoreViewModel : BindableBase { #region 属性或构造方法 /// <summary> /// 课程名称 /// </summary> private string courseName; public string CourseName { get { return courseName; } set { SetProperty(ref courseName, value); } } /// <summary> /// 学生姓名 /// </summary> private string studentName; public string StudentName { get { return studentName; } set { SetProperty(ref studentName, value); } } private ObservableCollection<ScoreInfo> scores; public ObservableCollection<ScoreInfo> Scores { get { return scores; } set { SetProperty(ref scores, value); } } private IDialogService dialogService; public ScoreViewModel(IDialogService dialogService) { this.dialogService = dialogService; this.pageNum = 1; this.pageSize = 20; } private void InitInfo() { Scores = new ObservableCollection<ScoreInfo>(); var pagedRequst = ScoreHttpUtil.GetScores(this.StudentName, this.CourseName, this.pageNum, this.pageSize); var entities = pagedRequst.items; Scores.AddRange(entities.Select(r=>new ScoreInfo(r))); // this.TotalCount = pagedRequst.count; this.TotalPage = ((int)Math.Ceiling(this.TotalCount * 1.0 / this.pageSize)); } #endregion #region 事件 private DelegateCommand loadedCommand; public DelegateCommand LoadedCommand { get { if (loadedCommand == null) { loadedCommand = new DelegateCommand(Loaded); } return loadedCommand; } } private void Loaded() { InitInfo(); } private DelegateCommand queryCommand; public DelegateCommand QueryCommand { get { if (queryCommand == null) { queryCommand = new DelegateCommand(Query); } return queryCommand; } } private void Query() { this.pageNum = 1; this.InitInfo(); } /// <summary> /// 新增命令 /// </summary> private DelegateCommand addCommand; public DelegateCommand AddCommand { get { if (addCommand == null) { addCommand = new DelegateCommand(Add); } return addCommand; } } private void Add() { this.dialogService.ShowDialog("addEditScore", null, AddEditCallBack, "MetroDialogWindow"); } private void AddEditCallBack(IDialogResult dialogResult) { if (dialogResult != null && dialogResult.Result == ButtonResult.OK) { //刷新列表 this.pageNum = 1; this.InitInfo(); } } /// <summary> /// 编辑命令 /// </summary> private DelegateCommand<object> editCommand; public DelegateCommand<object> EditCommand { get { if (editCommand == null) { editCommand = new DelegateCommand<object>(Edit); } return editCommand; } } private void Edit(object obj) { if (obj == null) { return; } var Id = int.Parse(obj.ToString()); var score = this.Scores.FirstOrDefault(r => r.Id == Id); if (score == null) { MessageBox.Show("无效的成绩ID"); return; } IDialogParameters dialogParameters = new DialogParameters(); dialogParameters.Add("score", score); this.dialogService.ShowDialog("addEditScore", dialogParameters, AddEditCallBack, "MetroDialogWindow"); } /// <summary> /// 编辑命令 /// </summary> private DelegateCommand<object> deleteCommand; public DelegateCommand<object> DeleteCommand { get { if (deleteCommand == null) { deleteCommand = new DelegateCommand<object>(Delete); } return deleteCommand; } } private void Delete(object obj) { if (obj == null) { return; } var Id = int.Parse(obj.ToString()); var score = this.Scores.FirstOrDefault(r => r.Id == Id); if (score == null) { MessageBox.Show("无效的成绩ID"); return; } if (MessageBoxResult.Yes != MessageBox.Show("Are you sure to delete?", "Confirm", MessageBoxButton.YesNo)) { return; } bool flag = ScoreHttpUtil.DeleteScore(Id); if (flag) { this.pageNum = 1; this.InitInfo(); } } #endregion } }
注意:关于分页功能,与其他模块代码通用,所以此处略去。