2023年2月

在我的前面很多文章中,都介绍过通用附件模块的管理,本篇随笔主要介绍在一些应用模块中的列表展示中,包含附件的记录,在GridView列表界面中使用图标来快速显示是否有附件的情况。

1、通用附件模块的应用界面效果

例如在我的《通用人员信息管理模块》中(可以参考随笔《
Winform开发框架之通用人员信息管理
》、《
Winform开发框架之通用人员信息管理实现代码介绍
》,很多都使用了《通用附件管理模块》(可以参考《
Winform开发框架之附件管理应用
》来处理附件信息。如下界面所示。

但有时我们在列表中展示主要的数据的时候,我们希望知道哪一条记录是否包含附件,以便我们快速查看相关的附件的时候,如下所示的展示效果,我们需要怎么做呢?

2、具体实现代码

首先我们在绑定数据操作中增加一个字段列,用来在GridView列表中进行展示是否有附件的图标的,我们定义为 HasAttachment,然后判断附件列表中是否存在附件,并赋值给存在附件的列,具体逻辑代码如下所示。

        /// <summary>
        ///绑定列表数据/// </summary>
        public voidBindData()
{
//entity this.winGridViewPager1.DisplayColumns = "Seq,Category,Title,HasAttachment";#region 添加别名解析 this.winGridViewPager1.AddColumnAlias("Seq", "序号");this.winGridViewPager1.AddColumnAlias("Category", "科研情况类别");this.winGridViewPager1.AddColumnAlias("Title", "标题信息");this.winGridViewPager1.AddColumnAlias("AttachGUID", "附件GUID");this.winGridViewPager1.AddColumnAlias("HasAttachment", "附件信息");#endregion string where =GetConditionSql();
DataTable dt
= WHC.Framework.ControlUtil.BLLFactory<StaffResearch>.Instance.FindToDataTable(where);if (dt != null)
{
dt.Columns.Add(
"HasAttachment", typeof(Image));foreach (DataRow dr indt.Rows)
{
string attachGUID = dr["AttachGUID"].ToString();
Dictionary
<string, string> list = WHC.Framework.ControlUtil.BLLFactory<WHC.Attachment.BLL.FileUpload>.Instance.GetFileNames(attachGUID);if (list.Count > 0)
{
dr[
"HasAttachment"] =WHC.StaffData.Properties.Resources.attach2;
}
}
}
this.winGridViewPager1.DataSource =dt;this.winGridViewPager1.PrintTitle = "人员科研情况信息报表";
}

然后我们就是需要在GridView显示的时候,进行一些处理工作,如定义这个HasAttachment列的显示类型,并限定它的宽度等操作,以便进行更好的展示,如下代码所示。

            this.winGridViewPager1.gridView1.DataSourceChanged += new EventHandler(gridView1_DataSourceChanged);
        /// <summary>
        ///绑定数据后,分配各列的宽度/// </summary>
        private void gridView1_DataSourceChanged(objectsender, EventArgs e)
{
if (this.winGridViewPager1.gridView1.Columns.Count > 0 && this.winGridViewPager1.gridView1.RowCount > 0)
{
this.winGridViewPager1.gridView1.Columns["Seq"].Width = 50;this.winGridViewPager1.gridView1.Columns["Category"].Width = 50;this.winGridViewPager1.gridView1.Columns["Title"].Width = 200;

RepositoryItemPictureEdit pic
= newRepositoryItemPictureEdit();
pic.SizeMode
=DevExpress.XtraEditors.Controls.PictureSizeMode.Zoom;
pic.NullText
= " ";
pic.BestFitWidth
= 80;this.winGridViewPager1.gridView1.Columns["HasAttachment"].ColumnEdit =pic;this.winGridViewPager1.gridView1.Columns["HasAttachment"].MaxWidth = 100;this.winGridViewPager1.gridView1.Columns["HasAttachment"].MinWidth = 80;
}
}

完成上面的几步操作,我们就能在GridView中正常显示一个图标了,通过这个案例,我们拓展开来,可以在GridView中展示更好的效果,例如可以把一些字段的Image图形数据读取出来并展示出来等操作。

开发《
客户关系管理系统(CRM)
》软件过程,也就是一个标准的Winform程序的开发过程,我们可以通过这个典型的软件开发过程来了解目前的开发思路、开发理念,以及一些必要的高效率手段。本篇随笔主要介绍我在开发这个CRM客户关系管理系统过程中,所用到的一些开发工具,力求从开发工具的层面使大家对这个系统的形成过程有一个大致的了解。

在文章的开篇,我们先来聊几句。一直以来,我都知道,广州这个城市,在图书馆建设方面都做的很好,提供了很多公众的借阅服务,几年前也曾经在区一个小的图书馆里借阅过书籍,不过但是对整个过程没觉得什么特别。上个周末,去了广东省立中山大学图书馆(这个可能是广州最为完善、大型的图书馆)逛了一趟,发现借书的流程真是方便,随着科技工具的引入,一切都非常轻松和高效。曾经在大学年代,也是对图书热爱的年代,找一本书,用替书牌,东找西找找到几本满意的书籍后,拿去登记更是痛苦,排队到自己后,把每本书的借阅栏填好,折到条形码页面,给老师一本本扫描录入。但是那天去了中山大学图书馆,发现藏书很多不说,不用替书牌,找到一摞拿出来后,一并放到一个黑色板上,交上借阅卡,几秒钟就搞定。这一切也是工具引入的效率改进,因此,不管是日常生活,或者是软件的开发过程,以及更多的方方面面,好的工具引入,能给我们提供更快的速度,更高的效率。

言归正传,开发《
客户关系管理系统(CRM)
》软件过程,也用到了很多工具来辅助我们进行高效的软件开发,否则一个软件,可能需要更多的时间,更多的资源,更多的校验排错投入,本文以《
客户关系管理系统(CRM)
》为例,介绍在这个软件开发过程中用到的相关工具和开发思路,希望对大家有一定的借鉴作用。

1、软件的数据库表设计PowerDesigner

我们知道,做任何一件事情,都有一个开始的过程,对于常规的软件工程过程而言,一般开始是需要设计模型、流程等东西的,不过从我自身十多年的开发历程,先做UML的一般不多,而我们往往是通过数据库设计来完善整个模型。数据库设计也有很多工具辅助,不过我觉得使用PowerDesigner就已经很好了,这个设计工具从刚一开始使用,就觉得非常方便实用,所以这么多年也都是以它进行数据库表的设计工作的。

一般我们可以根据需要先在一个数据库类型上设计,如基于SqlServer的,还是Oracle的,或者是其他的,通常用这两种居多。数据库设计过程,可以指定表名称、字段名称(备注作为字段描述)、表之间的关系等方面。

如果我们已经有一些现成的数据库表,那么也可以通过反向工程的方式,把数据库表的设计导入到PowerDesigner上进行管理,也非常方便。

使用PowerDesigner设计的数据库表,非常方便维护,也方便修改,因为我们设计数据库,不可能一下就全部考虑的很好,可能需要多次反复的过程才能最终确定整个表的结构,使用这样的数据库设计工具,能给我们节省更多的修改时间,提高效率。

基于PowerDesigner的数据库设计,我们还可以更改数据库的类型,使之生成其他类型的数据库脚本,PowerDesigner本身也支持很多数据库类型,因此生成的脚本也具有更高的通用性,即使我们变换数据库的时候,数据库类型可能需要一定的调整,但是已经能节省大部分的时间了。

2、数据库工具

在我的《
客户关系管理系统(CRM)
》开发过程中,前期我一直使用SqlServer作为基础的数据库开发,因为它的方便和通用性,因为使用Microsoft SQL Server Management Studio来管理数据库非常方便,比很多数据库可能都方便一些,SqlServer用的人比较多,我也就不再赘述。

当然,我对自己开发的系统,一般都会考虑多种数据库的兼容性,以适应客户自己的部署环境要求,因此同时也会考虑Oracle、Sqlite等不同类型的数据库的兼容。对于Oracle数据库来说,一般都要求使用PLSQL Developer来进行数据库管理,这个工具弥补了Oracle本身的数据库表管理和维护的短板。

对于非常好用的单机版数据库SQLite,我们也需要一些管理工具来管理相关的表,我对比过很多SQLite的管理工具,我觉得SQLite Developer还是最好,使用起来最方便,功能也挺多。虽然一直觉得Sqlite Developer导入SqlServer的表的备注字段的时候有乱码这个问题外,其他都挺好,没有什么其他问题,我在之前有一些文章介绍过这个工具的使用:《
使用SQLite数据库和Access数据库的一些经验总结
》、《
在Winform开发框架中实现对数据库的加密支持
》。

Sqlite的数据库设计,我一般很少进行独立的设计,基本上都是从SqlServer数据库或者其他数据库进行导入,导入后,除了备注类型有乱码外,其他的基本没什么问题,因此用起来也非常方便。一般在我软件绝大多数功能快要完成的时候,就会增加这种数据库类型,然后进行一些功能上的调试。

对于另外一种Access数据库,如非历史原因,基本上可以使用Sqlite来替代它了。

3、代码生成工具Database2Sharp

代码生成工具,是综合体现一个软件开发过程的重要因素,因此选择好的代码生成工具,也意味着整体性的开发思路,以及开发效率方面都应该有所提高,而且尽可能贴近开发的实际要求。 如果基于演示原因或者简单的小软件,使用任何代码生成工具都可以满足要求,但是,实际上,开发一个成熟、稳定、美观的软件,是一个综合的过程,要求处处皆为开发者考虑才能使我们应用起来得心应手。

代码生成工具Database2Sharp(
http://www.iqidi.com/database2sharp.htm
)是我05年的时候开发起来的(之前还开发过一个SharpCode的代码生成工具),这么多年一直完善着、丰富着。坚持以自己为第一用户的原则,尽可能使自己的开发工作得到减轻,效率得到提高,并且综合各种最新的技术,封装好各种基类和继承关系,使得生成的框架代码更少,提供的支持更多,具有更好的扩展性,不用机械式的生成代码。通过全面的整合,可以一键生成一个完整的项目工程代码,生成后直接可以使用VS打开编译。

代码生成工具Database2Sharp本身是基于模板引擎NVelocity的技术,能够更方便调整和扩展,因此能够迅速响应我的开发要求,整合了整个解决解决方案的代码,同时包括了界面层的代码也可以快速生成。

4、基于现成的开发模块

我们做任何事情,都是继承性的,不可能一切都从头来过,我们人类的知识能继续丰富的原因,很大一个原因就是我们是基于原有的知识成果的基础上进行探索,进行研究的。做开发工作也一样,虽然微软给我们提供了很多丰富的.NET 的API,但是我们不可能什么都是从零开始,很多开发是有时间要求的,更有质量要求,基于现成的一些开发模块开发,也成为了我们的必然选择。

我自己本身一直在做软件的开发工作,很大部分都是基于.NET的开发,因此在吸收很多前辈的知识基础上,自己也逐渐积累了很多现成的模块,这些模块经过多个项目的反复利用,反复锤炼,都有着很高的稳定性、通用性,这些模块我在我的随笔分类《
Winform开发框架
》介绍的很多,其中包括了主体的Winform开发框架、通用权限管理系统、通用字典管理模块、Winform分页控件、程序自动更新模块、公用类库、附件管理模块、人员管理模块等等。这些模块组成一个生态体系, 不断的完善着整体的Winform开发框架,同时也为更多的项目应用提供支持,借用我很早的一篇随笔《
Winform开发框架之通用人员信息管理
》中的插图来进行说明。

在我的《
客户关系管理系统(CRM)
》这个系统里面,基本上使用到了上面的所有模块。首先是基于主体Winform开发框架进行各种界面的集成展示,每个模块同时可能有权限的集成,字典数据的使用,附件的管理,公用类库使用更能提高开发的效率,Winfrom分页用于数据的展示模块,使得内容更加合理的展示。

5、集成
开发
环境IDE Visual Studio

和Java的开发标准配置是
Eclipse的IDE
一样,.NET 领域都使用VS(Visual Studio)来进行开发,这个也是一站式的开发平台,我们可以使用它进行代码编辑,界面设计,调试等工作,应该做.NET的没有人不用它的了,这些顺带介绍一下,以突显它的重要性。同时,我们一般也使用VS来进行程序的打包工作,虽然VS2012起,集成使用了InstallShield来进行安装包的制作了,但是目前我还是主要用VS2010,暂不影响。

和VS配套使用,可能有一些类似Resharper的工具来提高开发效率,不过以目前IDE的智能程度,基本上也可以不用其他插件了。

6、代码对比工具Beyond Compare

除了VS这个庞大的IDE工具外,我们很多时候,还需要一些小工具来配合软件的开发。例如,我们开发的代码或者重新生成的代码,需要进行对比合并,那么这个时候 Beyond Compare就会非常好用,我经常性的使用这个工具,配合一些增量的修改工作,可以大大的提高合并的效率,减少出错机会的。

7、常用代码编辑工具EmEditor

代码编辑工具EmEditor也是VS开发工具的有效补充,这款文本编辑器的功能是非常强大的,特别是语法色彩加亮方面,可以支持很多类型的语法加亮显示,同时它又具有很多方便的功能,可以作为一个替换记事本最有潜力的工具了,有了这个工具,你可能再也不太想使用记事本那样低级的文本编辑工具了。

8、代码混淆工具.NET Reactor

在做软件开发的时候,可能会有很多人批评你没有开源,没有共享,其实,对于你花了大量精力,大量心血做出的一个系统,或者即使是一个小软件,都希望得到应有的尊重,而.NET不像C++的那样编译的方式,代码很容易被反编译出来,因此软件代码的混淆和保护就显得非常重要,对公司对个人都一样,除非你自己愿意分享奉献,否则一定的保护措施还是必要的。

用过好多款的代码混淆工具,有微软自带免费的Dotfuscator,也有很多商业收费的,最终觉得还是 .NET Reactor比较好用,方便高效,而且混淆的代码有不错的效果,虽然可能不会100%的安全,但是我觉得这款软件还是非常好的,一直以来都用这个进行一定的程序集保护处理。

9、图片处理工具Photoshop CS

图片的处理是我们需要掌握的一些必要的技能,即使对我们这样非专业的图形处理的程序员,具备一定的图形变换、涂改、修剪、设计工作也很有必要,即使做不出专业图形处理人员那样的效果特技,能够处理一般的图形要求就很不错了,我们在Winform开发过程中,有一些如启动界面,背景图片等,可能都需要经过一定的处理才能使用,那么就可以使用这个强大的图形处理工具Photoshop CS来做一些工作了。

10、虚拟机部署测试VMware Workstation

很多时候,如果你没有在一些干净(没有安装VS等开发工具的环境)系统上测试一款软件,你就不应该发给客户,即使你对它有足够的信心,也很难避免一些意外的情况,最好的措施就是自己先在干净的系统上进行测试,但我们不需要开着几个机器开做这样的事情,用虚拟机足够处理这些工作了。

虚拟机我常用的就是VMware Workstation,我16G的内存+SD硬盘,可以同时快速构建几个虚拟机了,但一般也不需要同时使用几个,同时用一两个足以,简单测试一下,如果正常启动运行,那就减少了很多错误的问题了。

虚拟机可以很方便的复制文件,也可以模拟网络环境,作为一个单独的机器使用,对于我们不常用的Oracle数据库,把它部署到虚拟机,偶尔测试使用,也不失为一个很好的办法的。

虚拟机的软件测试,可以减少一些如引用类库缺少,运行环境支持不够,路径配置错误等问题,我们把一个运行不起来的软件发给客户,而且客户抱怨我们还不知道具体原因是什么,那就显得太不专业了。

在前面介绍了很多CRM相关的界面和实现思路的随笔文章,本篇继续介绍一下系统中用到的一些经验和技巧片段。本篇随笔主要介绍客户关系管理系统(CRM)的报价单和销售单的处理界面效果,使用列表内置的选择代替弹出对话框选择产品的方式,实现报价单和销售单的产品列表快速选择。

1、功能界面效果展示

我们知道,在很多列表的内容输入中,很多都使用内置的列表选择方式代替弹出式对话框的选择方式,在我研究了解的很多个CRM系统中,报价单和销售单都基本上是采用这种内置的选择方式,为了适应业界的标准习惯,我在我的系统中也使用了这种方式,新建报价单界面
如下
所示。

新建销售订单界面如下所示。

报价单或者销售单,一般有状态的区分,为了方便展示各种状态,可以通过不同的背景色进行区分,并且单据通过分页控件进行分页展示,以达到更好的效率,如下所示。

报价单列表界面如下所示。

其中报价单可以直接生成销售订单,在报价单的列表上右键菜单弹出“转化为销售订单”即可实现该功能。

2、实现控件界面的设计步骤

首先我们介绍内嵌式的GridControl,实现实现数据的快速录入和选择的操作。在这个GridControl控件的设计视图里面,需要增加一些在界面上绑定的列,并绑定相关的FieldName字段。另外,我们把产品名称设计为可以通过弹出列表选择的方式,因此需要修改它的ColumnEdit编辑控件为SeachLookupEdit控件,如下界面所示的控件。

调整后的GridControl的设计界面如下所示。

然后需要在设计窗口的Repository选项卡界面中的In-place Editor Repository的设计模块里面进行调整,如下所示。

通过设置控件的显示成员(DisplayMemeber)和值成员(ValueMember)即可实现数据的显示和绑定。

3、实现控件界面的代码处理

通过第二步的界面配置,我们指定了GridControl的列表展示效果,但是真正需要运行起来,还需要通过代码进行数据的动态绑定和处理,才能正常使用。

    public partial classFrmEditQuotation : BaseEditForm
{
private QuotationInfo tempInfo = newQuotationInfo();/// <summary> ///所属客户ID/// </summary> public string CustomerID { get; set; }publicFrmEditQuotation()
{
InitializeComponent();
//绑定所有在用的产品信息 this.repositoryItemSearchLookUpEdit1.DataSource = BLLFactory<Product>.Instance.GetAllInUsed();//对控件列编辑控件的预处理 this.gridView1.ShowingEditor += newCancelEventHandler(gridView1_ShowingEditor);//控件列编辑控件的值变化的处理 this.gridView1.CellValueChanged += newDevExpress.XtraGrid.Views.Base.CellValueChangedEventHandler(gridView1_CellValueChanged);
}

另外,我们在绑定数据的时候,必须绑定GridControl的控件数据源,如下代码所示。

        /// <summary>
        ///数据显示的函数/// </summary>
        public override voidDisplayData()
{
InitDictItem();
//数据字典加载(公用) if (!string.IsNullOrEmpty(ID))
{
................................
}
else{
...............................
}
this.gridControl1.DataSource = GetDetailTable();//绑定订单详细列表 SetAttachInfo(tempInfo);
}
        /// <summary>
        ///绑定明细信息/// </summary>
        /// <returns></returns>
        publicDataTable GetDetailTable()
{
string columnString = "ID,ProductName,ProductNo,MaterialCode,ProductType,Specification,Model,Unit,SalePrice|decimal,Quantity|int,Note,ExpireDate|DateTime";
DataTable dt
=DataTableHelper.CreateTable(columnString);if (!string.IsNullOrEmpty(ID))
{
List
<QuotationDetailInfo> list = BLLFactory<QuotationDetail>.Instance.FindByOrderNo(this.txtHandNo.Text);
DataRow dr
= null;foreach (QuotationDetailInfo info inlist)
{
dr
=dt.NewRow();
dr[
"ID"] =info.ID;
dr[
"ProductName"] =info.Product_ID;
dr[
"ProductNo"] =info.ProductNo;
dr[
"MaterialCode"] =info.MaterialCode;
dr[
"ProductType"] =info.ProductType;
dr[
"Specification"] =info.Specification;
dr[
"Model"] =info.Model;
dr[
"Unit"] =info.Unit;
dr[
"SalePrice"] =info.SalePrice;
dr[
"Quantity"] =info.Quantity;
dr[
"Note"] =info.Note;if (info.ExpireDate > Convert.ToDateTime("1900-1-1"))
{
dr[
"ExpireDate"] =info.ExpireDate;
}
dt.Rows.Add(dr);
}
}
returndt;
}

其中注意上面的红色的代码,在产品名称的列上绑定的值是产品ID,而非产品名称即可。

另外,上面代码提到的
CellValueChanged事件是当列表选定后,在该行的其他列上显示数据的操作,如果没有选择,则清空相关的单元格内容,具体代码如下所示。

        private void gridView1_CellValueChanged(objectsender, DevExpress.XtraGrid.Views.Base.CellValueChangedEventArgs e)
{
if (e.Column.FieldName == "ProductName")
{
ProductInfo info
= BLLFactory<Product>.Instance.FindByID(e.Value.ToString());if (info != null)
{
this.gridView1.SetRowCellValue(e.RowHandle, "ProductNo", info.HandNo);this.gridView1.SetRowCellValue(e.RowHandle, "MaterialCode", info.MaterialCode);this.gridView1.SetRowCellValue(e.RowHandle, "ProductType", info.ProductType);this.gridView1.SetRowCellValue(e.RowHandle, "Specification", info.Specification);this.gridView1.SetRowCellValue(e.RowHandle, "Model", info.Model);this.gridView1.SetRowCellValue(e.RowHandle, "Unit", info.Unit);this.gridView1.SetRowCellValue(e.RowHandle, "SalePrice", info.SalePrice);
}
else{this.gridView1.SetRowCellValue(e.RowHandle, "ProductNo", DBNull.Value);this.gridView1.SetRowCellValue(e.RowHandle, "MaterialCode", DBNull.Value);this.gridView1.SetRowCellValue(e.RowHandle, "ProductType", DBNull.Value);this.gridView1.SetRowCellValue(e.RowHandle, "Specification", DBNull.Value);this.gridView1.SetRowCellValue(e.RowHandle, "Model", DBNull.Value);this.gridView1.SetRowCellValue(e.RowHandle, "Unit", DBNull.Value);this.gridView1.SetRowCellValue(e.RowHandle, "SalePrice", DBNull.Value);this.gridView1.SetRowCellValue(e.RowHandle, "Quantity", DBNull.Value);this.gridView1.SetRowCellValue(e.RowHandle, "Note", DBNull.Value);this.gridView1.SetRowCellValue(e.RowHandle, "ExpireDate", DBNull.Value);
}
}
}

最后就是在表单保存的时候,更新相关的主表数据后,接着保存相关的报价单明细或者订单明细即可,如下面的红色部分代码所示。

        /// <summary>
        ///新增状态下的数据保存/// </summary>
        /// <returns></returns>
        public override boolSaveAddNew()
{
QuotationInfo info
=tempInfo;
SetInfo(info);
info.Creator
=LoginUserInfo.Name;
info.CreateTime
=DateTime.Now;try{#region 新增数据 bool succeed = BLLFactory<Quotation>.Instance.Insert(info);if(succeed)
{
//可添加其他关联操作 SaveDetail();return true;
}
#endregion}catch(Exception ex)
{
LogTextHelper.Error(ex);
MessageDxUtil.ShowError(ex.Message);
}
return false;
}

4、列表单元格背景颜色变化的处理

刚才在1小节里面提到报价单或者销售单,一般有状态的区分,为了方便展示各种状态,可以通过不同的背景色进行区分,如下所示。

上面这种效果如何实现的呢,一般通过增加对RowCellStyle的处理即可,具体代码如下所示。

        void gridView1_RowCellStyle(objectsender, DevExpress.XtraGrid.Views.Grid.RowCellStyleEventArgs e)
{
if (e.Column.FieldName == "OrderStatus" || e.Column.FieldName == "HandNo")
{
//待处理 处理中 已完成 string status = this.winGridViewPager1.gridView1.GetRowCellValue(e.RowHandle, "OrderStatus").ToString();
Color color
=Color.White;if (status == "处理中")
{
e.Appearance.BackColor
=Color.Yellow;
e.Appearance.BackColor2
=Color.LightCyan;
}
if (status == "发货中")
{
e.Appearance.BackColor
=Color.Purple;
e.Appearance.BackColor2
=Color.LightCyan;
}
if (status == "已取消")
{
e.Appearance.BackColor
=Color.Red;
e.Appearance.BackColor2
=Color.LightCyan;
}
else if (status == "已完成")
{
e.Appearance.BackColor
=Color.Green;
e.Appearance.BackColor2
=Color.LightCyan;
}
}
}

另外附上该系列的相关文章链接,需要可以详细了解相关的内容。

Winform开发框架之客户关系管理系统(CRM)的开发总结系列1-界面功能展示

Winform开发框架之客户关系管理系统(CRM)的开发总结系列2-基于框架的开发过程

Winform开发框架之客户关系管理系统(CRM)的开发总结系列3-客户分类和配置管理实现

Winform开发框架之客户关系管理系统(CRM)的开发总结系列4-Tab控件页面的动态加载

客户关系管理系统(CRM)的开发过程中使用到的开发工具总结

Winform开发框架之客户关系管理系统(CRM)的报价单和销售单的处理

以上就是CRM系统模块里面的一些经验和技巧,希望对大家构建更好的Winform界面有所帮助和启发。

在很多项目里面,字典管理是必备的项目模块,而这个又是比较通用的功能,因此可以单独做成一个通用字典管理,例如这个模块,可以通过集成的方式,使用在我的
《Winform开发框架》
、《
WCF开发框架
》、《
混合式开发框架
》中,这样可以避免代码的重复维护管理,对企业来说,也可以更好管理核心的代码,实现统一的版本更替。通用的字典管理模块,主要包括了字典类型的管理、字典数据的管理,另外,为了方便,还可以加入批量添加数据的功能,以便更适合实际使用场景,加快字典数据的快速录入。
借着这个机会,把我的通用字典管理模块的内容大概介绍一下吧。

1、通用字典管理模块的数据管理

普通的字典模块管理如下所示

其中左边的是字典类型的树列表,可以通过右键菜单进行维护,如下界面所示。

批量插入的字典模块管理如下所示

2、全国省市地区的数据字典管理

在最近的项目中,由于需要使用到全国地区的省份、城市、区县这些新政区划的信息,网上的很多数据脚本都是早期的,因此花了一整天的时间,重新校对了这些省份、城市、地区的数据内容,以及编写一个对全国省市地区的数据字典管理模块,由于这些模块的数据是属于数据字典的范畴,因此把这些新政区划的管理也整合通用字典模块里面,以方便更好的应用。

这些全国的新政区划数据,最权威的数据当然来自国家统计局了(
http://www.stats.gov.cn/tjbz/xzqhdm/t20130118_402867249.htm
),从里面的数据可以看出,这两三年,全国的省份信息基本没有变化,但是城市、区县的数据变化还是不少,如调整了三亚,增加了三沙市,以及合并一些省份的城市,广州等城市的区县也有所变化等等,因此花了不少精力时间来对这些数据进行整理,希望能够给自己方便使用的同时,也方便需要用到这些省份城市行政区的开发人员。

全国地区的省份、城市、区县这些新政区划的管理界面如下所示

继承我通用字典管理的功能,在城市行政区维护的功能上,也增加了一个批量插入数据的操作,以方便更好的数据增加需要(虽然我们很少调整这些城市行政区数据)。

3、如果使用通用字典模块的行政区划数据

上面的字典管理界面只是提供了方便对数据的维护,在实际项目模块中,我们需要增加对省份、城市、区县地区的联动选择支持,如我在《
客户关系管理系统CRM
》中,客户数据的编辑界面如下所示。

首先需要在绑定省份的字典数据,如下代码所示。

        /// <summary>
        ///初始化数据字典/// </summary>
        private voidInitDictItem()
{
this.txtProvince.Properties.BeginUpdate();this.txtProvince.Properties.Items.Clear();
List
<ProvinceInfo> provinceList = BLLFactory<Province>.Instance.GetAll();foreach (ProvinceInfo info inprovinceList)
{
this.txtProvince.Properties.Items.Add(newCListItem(info.ProvinceName, info.ID.ToString()));
}
this.txtProvince.Properties.EndUpdate();

.....................

}

然后在省份、城市的下拉列表控件的事件中实现其他行政区划数据的显示即可。

        private void txtProvince_SelectedIndexChanged(objectsender, EventArgs e)
{
CListItem item
= this.txtProvince.SelectedItem asCListItem;if (item != null)
{
string provinceId =item.Value;this.txtCity.Properties.BeginUpdate();this.txtCity.Properties.Items.Clear();
List
<CityInfo> cityList = BLLFactory<City>.Instance.GetCitysByProvinceID(provinceId);foreach (CityInfo info incityList)
{
this.txtCity.Properties.Items.Add(newCListItem(info.CityName, info.ID.ToString()));
}
this.txtCity.Properties.EndUpdate();
}
}
private void txtCity_SelectedIndexChanged(objectsender, EventArgs e)
{
CListItem item
= this.txtCity.SelectedItem asCListItem;if (item != null)
{
string cityId =item.Value;this.txtDistrict.Properties.BeginUpdate();this.txtDistrict.Properties.Items.Clear();
List
<DistrictInfo> districtList = BLLFactory<District>.Instance.GetDistrictByCity(cityId);foreach (DistrictInfo info indistrictList)
{
this.txtDistrict.Properties.Items.Add(newCListItem(info.DistrictName, info.ID.ToString()));
}
this.txtDistrict.Properties.EndUpdate();
}
}

4、行政区划sql数据脚本

全国地区的省份、城市、区县这些新政区划的最新Sql脚本下载地址如下所示,希望对大家有帮助。

https://files.cnblogs.com/wuhuacong/CityDistrict.rar

上面的省市区县的行政区划脚本数据,是我通过花了几乎一天的时间进行整理核对的,数据来源于国家统计局2013最新的行政区划数据,绝对有保证,如果对你有帮助,请顺手推荐一下,让更多的人受益,谢谢支持。

最近一直在做一些技术性的研究和框架改进工作,博客也落下好几天没有更新了,也该是时候静下心来,总结这段时间的一些技术改进的经验了。和上一阶段的CRM系统开发和技术研究一样,我都喜欢在一个项目或者模块完成后,做一些相关的总结性工作,记录下前一阶段的技术脚印,希望给自己留下一个脚印快照,同时给读者了解自己的技术动向外,也有所收获。本随笔主要介绍在下拉列表中展示一个列表,以便实现数据结构的良好展示,并能快速选定所需的节点,这个就是TreeListLookupEdit控件的使用。

1、界面效果展示

首先我们来看看我的Winform开发框架之权限管理系统模块改进完善后的主界面,然后在介绍其中用到的功能点,以及技术实现。

系统用户信息管理界面如下所示。

其中用户信息编辑界面如下所示。

其中编辑用户信息的界面包括了所属公司、所属部门、直属经理三个输入的内容,为了减少数据量的显示,这几个输入框是通过级联的方式进行展示,也就是说,先选定所属公司,然后在所属公司中列出该公司的部门列表,选定部门后,再通过获取指定部门的人员信息,作为直属经理的人员展示。

了解这些逻辑关系后,我们来看看所属公司的列表展示,如下所示。

选定了所属公司后,所属部门就根据公司来进行过滤了,如下所示。

通过公司和部门的条件,我们就可以列出有限的人员列表,作为直属经理的人员列表了,这个列表使用普通的下拉列表展示即可,在此不再赘述。

以上的树状结构的下拉列表,在DevExpress控件中是通过TreeListLookupEdit控件进行实现的。

2、基于DevExpress控件的功能实现及代码展示

由于这些所属公司、所属部门的功能模块,一般需要不少代码进行处理,为了更好重用这些模块,我们通过用户控件的封装方式进行,然后在数据编辑界面上,通过拖动控件方式即可实现布局,并只需要设置或者访问某个属性即可,封装的用户界面控件如下所示。

1)所属公司控件代码

所属公司是一个用户控件,让其继承自XtraUserControl即可,为了在选择内容后触发值的变化事件,我们定义了一个事件EventHandler EditValueChanged,这样我们在内部控件的值变化的时候,可以通知外部的界面进行处理。

    public partial classCompanyControl : XtraUserControl
{
/// <summary> ///选择的值发生变化的时候/// </summary> public eventEventHandler EditValueChanged;publicCompanyControl()
{
InitializeComponent();
this.txtCompany.EditValueChanged += newEventHandler(txtCompany_EditValueChanged);
}
void txtCompany_EditValueChanged(objectsender, EventArgs e)
{
if (EditValueChanged != null)
{
EditValueChanged(sender, e);
}
}

为了实现列表数据的绑定,以及增加图标作为区分节点,TreeListLookupEdit控件的数据绑定操作是这个控件的核心所在。

在下面的代码逻辑里面,我们通过获取公司列表,然后把它转换为一个DataTable,并根据集团、公司的层次给予不同的图标,绑定数据,并设置好控件的KeyFieldName、ParentFieldName、ValueMember、DisplayMember几个重要属性即可,如下所示。

        /// <summary>
        ///初始化数据/// </summary>
        public voidInit()
{
DataTable dt
= DataTableHelper.CreateTable("ImageIndex|int,ID,PID,Name");
List
<OUInfo> list = BLLFactory<OU>.Instance.GetGroupCompany();
DataRow dr
= null;foreach (OUInfo info inlist)
{
dr
=dt.NewRow();
dr[
"ImageIndex"] =Portal.gc.GetImageIndex(info.Category);
dr[
"ID"] =info.ID.ToString();
dr[
"PID"] =info.PID.ToString();
dr[
"Name"] =info.Name;
dt.Rows.Add(dr);
}
//设置图形序号 this.treeListLookUpEdit1TreeList.SelectImageList = this.imageList2;this.treeListLookUpEdit1TreeList.StateImageList = this.imageList2;this.txtCompany.Properties.TreeList.KeyFieldName = "ID";this.txtCompany.Properties.TreeList.ParentFieldName = "PID";this.txtCompany.Properties.DataSource =dt;this.txtCompany.Properties.ValueMember = "ID";this.txtCompany.Properties.DisplayMember = "Name";
}

为了方便编辑界面中,对所属公司的赋值与获取操作,我们需要增加一个Text属性和一个Value属性。我们需要重载Text属性,用来获取或设置所属公司的名称;添加一个Value属性,用来获取所属公司的ID,如下所示。

        /// <summary>
        ///公司名称/// </summary>
        public override stringText
{
get{return this.txtCompany.Text;
}
set{this.txtCompany.Text =value;
}
}
/// <summary> ///公司ID/// </summary> public stringValue
{
get{string result = "-1";if (this.txtCompany.EditValue == null || this.txtCompany.EditValue.ToString() == "0")
{
result
= "-1";
}
else{
result
= this.txtCompany.EditValue.ToString();
}
returnresult;
}
set{this.txtCompany.EditValue =value;
}
}

2)所属部门的控件代码

所属部门的代码逻辑和所属公司差不多,唯一不同的是,所属部门需要一个上级的部门标识(公司ID)作为数据的过滤获取,如下所示。

        /// <summary>
        ///初始化部门信息/// </summary>
        public voidInit()
{
//InitUpperOU DataTable dt = DataTableHelper.CreateTable("ImageIndex|int,ID,PID,Name,HandNo,Category,Address,Note");
DataRow dr
= null;if (!string.IsNullOrEmpty(ParentOuID))
{
List
<OUInfo> list = BLLFactory<OU>.Instance.GetAllOUsByParent(ParentOuID.ToInt32());foreach (OUInfo info inlist)
{
dr
=dt.NewRow();
dr[
"ImageIndex"] =Portal.gc.GetImageIndex(info.Category);
dr[
"ID"] =info.ID.ToString();
dr[
"PID"] =info.PID.ToString();
dr[
"Name"] =info.Name;
dr[
"HandNo"] =info.HandNo;
dr[
"Category"] =info.Category;
dr[
"Address"] =info.Address;
dr[
"Note"] =info.Note;
dt.Rows.Add(dr);
}
}
//增加一行空的 dr =dt.NewRow();
dr[
"ID"] = "0"; //使用0代替-1,避免出现节点的嵌套显示,因为-1已经作为了一般节点的顶级标识 dr["PID"] = "-1";
dr[
"Name"] = "";
dt.Rows.InsertAt(dr,
0);//设置图形序号 this.treeListLookUpEdit1TreeList.SelectImageList = this.imageList2;this.treeListLookUpEdit1TreeList.StateImageList = this.imageList2;this.txtDept.Properties.TreeList.KeyFieldName = "ID";this.txtDept.Properties.TreeList.ParentFieldName = "PID";this.txtDept.Properties.DataSource =dt;this.txtDept.Properties.ValueMember = "ID";this.txtDept.Properties.DisplayMember = "Name";
}

3)主界面的控件使用

封装好所属公司和所属部门的控件后,我们就可以通过在工具箱里面拖动控件到主编辑界面里面去了,这样我们只需要简单对控件进行设置和赋值即可实现,减少对控件逻辑过多的代码处理。

数据编辑界面中,控件的使用代码如下所示。

a)界面赋值操作

                    this.txtCompany.Value =info.Company_ID;this.txtDept.Value = info.Dept_ID;

b)界面数据获取操作

            info.Dept_ID =txtDept.Value;
