窗体开发的主题内容
1. 窗体属性设置
2. CRUD与代码生成
3. 高级选项
4. 设计规范
使用EPN框架制作员工主档窗体,效果图如下
属性设置
增加窗体,EmployeeMaster
设置继承的类为EntryForm
[FunctionCode("SAISEM")]
public partial class EmployeeMaster : EntryForm
{
public EmployeeMaster()
{
InitializeComponent();
}
}
并且添加功能编码SAISEM
在EPN的分类设置选项中,设置窗体的EPN属性如下
这表示,窗体支持新增,修改,删除,拷贝,导入/导出,过帐,增加附件,不支持批核(工作流)
拖动一个EntityCollection到窗体中,并设置它的EntityFactoryToUse对象
依照需要展示的数据层次结构,拖动为数个BindingSource到窗体中。
举例:比如要显示采购单头和采购明细信息,需要拖动2个BindingSource,分别显示采购单头和采购明细
对于本例,需要显示员工表的表头信息和文件(Document)信息,则需要拖动2个BindingSource
组件窗口看起来是这样
然后依次设置BindingSource的数据源
设置employeeBindingSource的数据源
设置employeeDocumentBindingSource的数据源
依据需要显示的字段,放窗体中拖放TextEditor,NumberiEditor,Grid,CheckBox
在Visual Studio 2010中,看起来是这样
设置窗体的数据源属性,MainBindingSource和NavigateBindingSource
如果当前窗体要一次性将所有的Entity读入到窗体的GridView,则将Form的MainBindingSource与navigatorBindingSource都设置为employeeBindingSource。
如果窗体打开时只需显示一条Entity,则将窗体的MainBindingSource设为employeeBindingSource,再添加一个新的bindingSource组件,数据源不要选,留空。将其设为窗体的NavigatorBindingSource。
设置窗体主键属性
设置值如下
至此,F5,窗体已经可以运行,在快速启动栏中输入SAISEM,即可启动窗体,如开篇所展示的效果。
代码生成
到目前为止,窗体已经可以运行,但并不可以进行数据的读写,我们还要给它新增加数据读写代码。
再引用开发框架中关于库存项目的一张图
BusinessLogic 业务逻辑,业务实体,这个由代码生成器自动生成
Interface 接口 读写数据和业务逻辑的接口
Manager 实现接口 实现接口
System Administration/Inventory, 界面,放入EmployeeMaster窗体
为此,祭出代码生成工具CodeSmith,帮忙我们完成枯燥容易出错的应用框架的代码。
接口文件IEmployeeManager,将生成的文件拷贝到Interface项目中
实现文件EmployeeManager,将生成的文件拷贝到Manager项目中
窗体文件生成,将生成的文件内容拷贝到EmployeeMaster窗体文件中
代码如下
private IEmployeeManager _EmployeeManager = null ;
private EmployeeEntity _Employee = null ;
protected override void OnShown(EventArgs e)
{
base.OnShown(e);
if (!DesignMode)
{
}
}
protected override void OnLoad(EventArgs e)
{
if(!DesignMode)
this._EmployeeManager = ClientProxyFactory.CreateProxyInstance<IEmployeeManager>();
base.OnLoad(e);
}
protected override void InitNavigator(InitNavigatorArgs args)
{
base.InitNavigator(args);
args.SortExpression.Add(EmployeeFields.EmployeeNo | SortOperator.Ascending);
}
protected override EntityBase2 LoadData(Dictionary<string, string> refNo)
{
base.LoadData(refNo);
string employeeNo = string.Empty;
if (refNo.TryGetValue("EmployeeNo", out employeeNo))
{
IPrefetchPath2 prefetchPath = new PrefetchPath2((int)EntityType.EmployeeEntity);
prefetchPath.Add(EmployeeEntity.PrefetchPathEmployeeDocument);
_employee = _employeeManager.GetEmployee(employeeNo, prefetchPath);
}
else
{
_employee = new EmployeeEntity();
}
return _employee;
}
protected override void BindControls(EntityBase2 entity)
{
base.BindControls(entity);
this.employeeBindingSource.DataSource = entity;
}
protected override EntityBase2 Add( )
{
base.Add();
this._Employee = new EmployeeEntity();
return _Employee;
}
protected override EntityBase2 Save(EntityBase2 entityToSave)
{
base.Save(entityToSave);
EmployeeEntity _Employee = (EmployeeEntity)entityToSave;
this._Employee =this._EmployeeManager.SaveEmployee(_Employee);
return _Employee;
}
protected override void Delete(EntityBase2 entityToDelete)
{
base.Delete(entityToDelete);
EmployeeEntity Employee = (EmployeeEntity)entityToDelete;
this._EmployeeManager.DeleteEmployee(Employee);
}
protected override object Clone(Dictionary<string, string> refNo)
{
base.Clone(refNo);
string employeeNo = string.Empty;
refNo.TryGetValue("EmployeeNo", out employeeNo);
if (string.IsNullOrEmpty(employeeNo))
{
using (ILookupForm lookup = EPN.Common.GetLookupForm("EmployeeLookup"))
{
lookup.SetCurrentValue(CurrentRefNo);
if (lookup.ShowDialog() != DialogResult.OK)
return null;
employeeNo = lookup.GetFirstSelectionValue();
}
}
if (!string.IsNullOrEmpty(employeeNo))
{
this._employee = this._employeeManager.CloneEmployee(employeeNo);
return this._employee;
}
return null;
}
protected override void ReleaseResources( )
{
base.ReleaseResources();
try
{
_Employee = null;
_EmployeeManager= null;
}
catch
{
}
}
到此,已经完成了窗体开发的所有内容。
为加入更多的验证逻辑,生成一些验证,放到验证项目中
比如,需要验证身份证号码必须输入,电话号码要符合当地的编号规则,等等,这些逻辑验证,都可以写在这里
高级选项
到此为止,几乎是没有敲任何的代码,我们就完成了员工主档的开发,这样的速度,肯定让你吃惊,也开始疑惑:
1. 业务逻辑写在哪里?
比如,当输入员工的出生日期为1983年时,程序应当自动算出员工的年龄为DateTime.Now- DateTime(1983,10,20), 当输入员工的学历为本科时,它在公司的组织架构级别中,就自动对应22B,而不是大专毕业对应的21B。
参考一开始设计的图,程序从一开始就是界面和逻辑分离的,逻辑直接写到BusinessLogical项目中去,也就是代码自动生成的EmployeeEntity文件。
这里有一个小技巧,代码生成器会为我们生成2个EmployeeEntity文件,一个是EmployeeEntity.cs, 另一个是EmployeeEntity.Logical.cs的文件,前一个文件,是依据数据表的字段,自动生成的文件,后一个文件,是存放自定义逻辑的地方,也就是需要放入逻辑的代码文件。
2. 程序界面上的特殊要求,通过以上生成的代码肯定没有做到。
比如,需要给员工主档添加图片显示,如图中,显示James Bond的图片,这个功能需要手动写。
其次,要给物料主档添加条形码支持,将生成的条形码作为物料编码的依据,这个功能也需要手写。
再比如开篇中的那张图片,Employee No.后面的按钮,是用来给员工智能编码用的。一般会依据员工学历,工作经验,专业的不同来产生相应的员工编码。
3. 逻辑和界面严格分离带来的一些问题。
如果要在逻辑里面根据不同的条件,给用户一个提示,这个会相当麻烦。因为逻辑与界面已经严格分离,绝不允许在逻辑中调用MessageBox.Show, 为了阻止这个行为的发生,在逻辑的References中,直接删除了System.Windows.Forms的引用。
同理,在界面中访问逻辑中的部分代码片段也相当麻烦,有时候在界面中也需要直接访问数据库,比如,在做销售送货时,根据当前用户输入的销售单号,判断这个SO是否已经完成送货,如果已经完成送货,则不允许产生送货单,这个逻辑只有在界面上实现,因为在BusinessLogical中Shipment对象还没有产生,无法做判断。
4. 性能 Performance
在把数据从数据库读到界面时,ORM和ADO.NET性能区别不大,但是,在做保存和更新时,ORM会判断当前已经做出修改的数据,仅仅生成这个被修改数据的UPDATE语句,这样的性能提升是很占优势的。
ADO.NET中,更新员工的语句一般是
UPDATE Name=’James Bond’ , Country=’ England’ WHERE EmployeeNo=’007’
很少可能会这样写,为了更新员工的名字,写一个SQL
UPDATE Name=’James Bond’ WHERE EmployeeNo=’007’
更新员工的国籍,于是又写一个SQL
UPDATE Country=’ England’ WHERE EmployeeNo=’007’
ORM框架会很高兴的帮忙我们判断,究竟有哪些数据被修改(dirty),并且只产生这些数据的UPDATE.
窗体规范
CodeSmith已经帮忙我们做好了大量的重复代码,也相当规范。
如果仍然要手写代码,遵守共同的规范,会使代码看起来更统一,规范
控件命名规范
控件类型 | 前缀 | 举例 |
Label | lbl | lblMessage |
Button | Btn | btnSave |
GroupBox | gbx | gbxMain |
GridView | grd | grdSalesOrderDetails |
仅有这个规范还不行,毕竟遵守起来还是有点困难,EPN框架还运用分词技术,自动为窗体中的控件重命名。
我们的窗体中的控件代码,看起来是这样
private EPN.WinUI.TextEditor txtDeptName;
private EPN.WinUI.Label lblDeptName;
private EPN.WinUI.PictureBox picImage;
private EPN.WinUI.GridView grdSalesOrder;
当为控件绑定属性时,自动为控件重命名。比如,我将TextBox1绑定到数据源EmployeeNo,于是控件被重命名为txtEmployeNo,看控件名称,就可以知道数据源,也知道控件类型,以达到高度的规范统一。
窗体的设计标准
1 DialogBox,对话框,一般要能响应ESC和Enter键,也有OK和Cancel2个按钮
2 窗体中控件的Tab order是否合理
3 Short cut key,快键键访问,所有的窗体也需要一致,也不能有冲突。
4 当窗体中需要盛放的数据量较大时,有2种思路
以GroupBox分组,以方便查看
当数据量再增多时,以Tab分组
5 窗体执行时间较长的任务,要使BackgroundWorker,以保证窗体可以接受响应,任务执行完毕后,或用MessageBox.Show显示successfully done,或与VS2010一样,在status bar中显示执行结果.比如在VS2010中成功check in代码后,在status bar中会显示成功签入变更集54498.
同时,cursor也不能放过,像这样写:
this.Cursor=Cursors.WaitCursor;
.....long operation
this.Cursor=Cursors.Default;
6 窗体组织方式,避免MDI forms,推荐tab式 MDI。
必要时,可以这样,把form放到panel中,设置form的TopLevel = false
7 ListView控件,要支持copy,而且可以多行copy
8 Enter和Tab key的作用,都会去下一个tab order的control
9 WinForms程序,最好的可以接受command line的启动方式,这样可以用bat命令启动。而且可以加启动参数。
VS2005、2008,2010都可以从command line启动。还记得常用的deven /resetpackage吗?
10 尽量阻止一台电脑,同时运行程序的两个实例,使用Singletion模式。
11 Target尽量是AnyCPU,而不x86。因为Microsoft Jet Access 数据库没有64bit的版本,宁愿移除对Jet Access的依赖和使用,也不愿把Target改成x86. 自Windows 7以来,64bit的OS已经相当普及,要使自己的程序不会一打开就stop working,就尽量编译成AnyCPU吧。
12 RichTextBox,如果是程序添加内容,需要auto scroll到最行的一行数据。观察VS2010编译的时候,Output窗口
13 对于用户不能修改的数据,不仅要使TextBox为readonly,最好还要使它的背景颜色变成gray,一目了然。
必须输入数据的控件,需要有个统一的提示,像这样
当该控件有值时,右边的红色图片hidden,没有值时,显示图片。这和Ajax中的watermark是相同的原理