笔者入门C#,熟悉C#语法之后,来做一个Winform项目巩固知识,记录一下学习过程。
一、什么是WinformWinForm 是 Windows Form 的简称,是基于 .NET Framework 平台的客户端(PC软件)开发技术,一般使用C#编程。C# WinForm 编程需要创建「Windows窗体应用程序」项目。
.NET 提供了大量 Windows 风格的控件和事件,可以直接拿来使用,上手简单,开发快速
Windows 窗体应用程序是 C# 语言中的一个重要应用,也是 C# 语言最常见的应用。
2.2涉及知识点绩效考核管理系统,主要根据企业员工的身份,绩效考核,来综合计算员工的薪资。
2.3数据库结构窗体容器、数据库操作(Sql Server)、数据绑定与获取、委托事件、Sql参数化、泛型反射、反射、分层架构、工厂模式、缓存、泛型缓存、单例模式、动态创建控件。
本项目主要设计四张表,人员表Users、员工绩效表UserAppraisals、身份基数表AppraisalBases、考核系数表AppraisalCoefficients。表关系如下:
2.3.1人员表Users
存储员工的基本信息,如姓名、性别、密码、身份Id、软删除标记
创建表格SQL语句如下:
GO SET ANSI_NULLS ON GO SET QUOTED_IDENTIFIER ON GO SET ANSI_PADDING ON GO CREATE TABLE [dbo].[Users]( [Id] [int] IDENTITY(1,1) NOT NULL, [UserName] [varchar](50) NULL, [Sex] [varchar](8) NULL, [Password] [varchar](50) NULL, [BaseTypeId] [int] NULL, [IsDel] [bit] NULL, CONSTRAINT [PK_Users] PRIMARY KEY CLUSTERED ( [Id] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] ) ON [PRIMARY]
插入数据SQL语句如下:
INSERT INTO Users(UserName,Sex,Password,BaseTypeId,IsDel) VALUES('张三','男','111','1','False') INSERT INTO Users(UserName,Sex,Password,BaseTypeId,IsDel) VALUES('李四','女','111','2','False') INSERT INTO Users(UserName,Sex,Password,BaseTypeId,IsDel) VALUES('王五','男','111','3','False') INSERT INTO Users(UserName,Sex,Password,BaseTypeId,IsDel) VALUES('赵六1','女','111','4','False') INSERT INTO Users(UserName,Sex,Password,BaseTypeId,IsDel) VALUES('田七','男','111','5','False') INSERT INTO Users(UserName,Sex,Password,BaseTypeId,IsDel) VALUES('周八','女','111','6','False') INSERT INTO Users(UserName,Sex,Password,BaseTypeId,IsDel) VALUES('吴九','女','111','6','True') INSERT INTO Users(UserName,Sex,Password,BaseTypeId,IsDel) VALUES('test','男','123456','1','True') INSERT INTO Users(UserName,Sex,Password,BaseTypeId,IsDel) VALUES('test2','男','111','1','False') INSERT INTO Users(UserName,Sex,Password,BaseTypeId,IsDel) VALUES('test3','男','111','1','False') INSERT INTO Users(UserName,Sex,Password,BaseTypeId,IsDel) VALUES('test4','男','111','1','False')2.3.2员工绩效表UserAppraisals
用于记录所有员工的绩效情况,如请假次数,加班次数等
GO SET ANSI_PADDING OFF GO SET ANSI_NULLS ON GO SET QUOTED_IDENTIFIER ON GO CREATE TABLE [dbo].[UserAppraisals]( [Id] [int] IDENTITY(1,1) NOT NULL, [UserId] [int] NULL, [CoefficientId] [int] NULL, [Count] [float] NULL, [AssessmentYear] [int] NULL, [IsDel] [bit] NULL, CONSTRAINT [PK_UserAppraisal] PRIMARY KEY CLUSTERED ( [Id] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] ) ON [PRIMARY]
INSERT INTO UserAppraisals(UserId,CoefficientId,Count,AssessmentYear,IsDel) VALUES('1','1','12','2018','False') INSERT INTO UserAppraisals(UserId,CoefficientId,Count,AssessmentYear,IsDel) VALUES('1','2','2','2018','False') INSERT INTO UserAppraisals(UserId,CoefficientId,Count,AssessmentYear,IsDel) VALUES('1','3','12','2018','False') INSERT INTO UserAppraisals(UserId,CoefficientId,Count,AssessmentYear,IsDel) VALUES('1','4','158','2018','False') INSERT INTO UserAppraisals(UserId,CoefficientId,Count,AssessmentYear,IsDel) VALUES('1','5','36','2018','False') INSERT INTO UserAppraisals(UserId,CoefficientId,Count,AssessmentYear,IsDel) VALUES('1','6','3','2018','False') INSERT INTO UserAppraisals(UserId,CoefficientId,Count,AssessmentYear,IsDel) VALUES('3','1','3','2018','False') INSERT INTO UserAppraisals(UserId,CoefficientId,Count,AssessmentYear,IsDel) VALUES('3','2','3','2018','False') INSERT INTO UserAppraisals(UserId,CoefficientId,Count,AssessmentYear,IsDel) VALUES('3','3','6','2018','False') INSERT INTO UserAppraisals(UserId,CoefficientId,Count,AssessmentYear,IsDel) VALUES('3','4','0','2018','False') INSERT INTO UserAppraisals(UserId,CoefficientId,Count,AssessmentYear,IsDel) VALUES('3','5','116','2018','False') INSERT INTO UserAppraisals(UserId,CoefficientId,Count,AssessmentYear,IsDel) VALUES('3','6','0','2018','False') INSERT INTO UserAppraisals(UserId,CoefficientId,Count,AssessmentYear,IsDel) VALUES('5','1','12','2018','False') INSERT INTO UserAppraisals(UserId,CoefficientId,Count,AssessmentYear,IsDel) VALUES('5','2','12','2018','False') INSERT INTO UserAppraisals(UserId,CoefficientId,Count,AssessmentYear,IsDel) VALUES('5','3','2','2018','False') INSERT INTO UserAppraisals(UserId,CoefficientId,Count,AssessmentYear,IsDel) VALUES('5','4','2','2018','False') INSERT INTO UserAppraisals(UserId,CoefficientId,Count,AssessmentYear,IsDel) VALUES('5','5','2','2018','False') INSERT INTO UserAppraisals(UserId,CoefficientId,Count,AssessmentYear,IsDel) VALUES('5','6','12','2018','False')2.3.3身份基数表AppraisalBases
用于记录员工对应身份的基本工资,如政法编制基本工资为20000元;行政编制基本工资为1800元
GO SET ANSI_NULLS ON GO SET QUOTED_IDENTIFIER ON GO SET ANSI_PADDING ON GO CREATE TABLE [dbo].[AppraisalBases]( [Id] [int] IDENTITY(1,1) NOT NULL, [BaseType] [varchar](50) NULL, [AppraisalBase] [int] NULL, [IsDel] [bit] NULL, CONSTRAINT [PK_AppraisalBases_1] PRIMARY KEY CLUSTERED ( [Id] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] ) ON [PRIMARY]
INSERT INTO AppraisalBases(BaseType,AppraisalBase,IsDel) VALUES('政法编制1','20000','False') INSERT INTO AppraisalBases(BaseType,AppraisalBase,IsDel) VALUES('行政编制','18000','False') INSERT INTO AppraisalBases(BaseType,AppraisalBase,IsDel) VALUES('事业编制(管理类)','18000','False') INSERT INTO AppraisalBases(BaseType,AppraisalBase,IsDel) VALUES('事业编制(专业技术类)','19800','False') INSERT INTO AppraisalBases(BaseType,AppraisalBase,IsDel) VALUES('事业编制(工勤类)','16000','False') INSERT INTO AppraisalBases(BaseType,AppraisalBase,IsDel) VALUES('社会化用工','8000','False')2.3.4考核系数表AppraisalCoefficients
存储考核方式对绩效的影响关系,如影响系数,计算方式(-代表扣除工资;+代表奖励工资)
GO SET ANSI_PADDING OFF GO SET ANSI_NULLS ON GO SET QUOTED_IDENTIFIER ON GO SET ANSI_PADDING ON GO CREATE TABLE [dbo].[AppraisalCoefficients]( [Id] [int] IDENTITY(1,1) NOT NULL, [AppraisalType] [varchar](50) NULL, [AppraisalCoefficient] [float] NULL, [CalculationMethod] [int] NULL, [IsDel] [bit] NULL, CONSTRAINT [PK_AppraisalBases] PRIMARY KEY CLUSTERED ( [Id] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] ) ON [PRIMARY]
INSERT INTO AppraisalCoefficients(AppraisalType,AppraisalCoefficient,CalculationMethod,IsDel) VALUES('请假','0.1','-1','False') INSERT INTO AppraisalCoefficients(AppraisalType,AppraisalCoefficient,CalculationMethod,IsDel) VALUES('迟到','0.05','-1','False') INSERT INTO AppraisalCoefficients(AppraisalType,AppraisalCoefficient,CalculationMethod,IsDel) VALUES('加班','0.1','1','False') INSERT INTO AppraisalCoefficients(AppraisalType,AppraisalCoefficient,CalculationMethod,IsDel) VALUES('办案数量','0.0005','1','False') INSERT INTO AppraisalCoefficients(AppraisalType,AppraisalCoefficient,CalculationMethod,IsDel) VALUES('维护次数','0.002','1','False') INSERT INTO AppraisalCoefficients(AppraisalType,AppraisalCoefficient,CalculationMethod,IsDel) VALUES('项目开发','0.3','1','False')三、编码 3.1项目创建
右上角 文件-新建-项目,选择C# Windows 桌面选项下的Windows窗体应用(.NET Framework)
3.2数据库连接
首先来了解一下C#中using关键词的三个用法:
1.引用命名空间
类似java的import。可以导入其他文件中的类,而不必指明其详细的命名空间。
如using System之后,就可以直接用string代替System.string
2.using别名
给某个详细的命名空间中的某个类型取别名,用于区分不同命名空间中名字相同的类。
如using test1=A.test;
using test2=B.test;
3.用来简化资源的释放
using定义了一个范围,一旦超出了这个范围,自动调用IDisposable释放调该对象资源,
只有实现了IDisposable的类可以使用这种语法。
using(SqlConnection conn=new SqlConnection(ConStr))
using实质是一个try-finally语句,并在finally中调用对象的Dispose方法释放资源,以防程序在出错时,资源得不到释放。
连接数据库操作:
1.编写连接字符串,为了简洁,将连接字符串写在App.config里面
2.创建SqlConnection对象
namespace Appraisal_System.Utility { public class SqlHelper { public static string ConStr { get; set; } public static DataTable ExecuteTable(string cmdText, params SqlParameter[] sqlParameters) { using(SqlConnection conn=new SqlConnection(ConStr)) { conn.Open(); SqlCommand cmd = new SqlCommand(cmdText, conn); cmd.Parameters.AddRange(sqlParameters); SqlDataAdapter sda = new SqlDataAdapter(cmd); DataSet ds = new DataSet(); sda.Fill(ds); return ds.Tables[0]; } } public static int ExecuteNonQuery(string cmdText,params SqlParameter[] sqlParameters) { using (SqlConnection conn = new SqlConnection(ConStr)) { conn.Open(); SqlCommand cmd = new SqlCommand(cmdText, conn); cmd.Parameters.AddRange(sqlParameters); int rows = cmd.ExecuteNonQuery(); if (rows <= 0) { //throw new Exception("数据库操作失败"); } return rows; } } } }
3.3用户管理窗体params修饰可变数组,表示传入的参数个数不定;
SqlParameter类用来实现带参数的Sql语句,防止Sql注入。该类表示SqlCommand的参数,实际调用时可以通过SqlCommand的Parameters.Add()或AddRange()方法传入Sql参数。
窗体元素如下:
先介绍几个常见的窗体元素:
label:是一个标签,用来进行文字说明的
textBox:是一个输入框,输入文字。类似于html中的input标签中的text
comboBox:下拉框,常用于在多个项之间进行选择,如选择城市
checkBox:单选框
dataGridView:数据表格,以表格的形式展现数据
button:按钮
在dataGridView中添加列
编写AppraisalBases充血模型代码
充血模型和贫血模型区别:
充血模型:
- 数据和对应的业务逻辑被封装到同一个类中。因此,这种充血模型满足面向对象的封装特性,是典型的面向对象编程风格。
- 业务逻辑集中在 Service 类中。基于充血模型,Service 层包含 Service 类和 Domain 类两部分。Domain 是基于充血模型开发的,既包含数据,也包含业务逻辑。而 Service 类变得非常单薄。
充血模型中,绝大多业务逻辑都应该被放在domain里面,包括持久化逻辑,而Service层是很薄的一层,仅仅封装事务和少量逻辑,不和DAO层打交道。service :组合服务也叫事务服务;model:除包含get set方法,还包含原子服务和数据持久化的逻辑贫血模型:
- 贫血模型是一种领域模型,其中领域对象包含很少或没有业务逻辑。是一种面向过程的编程模式,它与面向对象设计的基本思想相悖,将数据和过程结合在一起。
- 因为贫血模型没有逻辑实现,所以逻辑基本上会放到调用贫血模型的service中,这些service类会转换领域对象的状态。
- 贫血模型中,domain包含了不依赖于持久化的原子领域逻辑,而组合逻辑在Service层。service :组合服务,也叫事务服务。model:除包含get set方法,还包含原子服务(如获得关联model的id)。dao:数据持久化。
AppraisalBases实体类如下
using System; using System.Collections.Generic; using System.Data; using System.Data.SqlClient; using System.Linq; using System.Text; using System.Threading.Tasks; using Appraisal_System.Utility; namespace Appraisal_System.Models { public class AppraisalBases { public int Id { get; set; } public string BaseType { get; set; } public int AppraisalBase { get; set; } public bool IsDel { get; set; } public static List ListAll() { List list = new List(); DataTable dt = SqlHelper.ExecuteTable("SELECt * FROM AppraisalBases"); foreach (DataRow dr in dt.Rows) { list.Add(dr.DataRowToModel()); } return list; } private static AppraisalBases ToModel(DataRow dr) { AppraisalBases appraisalBase= new AppraisalBases(); appraisalBase.Id = (int)dr["Id"]; appraisalBase.BaseType = dr["BaseType"].ToString(); appraisalBase.AppraisalBase = (int)dr["AppraisalBase"]; appraisalBase.IsDel = (bool)dr["IsDel"]; return appraisalBase; } public static int Update(AppraisalBases appraisal) { string sql = "UPDATE AppraisalBases SET BaseType=@BaseType,AppraisalBase=@AppraisalBase,IsDel=@IsDel WHERe Id=@Id"; int rows = SqlHelper.ExecuteNonQuery(sql, new SqlParameter("@Id", appraisal.Id), new SqlParameter("@BaseType", appraisal.BaseType), new SqlParameter("@AppraisalBase", appraisal.AppraisalBase), new SqlParameter("@IsDel", appraisal.IsDel) ); return rows; } } }
Users实体类如下
using Appraisal_System.Utility; using System; using System.Collections.Generic; using System.Data; using System.Data.SqlClient; using System.Linq; using System.Text; using System.Threading.Tasks; namespace Appraisal_System.Models { public class Users { public int Id { get; set; } public string UserName { get; set; } public string PassWord { get; set; } public string Sex { get; set; } public int BaseTypeId { get; set; } public bool IsDel { get; set; } public static ListListAll() { List list = new List (); string sql= "SELECT u.Id,u.UserName,u.PassWord,u.Sex,u.BaseTypeId,u.IsDel FROM Users u"; DataTable dt = SqlHelper.ExecuteTable(sql); foreach (DataRow dr in dt.Rows) { list.Add(dr.DataRowToModel ()); } return list; } public static int Insert(Users user) { string sql = $"INSERT INTO Users(UserName,PassWord,Sex,BaseTypeId,IsDel) VALUES(@UserName,@PassWord,@Sex,@BaseTypeId,@IsDel)"; //using SqlParameter to set query params return SqlHelper.ExecuteNonQuery(sql, new SqlParameter("@UserName", user.UserName), new SqlParameter("@PassWord", user.PassWord), new SqlParameter("@Sex", user.Sex), new SqlParameter("@BaseTypeId", user.BaseTypeId), new SqlParameter("@IsDel", user.IsDel) ); } public static int Update(Users user) { string sql = $"UPDATE Users SET UserName=@UserName,PassWord=@PassWord,Sex=@Sex,BaseTypeId=@BaseTypeId,IsDel=@IsDel WHERe Id=@Id"; //using SqlParameter to set query params return SqlHelper.ExecuteNonQuery(sql, new SqlParameter("@UserName", user.UserName), new SqlParameter("@PassWord", user.PassWord), new SqlParameter("@Sex", user.Sex), new SqlParameter("@BaseTypeId", user.BaseTypeId), new SqlParameter("@IsDel", user.IsDel), new SqlParameter("@Id", user.Id) ); } } }
整个窗体代码
using Appraisal_System.Models; using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; namespace Appraisal_System { public delegate void DelBindDgv();//声明一个类似string/int的关键字DelBindDgv delBindDgv public partial class FrmUserManager : Form { DelBindDgv delBindDgv; public FrmUserManager() { InitializeComponent(); } private void FrmUserManager_Load(object sender, EventArgs e) { BindCbx(); BindDgv(); delBindDgv = BindDgv; } private void BindDgv() { string userName = txtUserName.Text.Trim(); int baseTypeId = (int)cbxBase.SelectedValue; bool isStop = chkIsStop.Checked; ListuserAppraisalBases = UserAppraisalBases.GetListJoinAppraisal(); dgvUserAppraisal.AutoGenerateColumns = false; if (baseTypeId == 0) { dgvUserAppraisal.DataSource = userAppraisalBases.FindAll (m => m.UserName.Contains(userName) && m.IsDel == isStop); } else { dgvUserAppraisal.DataSource = userAppraisalBases.FindAll (m => m.UserName.Contains(userName) && m.BaseTypeId == baseTypeId && m.IsDel == isStop); } } private void BindCbx() { List appraisalBases = new List(); //add an initial choice for "querying all" appraisalBases.Add(new AppraisalBases { Id = 0, BaseType = "-查询所有-", AppraisalBase = 0, IsDel = false }); appraisalBases.AddRange(AppraisalBases.ListAll()); //set dataSource = appraisalBases which queried cbxBase.DataSource = appraisalBases; //DisplayMember means what we see on the framework cbxBase.DisplayMember = "BaseType"; //ValueMember means its value such as Id; cbxBase.ValueMember = "id"; } private void button1_Click(object sender, EventArgs e) { BindDgv(); } private void dgvUserAppraisal_MouseDown(object sender, MouseEventArgs e) { if(e.Button == MouseButtons.Right) { tsmAdd.Visible = true; tsmEdit.Visible = false; tsmStart.Visible = false; tsmStop.Visible = false; } } private void dgvUserAppraisal_CellMouseDown(object sender, DataGridViewCellMouseEventArgs e) { if (e.Button == MouseButtons.Right) { if (e.RowIndex > -1) { dgvUserAppraisal.Rows[e.RowIndex].Selected = true; tsmAdd.Visible = true; tsmEdit.Visible = true; bool IsDel = (bool)dgvUserAppraisal.SelectedRows[0].Cells["IsDel"].Value; if (IsDel) { tsmStart.Visible = true; } else { tsmStop.Visible = true; } } } } private void tsmAdd_Click(object sender, EventArgs e) { FrmSetUser frmSetUser=new FrmSetUser(delBindDgv); frmSetUser.ShowDialog(); //Reload bindDgv data to refresh the latest data which was inserted just now BindDgv(); } private void tsmEdit_Click(object sender, EventArgs e) { int userId = (int)dgvUserAppraisal.SelectedRows[0].Cells["Id"].Value; FrmSetUser frmSetUser = new FrmSetUser(delBindDgv,userId); frmSetUser.ShowDialog(); } } }
编写用户管理窗体右键事件,出现新建、编辑、启用、停用子菜单
子菜单元素为ContextMeauStrip
新建、编辑窗体
using Appraisal_System.Models; using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; namespace Appraisal_System { public partial class FrmSetUser : Form { private DelBindDgv _delBindDgv; public FrmSetUser(DelBindDgv delBindDgv) { InitializeComponent(); _delBindDgv = delBindDgv; } private Users _user; public FrmSetUser(DelBindDgv delBindDgv,int userId):this(delBindDgv) { //:this(args) like java this(args) but in C# should be after method and java in method _user = Users.ListAll().Find(m => m.Id == userId); } private void FrmSetUser_Load(object sender, EventArgs e) { List appraisalBases = new List(); //add an initial choice for "querying all" appraisalBases.AddRange(AppraisalBases.ListAll()); //set dataSource = appraisalBases which queried cbxBase.DataSource = appraisalBases; //displayMember means what we see on the framework cbxBase.DisplayMember = "BaseType"; //displayMember means its value such as Id; cbxBase.ValueMember = "Id"; //load the user which is checked now if (_user != null) { txtUserName.Text = _user.UserName; cbxBase.SelectedValue = _user.BaseTypeId; cbxSex.Text = _user.Sex; chkIsStop.Checked = _user.IsDel; } } private void btnmSave_Click(object sender, EventArgs e) { string userName = txtUserName.Text.Trim(); int baseTypeId = (int)cbxBase.SelectedValue; string sex = cbxSex.Text; bool isDel = chkIsStop.Checked; if (_user == null) { Users user = new Users { UserName = userName, BaseTypeId = baseTypeId, IsDel = isDel, Sex = sex, PassWord = "111", }; Users.Insert(user); MessageBox.Show("用户添加成功!"); } else { _user.UserName = userName; _user.BaseTypeId = baseTypeId; _user.IsDel = isDel; _user.Sex = sex; Users.Update(_user); MessageBox.Show("用户修改成功!"); } _delBindDgv(); this.Close(); } } }
3.4基数管理窗体FrmSetUser中用到了委托机制。委托简而言之,就是将某个类的某个方法委托给其他类执行,需要Delegate关键字。
委托四部曲
1、声明委托类型
2、有一个方法包含了执行的代码
3、创建委托实例
4、调用委托实例
父类将委托对象作为构造方法中的参数传递给子类
子类在创建的时候调用自己的构造方法,绑定委托对象。子类就可以在自己内部,调用该方法。
因为子窗体是用来新建和编辑用户的,那么子窗体在提交的时候,需要进行表单的数据刷新,而表单的数据刷新是由父窗体中的BindDgv方法完成的。子窗体通过调用委托,实现了间接调用了父窗体中BindDgv的方法。
工厂模式+反射+泛型缓存优化窗体的创造
场景:左侧树形菜单点击进行窗体切换时,先前都是每切换一次,就新建一个窗体,复用性差,性能低下。
解决方案:将窗体的创造交给FormFactory工厂类去执行,且每个窗体设计成单例模式,缓存在List
using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; namespace Appraisal_System.Common { public class FormFactory { //single instance private static Form form; private static List