info.DeptName
=txtDept.Text;
info.Company_ID
=txtCompany.Value;
info.CompanyName
= txtCompany.Text;

c)界面事件的处理操作

由于界面上两个控件是级联的关系,因此需要处理他们控件的值发生变化的事件,如下所示。

    public partial classFrmEditUser : BaseEditForm
{
publicFrmEditUser()
{
InitializeComponent();
this.txtCompany.EditValueChanged += new EventHandler(txtCompany_EditValueChanged);
this.txtDept.EditValueChanged += new
EventHandler(txtDept_EditValueChanged);
}
void txtCompany_EditValueChanged(objectsender, EventArgs e)
{
if (!string.IsNullOrEmpty(this.txtCompany.Value))
{
//赋值给部门控件,并重新初始化部门列表 txtDept.ParentOuID = this.txtCompany.Value;
txtDept.Init();

}
}
void txtDept_EditValueChanged(objectsender, EventArgs e)
{
if (!string.IsNullOrEmpty(txtDept.Value))
{
//获取所属部门后,就通过部门ID来初始化直属经理的人员列表 InitManagers(txtDept.Value.ToInt32());
}
}
.................

3、基于传统Winform界面的控件实现

上面的介绍内容是基于DevExpress的控件组进行功能实现的,有些人可能会问,我使用传统样式的界面控件,这种效果如何实现呢,其实如果是使用内置的控件,是比较困难实现这个效果的,但是这样的界面控件有很多人开发好组件可以利用的,我在做这个模块的时候,也考虑到了这一点,因此也针对这些做了一些搜索查询,发现国外有一个同行封装的控件做的非常不错,地址是
http://www.brad-smith.info/blog/archives/477
,上面的效果基本上没问题的。这个人的博客和下载的例子里面,没有使用Demo的代码,我自己根据控件的属性进行了一个Demo例子的编写,效果和代码如下所示。

总体来说,这个控件实现的效果还是非常不错的,例子代码如下所示,其节点和TreeView的操作类似,通过ComboTreeNode对象进行添加即可。

        private void Form1_Load(objectsender, EventArgs e)
{
if (!this.DesignMode)
{
this.comboTreeBox1.Nodes.Clear();

ComboTreeNode parentNode
= newComboTreeNode();
parentNode.Text
= "爱奇迪集团";
parentNode.Nodes.Add(
"gz", "广州分公司");
parentNode.Nodes[
"gz"].ImageIndex = 1;//通过名字引用 parentNode.Nodes["gz"].ExpandedImageIndex = 1;//通过名字引用 ComboTreeNode bjNode= newComboTreeNode();
bjNode.Name
= "bj";
bjNode.Text
= "北京分公司";
bjNode.ImageIndex
= 1;
bjNode.ExpandedImageIndex
= 1;
bjNode.FontStyle
=FontStyle.Bold;

bjNode.Nodes.Add(
"财务部");
bjNode.Nodes.Add(
"市场部");
bjNode.Nodes.Add(
"人力资源部");

parentNode.Nodes.Add(bjNode);
this.comboTreeBox1.Nodes.Add(parentNode); comboTreeBox1.ExpandAll();
}
}

除了这个可以实现传统界面效果的下拉树展现外,CodeProject上的
http://www.codeproject.com/Articles/25471/Customizable-ComboBox-Drop-Down
这篇文字实现的效果也还不错。

不过可能没有上面的那个效果好一点。

4、总结

以上这些就是关于级联下拉列表,并在下拉列表里面展示层次关系的树形结构的功能实现和核心代码的操作,本随笔介绍了基于DevExpress样式和传统样式两种方案的实现过程,重点对DevExpress控件中的TreeListLookupEdit控件的实现进行介绍,希望大家能够在实际工作中用上。