2023年2月

在Winform程序开发中,界面部分的开发工作量一般是比较大的,特别是表的字段数据比较多的情况下,数据编辑界面所需要的繁琐设计和后台逻辑处理工作量更是直线上升,而且稍不注意,可能很多处理有重复或者错误,利用代码生成工具Database2Sharp设计数据编辑界面可以极大提高开发效率,减少出错机会,结合DevExpress的布局拖动调整,更是如虎添翼,使你从繁琐的界面设计开发工作中解脱出来,缺少的只是你的灵感。

1、数据编辑界面设计步骤

一般情况下,如无其他工具的辅助下,我们需要在界面上放置好相关的界面控件,并调整控件的名称,调整控件位置布局,在后台中逐个编写控件赋值的显示代码和保存到数据库的赋值语句,特别是控件命名、显示数据,保存数据这些操作,是非常繁琐累人的,也是最容易出错的地方。

利用代码生成工具Database2Sharp,除了可以快速生成整体Winform开发框架的界面代码,让你一秒钟内就搭建一个完整标准的框架应用,最重要的,Winform界面设计这些繁杂的工作,也可以使用它进行完成,再也不用拘束于重复的劳动中了,可以静下心来找找灵感了。代码生成工具Database2Sharp的Winform界面代码生成界面如下所示。

查询类和数据编辑类,基本上是我们设计界面的两种标准界面,如下所示。

由于这个主查询界面,利用分页控件,以及动态生成的条件基本上就能满足要求了,繁琐的工作主要在数据编辑界面上,本文主要介绍数据编辑界面的设计工作在利用代码生成工具
方面
的改进。

一般情况下,我们利用代码生成工具生成的界面都是需要调整的,但是界面的控件名称已经根据字段进行了对应,后台的数据显示、输入验证、数据保存等需要控件和实体类一一对应的关系,也已经做好了,所做的只需要把界面调整的更加好看即可。

所以,生成的界面大致情况如下所示。

虽然生成的编辑界面,字段说明作为控件的标签,字段名称作为控件名称的部分内容,并且日期和数值控件类型也已经调整好,但是离我们真正的呈现给用户的数据编辑界面还是有一定的距离。

不要紧,下面我来演示一下如何实现界面的进一步的调整。

首先,我们知道,DevExpress的LayoutControl布局控件,对于其中的控件位置,都可以在设计模式进行动态拖动调整的,如下所示。

于是,我们可以根据合理的排版布局,对其中的界面元素拖动到合理的布局,并增加一些空白的控制位置,或者调整布局单元的约束类型为自定义,并设置控件的最大宽度或者高度,这样可以合理调整好数据编辑界面。

另外,我们可以在设计模式下,控件的右上角上,调整控件为自己希望的类型,如下所示。

通过上面的多样化调整,我们可以最终把界面拖动设计成这个效果。

2、多选项卡的数据编辑界面

按照上面这样的调整流程,对于一般情况下的数据编辑界面,我们可能基本满足了要求了,但是对于一些字段输入比较多的表格,我们最好能够通过TabControl把不同范畴的输入内容分开,一个选项卡负责一部分数据,这样可以有效利用屏幕的空间进行输入。例如,上面的数据,通过引入一个TabControl, 把病人基本信息和随访信息分开,就可以得到这样的运行界面效果。

或者更多数据的人员信息的数据编辑界面效果如下所示,通过分门别类,我们可以有效管理看似很凌乱的数据内容了。

但是,这样多选项卡的数据编辑界面,我们可能无法一下就生成符合要求的界面效果,我们可以先整体生成一个全部所需字段的数据编辑界面,就是按照上面的流程一样生成,然后重新复制一份到一个临时的窗体里面作为备份保留。

在原来的界面上,删除除第一个选项卡页面的控件内容,这时候,我们知道,后台的对应代码还保留着这些删除界面控件的数据显示、赋值保存的对应关系的代码,我们要保留它们,因为我们不是真的删除这些关联的控件。

然后在TabControl中,创建第二个TabPage选项卡页面,然后在备份保留的窗体界面上,把第一个TabPage选项卡页面中存在的控件删除,然后把剩下的其他控件,全部通过LayoutControl方式复制到第二个TabPage选项卡页面里面,这样整个控件和后台代码的关系,就又完整了,如果有更多的控件需要拆分,按照上述的方法重新操作一次即可。

例如,下面的例子源自我的客户关系管理系统模块(CRM)中的联系人管理,由于联系人信息比较多,我把它分为了两个TabPage选项卡页面进行管理,操作的思路就是用刚才介绍的方式进行调整的。

一直以来,都希望整合一个以客户为中心的平台,有两个方面的考虑:一是实现客户数据、客户关系、客户管理等方面的整合,以便更好利用好客户的相关资源,发挥最大的营销效益;二是整合目前我的开发框架的所有模块和技术,包括权限管理和控制、字典管理模块、分页控件、公用类库、自动更新模块、附件管理模块、邮件短信的信息通知模块、工作流模块,以及来电显示记录模块、群发邮件营销模块、日历管理模块等方面的内容。本随笔系列主要介绍在开发我的CRM系统中的各种场景问题的解决思路,以及相关的功能实现,力求在逐步完善这个CRM系统模块过程中,介绍我目前基于我的Winform开发框架套件和Database2Sharp代码生成工具的CRM系统的开发历程和经验总结,希望记录下的点点滴滴,对自己,对同行都有着一定的借鉴、参考作用,同时也希望吸纳更多的批评指导意见,逐步提高自己整个Winform开发框架体系的深度和广度。

1)CRM系统功能的借鉴和设计

在实际开发之初很长一段时间,都很留意CRM系统的相关信息,包括从华军、天空网站下载了一些不错的CRM系统,以及一些急于B/S的在线CRM系统,吸收其中的一些思路和功能,同时也在Google、Baidu上搜索一些别人设计文档或者介绍文档,了解整个CRM系统可能包含的模块和关联的关系。

主要的功能模块包括:客户管理、服务管理、商品及销售、报表数据等几大块。

其中客户管理包括的内容有:客户管理、客户跟进、联系人管理、客户文档、合同文档、维护记录、售后服务、客户投诉、客户活动、销售机会、产品报价、销售记录、客户拜访、客户来电、发票记录、客户分组、联系人分组等方面的管理,基本上就是,以客户为中心的相关数据的管理。当然,后面还可以扩展添加,客户往来邮件、客户往来短信,以及和客户相关等各方面的数据。例如下面是我设计的客户往来的总体界面,包括了一切和客户相关的模块展示,主界面源自我的《
Winform开发框架
》的布局效果,整合了主体框架的内容,在主界面里面,也整合了Winform分页控件实现数据的合理高效的展示,并且通过把界面拆分多个可以调整的部分,使得更加友好。

2)客户分类管理

a、常规字典模块的动态加载

一般情况下,我们对客户的分类都需要动态加载,对这个客户分类的管理,我希望能给使用者提供足够的方便,如下面的客户状态、客户类型等节点里面的数据来源数据字典,用户调整对应字典模块的数据,这个树形列表也应该跟着变化。


上面的节点数据来自数据字典模块的内容,字典模块直接利用《
Winform开发框架
》现成的模块即可,界面如下所示。

b、客户省份、客户城市的动态加载

除了从数据字典中加载的节点数据,还有一种如客户省份、客户城市,我们知道这些数据很大,我们如果在树列表里面展示全国的城市,那么肯定是不好的用户体验,想想要在全国几百个城市找一个出来可不容易。

于是,我们应该换一种方法,试着从已有客户所在的省份、所在的城市,把他们动态加载出来,数据就少很多,友好很多,基本上我们看任何一个节点都会有数据,这就是贴心的设计,友好的体验。

以下就是从客户数据里面提取客户所在省份、客户所在城市,然后再在节点中列出。


c、客户分组的管理

除了上面两种,还有一种来自个人的客户组别的数据表数据,我们从其中获取到对应的客户分组信息,然后在客户分组节点中展示出来,选择对应的个人分组就可以获取对应的客户

上面的个人分组来自对客户的个人分组表里面,它的管理界面如下所示。

d、客户分类的配置管理

从上面对客户的分类,我们看到已经有很多大的类别了,每个类别展开还有好几项,这样就构成了一个很大的树,但是有时候有些客户可能不一定对所有的分类节点都感兴趣,如果能够给客户一个选择配置的机会,会显得更加友好,因此客户分类的功能需求也就应运而生,我们把这些预先设定一个系统节点表,然后每个用户存储在另外一张用户表里面,这样就可以实现了。


3)客户管理模块的动态加载

以客户为中心的数据管理,就会分为很多名目, 如上面提到的客户管理、客户跟进、联系人管理、客户文档、合同文档、维护记录、售后服务、客户投诉、客户活动、销售机会、产品报价、销售记录、客户拜访、客户来电、发票记录。

管理这些模块,我们在开发的时候,如果在一个TabControl集成那么多模块,也是可以的,不过不够弹性化,比较僵硬死板,而且可能不好动态调整,如果整合一起,字段控件的命名也成问题。

因此,我设计这些模块的时候,每个业务模块作为一个控件,然后再在统一在界面中动态整合,这样还可以实现本节所说的动态加载和配置管理,这里面也用到了Winform分页控件模块,使用代码生成工具Database2Sharp实现所有子模块的列表展示和数据编辑界面的快速开发,具体可以参考上一篇随笔《
利用代码生成工具Database2Sharp设计数据编辑界面
》。

和上面的客户分类一样,我觉得不是每个人都可能关注所有的相关资料,有些可能只需要关心几项内容,那么应该提供一个模块配置的界面给客户进行必要的调整,配置界面如下所示

4)数据编辑界面的设计

处理上面的分类设计要求,对每项数据的编辑界面,我也希望能给设计符合一般用户的使用习惯,如下面的客户信息的数据编辑界面和联系人数据编辑界面效果。这里面的界面效果,是使用代码生成工具Database2Sharp实现列表展示和数据编辑界面的快速开发,然后进行适当的调整,具体可以参考上一篇随笔《
利用代码生成工具Database2Sharp设计数据编辑界面
》。

客户联系人数据编辑界面如下所示。

以上就是这个CRM系统模块里面,涉及到的一些比较典型的界面设计,本篇不设计技术代码的研究,后续的文章将对这些设计的细节和实现思路进行分析介绍,希望大家多多支持,提供更多的批评意见,相互促进。

我的该CRM系统系列的几篇随笔链接如下,供阅读。

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

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

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

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

在上篇随笔《
Winform开发框架之客户关系管理系统(CRM)的开发总结系列1-界面功能展示
》中介绍了我的整个CRM系统的概貌,本篇继续本系列的文章,介绍如何基于我的《winform开发框架》的基础上进行CRM系统模块的开发工作,希望对大家在系统模块开发有所启示或者帮助。

在我整个开发框架的体系结构中,我都希望开发的业务模块尽可能重用,因此遵循这个要求,所有的模块除了一些基础模块外,尽可能和其他业务模块没有任何耦合关系,同时也可以动态对模块进行加载使用,和我在《
Winform开发框架之插件化应用框架实现
》的思想一样,各个模块之间可以动态组合起来,实现更多的业务整合。

1、CRM系统的工程项目介绍

本客户关系管理系统,也是基于这个目的和基础上进行模块开发,在整个项目模块开发过程中,将会利用到整个Winform开发框架的相关组件模块,包括基础界面模块、程序启动模块、
权限管理模块、字典管理模块、分页控件、公用类库、附件管理等公用模块。

整个CRM系统的界面效果如下所示。

首先我们来看看CRM系统主要项目工程的布局和说明。

解决方案项目工程介绍
WHC.Framework.BaseUIDx 重用模块。
各模块的界面基类,包括通用窗体界面基类BaseUI;通用高级查询模块AdvanceSearch;通用Excel导入模块;插件化接口相关类PlugInInterface等。
WHC.Framework.StarterDx 重用模块。
插件化应用程序启动界面。通过用户登录后,获取用户的菜单,动态创建菜单并加载插件,该模块集成权限管理系统模块、集成字典数据管理模块、集成多文档界面控件和布局控件、集成美观实用的登陆界面、闪屏展示界面、托盘缩小提示功能、全局运行一次实例限制模块代码等内容。
WHC.CRM.Core 新开发模块。
客户关系管理系统的核心模块,包括业务处理、数据接口层、数据访问层、实体层等相关类。
WHC.CRM.UIDx 新开发模块。
客户关系管理系统的界面层,包括所系统模块的各种界面、控件等内容。

设计好CRM的相关数据库表后,利用C#代码生成工具Database2Sharp生成框架各层的代码,模块开始开发的时候,可以一次性把所有业务表的代码一次性生成,然后在整个新的解决方案(.sln)上进行递增式完善即可,如果是后续模块的开发,则需要增量把生成的代码,复制到相关的框架目录即可,整理后的业务逻辑层代码结构如下所示

这个时候,我们生成了界面层以下的所有分层代码,整个代码生成后,一次性即可编译通过,界面层我们另外建立一个Winform项目工程
WHC.CRM.UIDx
,然后添加相关的界面引用程序集(如DevExpress的相关界面程序集)。处理完这些后,我们又可以利用C#代码生成工具Database2Sharp来实现界面的快速开发工作了,代码生成工具生成界面的操作界面如下所示,具体生成界面的操作可以参考随笔《
利用代码生成工具Database2Sharp设计数据编辑界面
》进行了解。

最后得到类似项目目录结构的CRM系统界面模块工程。

由于整个CRM系统包含很多界面元素,因此以上模块的界面部分只是其中一部分,如果内容较多,可以建立目录进行分类管理,这样会更加清晰。

2、CRM系统的界面层代码分析

利用C#代码生成工具Database2Sharp,可以快速生成所需要的框架界面代码,包括集成各种已有模块的界面基类、导入导出模块支持、高级查询能功能模块,各种实体类对应关系等内容,这些如果利用手工操作,效率非常低下,而且容易出错。即使利用一些代码生成工具,如果没有和现成的界面模块进行很好的整合,也需要花费大量的时间进行整理,下面通过几个界面代码的展示进行大致的了解。

1)列表显示界面的集成和分页整合

2)字典模块的整合处理(通过扩展类方法实现)

3)导入导出模块的整合

        private string moduleName = "客户合同信息";/// <summary>
        ///导入Excel的操作/// </summary>          
        private void btnImport_Click(objectsender, EventArgs e)
{
string templateFile = string.Format("{0}-模板.xls", moduleName);
FrmImportExcelData dlg
= newFrmImportExcelData();
dlg.SetTemplate(templateFile, System.IO.Path.Combine(Application.StartupPath, templateFile));
dlg.OnDataSave
+= newFrmImportExcelData.SaveDataHandler(ExcelData_OnDataSave);
dlg.OnRefreshData
+= newEventHandler(ExcelData_OnRefreshData);
dlg.ShowDialog();
}
void ExcelData_OnRefreshData(objectsender, EventArgs e)
{
BindData();
}
boolExcelData_OnDataSave(DataRow dr)
{
string customerName = dr["客户名称"].ToString();if (string.IsNullOrEmpty(customerName))return false;

CustomerInfo customerInfo
= BLLFactory<Customer>.Instance.FindByName(customerName);if (customerInfo == null)
{
throw new ArgumentException(string.Format("客户名称【{0}】不存在,记录已跳过", customerName));
}
bool success = false;bool converted = false;
DateTime dtDefault
= Convert.ToDateTime("1900-01-01");
DateTime dt;
ContractInfo info
= newContractInfo();
info.Customer_ID
= customerInfo.ID;//客户ID info.HandNo = dr["合同编号"].ToString();
info.ExpenditureType
= dr["收支类型"].ToString();
info.ContractType
= dr["合同类型"].ToString();
info.ContractName
= dr["合同名称"].ToString();
info.ContractMoney
= dr["合同金额"].ToString().ToDecimal();
converted
= DateTime.TryParse(dr["签约日期"].ToString(), outdt);if (converted && dt >dtDefault)
{
info.SignDate
=dt;
}
........................................

success
= BLLFactory<Contract>.Instance.Insert(info);returnsuccess;
}
/// <summary> ///导出Excel的操作/// </summary> private void btnExport_Click(objectsender, EventArgs e)
{
string file = FileDialogHelper.SaveExcel(string.Format("{0}.xls", moduleName));if (!string.IsNullOrEmpty(file))
{
string where =GetConditionSql();
List
<ContractInfo> list = BLLFactory<Contract>.Instance.Find(where);
DataTable dtNew
= DataTableHelper.CreateTable("序号|int,客户名称,合同编号,收支类型,合同类型,合同名称,合同金额,公司签约人,客户签约人,签约日期,签约地点,乙方名称,合同开始日期,合同结束日期,结算情况,合同状态,关联项目,联系人,联系人电话,联系人手机,合同内容,备注说明,经办人");
DataRow dr;
int j = 1;for (int i = 0; i < list.Count; i++)
{
dr
=dtNew.NewRow();
dr[
"序号"] = j++;
dr[
"客户名称"] = BLLFactory<Customer>.Instance.GetCustomerName(list[i].Customer_ID);//转义为客户名称 dr["合同编号"] =list[i].HandNo;
dr[
"收支类型"] =list[i].ExpenditureType;
dr[
"合同类型"] =list[i].ContractType;
......................................
dr[
"经办人"] =list[i].Operator;
dtNew.Rows.Add(dr);
}
try{string error = "";
AsposeExcelTools.DataTableToExcel2(dtNew, file,
outerror);if (!string.IsNullOrEmpty(error))
{
MessageDxUtil.ShowError(
string.Format("导出Excel出现错误:{0}", error));
}
else{if (MessageDxUtil.ShowYesNoAndTips("导出成功,是否打开文件?") ==System.Windows.Forms.DialogResult.Yes)
{
System.Diagnostics.Process.Start(file);
}
}
}
catch(Exception ex)
{
LogTextHelper.Error(ex);
MessageDxUtil.ShowError(ex.Message);
}
}
}

4)高级查询模块的整合

        privateFrmAdvanceSearch dlg;private void btnAdvanceSearch_Click(objectsender, EventArgs e)
{
if (dlg == null)
{
dlg
= newFrmAdvanceSearch();
dlg.FieldTypeTable
= BLLFactory<Contract>.Instance.GetFieldTypeList();
dlg.ColumnNameAlias
= BLLFactory<Contract>.Instance.GetColumnNameAlias();
dlg.DisplayColumns
= "Customer_ID,HandNo,ExpenditureType,ContractType,ContractName,ContractMoney,CompanySigner,CustomerSigner,SignDate,SignLocation,PartyBName,StartDate,EndDate,Settlement,Status,RelatedItems,Contact,ContactPhone,ContactMobile,Content,Note,Operator";#region 下拉列表数据 //dlg.AddColumnListItem("UserType", Portal.gc.GetDictData("人员类型"));//字典列表//dlg.AddColumnListItem("Sex", "男,女");//固定列表//dlg.AddColumnListItem("Credit", BLLFactory<Contract>.Instance.GetFieldList("Credit"));//动态列表 #endregiondlg.ConditionChanged+= newFrmAdvanceSearch.ConditionChangedEventHandler(dlg_ConditionChanged);
}
dlg.ShowDialog();
}
voiddlg_ConditionChanged(SearchCondition condition)
{
advanceCondition
=condition;
BindData();
}

5)编辑界面的基类继承

3、CRM系统的界面层的用户及权限信息传递

我们知道,每个业务模块都可能需要获取当前登录的一些用户信息和权限信息,以便达到更好的控制和数据的显示,如某些模块,可能只需要显示当前用户的数据,由于CRM系统的界面是独立开发,不整合在启动界面模块里面,那么如何获得用户和权限控制信息呢?例如下面的CRM模块里面的界面,需要根据当前用户获取到客户的分组列表的。

首先我们可以在界面基类中实现一个接口,以便传递相关的用户和权限信息。

namespaceWHC.Framework.BaseUI
{
public partial classBaseForm : DevExpress.XtraEditors.XtraForm, IFunction
{

其中IFunction的定义如下所示。

namespaceWHC.Framework.BaseUI
{
/// <summary> ///父窗体实现的权限控制接口/// </summary> public interfaceIFunction
{
/// <summary> ///初始化权限控制信息/// </summary> void InitFunction(LoginUserInfo userInfo, Dictionary<string, string>functionDict);/// <summary> ///是否具有访问指定控制ID的权限/// </summary> /// <param name="controlId">功能控制ID</param> /// <returns></returns> bool HasFunction(stringcontrolId);/// <summary> ///登陆用户基础信息/// </summary> LoginUserInfo LoginUserInfo { get; set; }/// <summary> ///登录用户具有的功能字典集合/// </summary> Dictionary<string, string> FunctionDict { get; set; }/// <summary> ///应用程序基础信息/// </summary> AppInfo AppInfo { get; set; }

}
}

其中就定义了接口进行用户和权限信息的赋值。在界面按钮构建相关模块的功能界面窗体的时候,我们可以为这些传递传递相关的对象信息。

        private void barCRMContact_ItemClick(objectsender, DevExpress.XtraBars.ItemClickEventArgs e)
{
ChildWinManagement.LoadMdiForm(
this, typeof(FrmCustomerContact));
}
private void barCRMCustomer_ItemClick(objectsender, DevExpress.XtraBars.ItemClickEventArgs e)
{
ChildWinManagement.LoadMdiForm(
this, typeof(FrmCustomerManage));
}
                tableForm =(Form) Activator.CreateInstance(formType);
tableForm.MdiParent
=mainDialog;//如果窗体集成了IFunction接口(第一次创建需要设置) IFunction function = tableForm as IFunction;if (function != null)
{
//初始化权限控制信息 function.InitFunction(Portal.gc.LoginUserInfo, Portal.gc.FunctionDict);//记录程序的相关信息 function.AppInfo = newAppInfo(Portal.gc.AppUnit, Portal.gc.AppName, Portal.gc.AppWholeName, Portal.gc.SystemType);
}

通过在构造窗体的时候,传入相关的用户和权限对象属性即可实现这些信息的传递和使用。

另外,为了更加方便信息的传递,我们也可以在用户登陆的时候,把这些信息通过Cache对象把它缓存起来,类似Web开发里面的Session功能。

我的该CRM系统系列的几篇随笔链接如下,供阅读。

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

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

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

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

我在本系列随笔的开始,介绍了CRM系统一个重要的客户分类的展示界面,其中包含了从字典中加载分类、从已有数据中加载分类、以及分组列表中加载分类等方式的实现,以及可以动态对这些节点进行配置,实现客户分类的界面配置处理。本文主要从逻辑代码实现的角度上解说以上功能的实现,介绍常规字典模块的动态加载、客户省份城市的动态加载、客户分组管理、客户分类配置管理等模块的具体实现。

一般情况下,我们对客户的分类都需要动态加载,对这个客户分类的管理,
包括下面几种分类。

1、常规字典模块的动态加载



以上节点是从字典模块的数据里面进行动态加载的,根据节点的不同,显示的内容不同。

首先我们需要在数据库里面建立一个表,用来记录需要显示的大的分类节点,如客户状态、客户类型、客户级别这些层次的节点,如下所示。

根据这个表的内容指引,我们在动态加载里面的子节点。

            TreeNode topNode = new TreeNode("全部客户", 0, 0);this.treeView1.Nodes.Add(topNode);

List
<SystemTreeNodeInfo> propList = BLLFactory<SystemTree>.Instance.GetTree("客户属性分类");foreach (SystemTreeNodeInfo nodeInfo inpropList)
{
if(ContainTree(nodeInfo.ID))
{
TreeNode subNode
= new TreeNode(nodeInfo.TreeName, 1, 1);
AddSystemTree(nodeInfo.Children, subNode,
2);this.treeView1.Nodes.Add(subNode);
}
}
this.treeView1.ExpandAll();for (int i = 0; i < this.treeView1.Nodes.Count; i++)
{
TreeNode node
= this.treeView1.Nodes[i];
AddDictData(node,
3);
}

其中使用递归函数进行创建树节点,也就是树节点可以是多层级的。

        /// <summary>
        ///从系统树形表里面获取数据,绑定客户属性分类和客户状态分类/// </summary>
        private void AddSystemTree(List<SystemTreeNodeInfo> nodeList, TreeNode treeNode, inti)
{
foreach (SystemTreeNodeInfo nodeInfo innodeList)
{
if(ContainTree(nodeInfo.ID))
{
TreeNode subNode
= newTreeNode(nodeInfo.TreeName, i, i);
subNode.Tag
= nodeInfo.SpecialTag;//用来做一定的标识 treeNode.Nodes.Add(subNode);

AddSystemTree(nodeInfo.Children, subNode, i
+ 1);
}
}
}

上面代码首先从一个SystemTree的业务对象里面加载列表信息,然后通过一个递归函数
AddSystemTree
实现节点的加载。

加载大的树节点完毕后,我们就从字典中获取对应的字典项目属性进行加载了,我们不管上面的树节点是集成,我们只需要知道,上面每一个节点都从数据库获取对应的项目进行绑定即可,从字典加载子节点的代码逻辑如下所示。

                List<DictDataInfo> dict = BLLFactory<DictData>.Instance.FindByDictType(treeNode.Text);foreach (DictDataInfo info indict)
{
if(ContainTree(info.ID))
{
TreeNode subNode
= newTreeNode(info.Name, i, i);if (treeNode.Tag != null)
{
subNode.Tag
= string.Format("{0}='{1}'", treeNode.Tag, info.Value);
}
treeNode.Nodes.Add(subNode);
}
}

2、
客户省份、客户城市的动态加载

除了从数据字典中加载的节点数据,还有一种如客户省份、客户城市,我们知道这些数据很大,我们如果在树列表里面展示全国的城市,那么肯定是不好的用户体验,想想要在全国几百个城市找一个出来可不容易。

于是,可以设计从已有客户所在的省份、所在的城市,把他们动态加载出来,数据就少很多,友好很多,界面效果图如下所示。



刚才我们看到了,从数据字典中动态加载子节点的操作了,其实这个和上面的操作类似,只是获取数据源的地方不同而已,我们可以根据树的节点(特殊节点)来对数据源进行不同的加载,具体如下代码所示。

        /// <summary>
        ///从数据库获取对应字典数据,并绑定到相关节点上/// </summary>
        private void AddDictData(TreeNode treeNode, inti)
{
string nodeText =treeNode.Text;if (nodeText == "客户省份")
{
List
<string> provinceList = BLLFactory<Customer>.Instance.GetCustomersProvince();foreach (string province inprovinceList)
{
TreeNode subNode
= newTreeNode(province, i, i);if (treeNode.Tag != null)
{
subNode.Tag
= string.Format("{0}='{1}'", treeNode.Tag, province);
}
treeNode.Nodes.Add(subNode);
}
}
else if (nodeText == "客户城市")
{
List
<string> cityList = BLLFactory<Customer>.Instance.GetCustomersCity();foreach (string city incityList)
{
TreeNode subNode
= newTreeNode(city, i, i);if (treeNode.Tag != null)
{
subNode.Tag
= string.Format("{0}='{1}'", treeNode.Tag, city);
}
treeNode.Nodes.Add(subNode);
}
}

通过预先在节点里面定义一些属性,我们就能构建一个可以查询出正确数据的过滤语句了,然后在树的AfterSelect事件里面实现对条件语句的查询即可。

        string treeConditionSql = "";private void treeView1_AfterSelect(objectsender, TreeViewEventArgs e)
{
if (e.Node != null)
{
//需要清空查询输入条件 this.customGridLookUpEdit1.EditValue = null;if (e.Node.Tag != null && !string.IsNullOrEmpty(e.Node.Tag.ToString()))
{
treeConditionSql
=e.Node.Tag.ToString();
BindData();

}
.....................

树的动态加载在很多地方都可以用到,例如下面的界面中,我对订单的各种属性状态进行了分类,方便操作。

3、客户分组的管理

除了上面两种,还有一种来自个人的客户组别的数据表数据,我们从其中获取到对应的客户分组信息,然后在客户分组节点中展示出来,选择对应的个人分组就可以获取对应的客户。

上面的个人分组来自对客户的个人分组表里面,它的管理界面如下所示。

个人分组的子节点加载操作代码如下所示,其中除了加载已有的客户分组外,还增加两个分组名称,如“未分组客户”和“全部客户”,方便操作。

            TreeNode myGroupNode = new TreeNode("个人分组", 4, 4);
List
<CustomerGroupNodeInfo> groupList = BLLFactory<CustomerGroup>.Instance.GetTree(LoginUserInfo.Name);
AddCustomerGroupTree(groupList, myGroupNode,
3);//添加一个未分类和全部客户的组别 myGroupNode.Nodes.Add(new TreeNode("未分组客户", 3, 3));
myGroupNode.Nodes.Add(
new TreeNode("全部客户", 3, 3));this.treeView1.Nodes.Add(myGroupNode);
myGroupNode.ExpandAll();
        /// <summary>
        ///获取客户分组并绑定/// </summary>
        private void AddCustomerGroupTree(List<CustomerGroupNodeInfo> nodeList, TreeNode treeNode, inti)
{
foreach (CustomerGroupNodeInfo nodeInfo innodeList)
{
if(ContainTree(nodeInfo.ID))
{
TreeNode subNode
= newTreeNode(nodeInfo.Name, i, i);
treeNode.Nodes.Add(subNode);

AddCustomerGroupTree(nodeInfo.Children, subNode, i);
}
}
}

然后在AfterSelect事件中处理即可实现对应数据的查询操作了。

                    else if (e.Node.FullPath.IndexOf("个人分组") >= 0)
{
if (e.Node.Text == "全部客户")
{
treeConditionSql
= "";
BindData();
}
else if (e.Node.Text == "未分组客户")
{
isUserGroupName
= true;
BindDataWithGroup(
null);
}
else{
isUserGroupName
= true;
BindDataWithGroup(e.Node.Text);
}
}
        private void BindDataWithGroup(stringgroupName)
{
//entity this.winGridViewPager1.DisplayColumns =displayColumns;this.winGridViewPager1.ColumnNameAlias = BLLFactory<Customer>.Instance.GetColumnNameAlias();//字段列显示名称转义 List<CustomerInfo> list = BLLFactory<Customer>.Instance.FindByGroupName(LoginUserInfo.Name, groupName, this.winGridViewPager1.PagerInfo);this.winGridViewPager1.DataSource = new WHC.Pager.WinControl.SortableBindingList<CustomerInfo>(list);this.winGridViewPager1.PrintTitle = "客户信息列表";
}

上面的代码中用到了当前用户的登陆名称作为一个标识(
LoginUserInfo.Name
),用来仅仅获取当前用户的分组信息的。

4、客户分类的配置管理

从上面对客户的分类,我们看到已经有很多大的类别了,每个类别展开还有好几项,这样就构成了一个很大的树,但是有时候有些客户可能不一定对所有的分类节点都感兴趣,如果能够给客户一个选择配置的机会,会显得更加友好


上面我们提供了一个单独的界面元素配置窗口给用户进行自定义的树节点配置,我们约定默认(在用户还没有保存配置的时候)是把所有节点勾选上去,如果用户选定并保存了,那么以用户配置的为准来加载树列表。

下面我们来看看具体如何实现这个操作的。

首先我们在用户初始化树的时候,把用户的保存列表获取到,并保存在一个局部变量里面,方便对节点进行判断,如下代码所示。

        private voidInitTree()
{
userTreeList
= BLLFactory<UserTreeSetting>.Instance.GetTreeSetting(treeCategory, LoginUserInfo.ID.ToString());

然后我们编写一个函数,用来判断是否需要勾选上去。刚才说到,默认如果没有保存,则需要勾选上去。

        /// <summary>
        ///如果列表为空或包含指定ID,则认为包含/// </summary>
        /// <param name="id">树ID节点</param>
        /// <returns></returns>
        private bool ContainTree(stringid)
{
bool result = false;if (userTreeList == null || userTreeList.Count == 0 ||userTreeList.Contains(id))
{
result
= true;
}
returnresult;
}

然后我们添加每个树节点的时候,使用这个函数判断是否勾选上去即可,注意每个节点的Tag使用了一个GUID作为记录,方便保存。

            List<SystemTreeNodeInfo> propList = BLLFactory<SystemTree>.Instance.GetTree("客户属性分类");foreach (SystemTreeNodeInfo nodeInfo inpropList)
{
TreeNode subNode
= new TreeNode(nodeInfo.TreeName, 1, 1);
subNode.Tag
=nodeInfo.ID;
subNode.Checked
=ContainTree(nodeInfo.ID);

AddSystemTree(nodeInfo.Children, subNode,
2);this.treeView1.Nodes.Add(subNode);
}
this.treeView1.ExpandAll();

最后,保存节点的时候,我们遍历每个节点的Tag的GUID内容,然后把它保存到用户配置表里面即可。

        private void btnOK_Click(objectsender, EventArgs e)
{
List
<string> nodeIdList = new List<string>();foreach (TreeNode node in this.treeView1.Nodes)
{
if (node.Checked && node.Tag != null && !string.IsNullOrEmpty(node.Tag.ToString()))
{
nodeIdList.Add(node.Tag.ToString());
}
nodeIdList.AddRange(GetNodeIdList(node));
}
bool result = BLLFactory<UserTreeSetting>.Instance.SaveTreeSetting(treeCategory, LoginUserInfo.ID.ToString(), nodeIdList);if(result)
{
ProcessDataSaved(
null, null);
MessageDxUtil.ShowTips(
"保存成功");
}
this.Close();
}

通过以上这些操作,我们就能在配置界面中,显示用户的选择节点,然后可以保存用户的选择内容到一个单独的配置表里面,在正式的树列表中,我们用同样的方法来判断用户是否勾选了对应的节点,如果没有勾选,那么我们不要创建这个节点即可,如下面的代码所示。

            List<SystemTreeNodeInfo> propList = BLLFactory<SystemTree>.Instance.GetTree("客户属性分类");foreach (SystemTreeNodeInfo nodeInfo inpropList)
{
if(ContainTree(nodeInfo.ID))
{
TreeNode subNode
= new TreeNode(nodeInfo.TreeName, 1, 1);
AddSystemTree(nodeInfo.Children, subNode,
2);this.treeView1.Nodes.Add(subNode);
}
}

以上就是我的CRM系统模块里面的一些常用界面元素具体实现逻辑,希望对大家分析学习有帮助。

本CRM系统主要是基于我的《
Winform开发框架
》基础上进行的模块开发,其中整合了整个框架体系里面的权限管理模块、字典管理模块、Winform分页控件、公用类库、自动更新模块、附件管理模块、人员管理模块,以及后续可能需要整合的流程管理模块、邮件收发服务模块、信息通知模块等一系列内容,希望开发出一个高效、易用的客户管理系统,同时也希望藉此系统的开发实践,进一步改进我的代码生成工具,以及进一步完善Winform开发框架各模块的内容,达到新的一个高度。


Winform开发框架

的主要功能概览如下图所示。

我的该CRM系统系列的几篇随笔链接如下,供阅读。

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

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

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

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

在前面介绍的几篇关于CRM系统的开发随笔中,里面都整合了多个页面的功能,包括多文档界面,以及客户相关信息的页面展示,这个模块就是利用DevExpress控件的XtraTabPage控件的动态加载实现的,本篇文章主要介绍两种方式的动态加载,一个是对用户控件(UserControl)模块的动态加载,一个是对普通窗体(Form)的动态加载,通过这两种方式,我们有时候可以动态实现很丰富的界面效果。

1、用户控件(UserControl)模块在Tab控件中的动态加载

参考了很多CRM的系统,一般都是把CRM系统中客户相关的信息放到下边,这也是成为了一种通用的界面布局,由于客户相关的信息很多,我们可以通过多文档方式放在下面的右下角的布局面板里面,由于整个程序是一个MDI多文档界面,不可以在子窗体里面实现另外一个MDI多文档的呈现,但是我们可以另辟蹊径,把它们放在一个TabPage页面里面,这样我们通过动态加载,也能实现我们需要的界面布局了。

由于我们想动态加载客户相关信息模块,因此每个模块可以通过用户控件的方式独立创建和维护,我们在项目工程里面创建相关的用户控件,如下所示。

我们为了方便更好的控制,我们需要定义一个所有用户控件的基类(BasePageControl)方便处理,该类定义了一些常规的数据和接口,代码如下所示。

    public partial classBasePageControl : XtraUserControl, IPageApply
{
/// <summary> ///控件的客户ID/// </summary> public string CustomerID { get; set; }/// <summary> ///登陆用户基础信息/// </summary> public LoginUserInfo LoginUserInfo { get; set; }
...............
    /// <summary>
    ///用作页面的应用接口/// </summary>
    public interfaceIPageApply
{
/// <summary> ///初始化页面的相关用户权限信息/// </summary> /// <param name="userInfo">用户信息</param> /// <param name="functionDict">权限信息</param> void InitFunction(LoginUserInfo userInfo, Dictionary<string, string>functionDict);/// <summary> ///根据客户ID属性绑定数据/// </summary> /// <param name="customerID">客户ID</param> void BindData(stringcustomerID);
}

设计好基类,然后设计相关模块的界面,继承自BasePageControl即可,该模块利用了我的
Winform分页控件
实现数据的展示,并在顶端设计了一个工具栏Bar。

以上的界面和普通的列表界面差不多,只是没有查询条件而已,所以可以利用代码生成工具Database2Sharp生成一个标准的列表页面,然后把大部分的代码复制过来进行适当的调整即可。

    public partial classActivityControl : BasePageControl
{
publicActivityControl()
{
InitializeComponent();
InitDictItem();
this.winGridViewPager1.OnPageChanged += newEventHandler(winGridViewPager1_OnPageChanged);this.winGridViewPager1.OnStartExport += newEventHandler(winGridViewPager1_OnStartExport);this.winGridViewPager1.OnEditSelected += newEventHandler(winGridViewPager1_OnEditSelected);this.winGridViewPager1.OnAddNew += newEventHandler(winGridViewPager1_OnAddNew);this.winGridViewPager1.OnDeleteSelected += newEventHandler(winGridViewPager1_OnDeleteSelected);this.winGridViewPager1.OnRefresh += newEventHandler(winGridViewPager1_OnRefresh);this.winGridViewPager1.AppendedMenu = this.contextMenuStrip1;this.winGridViewPager1.ShowLineNumber = true;this.winGridViewPager1.BestFitColumnWith = false;//是否设置为自动调整宽度,false为不设置 this.winGridViewPager1.gridView1.DataSourceChanged += newEventHandler(gridView1_DataSourceChanged);this.winGridViewPager1.gridView1.CustomColumnDisplayText += newDevExpress.XtraGrid.Views.Base.CustomColumnDisplayTextEventHandler(gridView1_CustomColumnDisplayText);
}

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

设计好相关的客户相关资料模块后,我们就需要在主界面(上图)中加载相关的内容了,由于前面博客随笔说到,需要根据用户的配置选择进行动态加载,因此,我们需要先获取客户的选择列表,然后在根据列表进行判断是否加载显示。

            userPageList = BLLFactory<UserTreeSetting>.Instance.GetTreeSetting(pageCategory, LoginUserInfo.ID.ToString());

然后在获取标准的Tab选项卡配置列表,代码如下所示。

            List<SystemTreeNodeInfo> propList = BLLFactory<SystemTree>.Instance.GetTree(pageCategory);

接着定义一个函数,看标准的页面列表是否包含在用户配置列表里面。

        /// <summary>
        ///如果列表为空或包含指定ID,则认为包含/// </summary>
        /// <param name="id">页面ID节点</param>
        /// <returns></returns>
        private bool ContainPage(stringid)
{
bool result = false;if (userPageList == null || userPageList.Count == 0 ||userPageList.Contains(id))
{
result
= true;
}
returnresult;
}

然后就是,根据相关信息,动态创建相关的Page页面,然后添加到Tab选项卡控件里面去即可,期间为了方便对界面的控制显示,我们需要传入主窗体的用户身份信息。

                    XtraTabPage page = newXtraTabPage();
page.Name
=nodeInfo.ID;
page.Text
=nodeInfo.TreeName;
page.ImageIndex
= i % 20;//设置图标,总图标只有21个 BasePageControl control=CreateCustomerControl(nodeInfo.SpecialTag);if (control != null)
{
control.InitFunction(LoginUserInfo, FunctionDict);
//给子窗体赋值用户权限信息 control.Dock =DockStyle.Fill;
page.Controls.Add(control);
}
this.tabCustomerRelated.TabPages.Add(page);

创建对象的函数CreateCustomerControl函数实现如下所示,通过上面的代码,我们就能动态创建相关的页面并显示出来了。

        /// <summary>
        ///通过数据库配置的控件名称,反射创建对象/// </summary>
        /// <param name="controlName">控件名称(简称)</param>
        /// <returns></returns>
        private BasePageControl CreateCustomerControl(stringcontrolName)
{
string namePrefix = "WHC.CRM.UI.CustomerPage.";
controlName
= namePrefix + controlName;//控件名称全称 BasePageControl userControl = null;try{
userControl
=CreateInstance(controlName, System.Reflection.Assembly.GetExecutingAssembly().GetName().Name);
}
catch(Exception ex)
{
LogTextHelper.Error(ex);
}
returnuserControl;
}
/// <summary> ///根据全名和路径构造对象/// </summary> /// <param name="sName">对象全名</param> /// <param name="sFilePath">程序集路径</param> /// <returns></returns> private static BasePageControl CreateInstance(string sName, stringsFilePath)
{
Assembly assemblyObj
=Assembly.Load(sFilePath);if (assemblyObj == null)
{
throw new ArgumentNullException("sFilePath", string.Format("无法加载sFilePath={0} 的程序集", sFilePath));
}

BasePageControl obj
= (BasePageControl)assemblyObj.CreateInstance(sName); //反射创建 returnobj;
}

最后提一下,我们创建的TabPage页面,虽然可以加上关闭图标,但是默认是关闭不了页面的,需要我们特殊处理,也就是需要在后台代码移除选定的选项卡页面,另外,为了考虑如果客户关闭了,我们可以保存它的设置,认为它在配置中隐藏显示这个页面。具体的代码逻辑如下所示。

        private void tabCustomerRelated_CloseButtonClick(objectsender, EventArgs e)
{
//关闭移除 this.tabCustomerRelated.TabPages.RemoveAt(this.tabCustomerRelated.SelectedTabPageIndex);

List
<string> nodeIdList = new List<string>();foreach (XtraTabPage page in this.tabCustomerRelated.TabPages)
{
//Tree ID= Page.Name if(!string.IsNullOrEmpty(page.Name));
{
nodeIdList.Add(page.Name);
}
}
bool result = BLLFactory<UserTreeSetting>.Instance.SaveTreeSetting(pageCategory, LoginUserInfo.ID.ToString(), nodeIdList);
}

2、普通窗体(Form)在Tab控件中的动态加载

第一小节我们介绍了在TabControl控件里面动态加载多个用户控件(UserControl)页面,但是,由于我的CRM系统模块会涉及很多相关数据处理的界面的,如果把界面全部封装成用户控件,使用起来可能会麻烦一些。

如有时候在启动界面(Starter)模块里面,我们可能在菜单配置一个独立的模块放到一级菜单里面,如果是普通的列表窗口界面,那么创建多文档的子窗口就非常方便,如果是用户控件,就调用不了;因此我希望能够尽可能保留普通窗口的类型,而不是把它改为用户控件类型。

在上图中,我们看到,每个管理模块里面,还需要展示多个子模块页面,如果利用前面小节说到的用户控件方式,在TabControl控件加载肯定完全没有问题,本小节继续探寻是否可以不改变窗口类型的情况下,动态加载普通窗口界面内容到TabControl控件里面。

首先我们设计一个模块管理的主界面,包含了列表和TabControl的控件样式。

然后再找后台代码实现TabControl控件动态加载普通窗口对象,创建窗口对象前,我们需要判断是否该类型已经在TabControl里面存在了,如果存在者前置选项卡页面即可,主要的加载代码如下所示。

        /// <summary>
        ///加载或者激活指定类型的对话框/// </summary>
        /// <param name="tabcontrol">XtraTabControl控件</param>
        /// <param name="formType">窗体类型,必须继承自BaseForm类型</param>
        private void LoadTabPageForm(XtraTabControl tabcontrol, Type formType, intimageIndex)
{
bool found = false;
XtraTabPage selectedPage
= null;foreach (XtraTabPage page intabcontrol.TabPages)
{
if (page.Tag != null && page.Tag.ToString() ==formType.Name)
{
found
= true;
selectedPage
=page;break;
}
}
if (!found)
{
selectedPage
= newXtraTabPage();
BaseDock dlg
=(BaseDock)Activator.CreateInstance(formType);
dlg.Visible
= true;
dlg.Dock
=DockStyle.Fill;
dlg.FormBorderStyle
=FormBorderStyle.None;
dlg.TopLevel
= false;//在这里一定要注意 dlg.InitFunction(LoginUserInfo, FunctionDict);//给子窗体赋值用户权限信息 selectedPage.Text =dlg.Text;
selectedPage.ImageIndex
=imageIndex;
selectedPage.Tag
=dlg.GetType().Name;
selectedPage.Controls.Add(dlg);
tabcontrol.TabPages.Add(selectedPage);
}

selectedPage.BringToFront();
tabcontrol.SelectedTabPage
=selectedPage;
}

实现上面的函数后,我们只需要在按钮事件里面,调用上面的函数,并传入相关的参数即可。

        private void itemProduct_LinkClicked(objectsender, DevExpress.XtraNavBar.NavBarLinkEventArgs e)
{
LoadTabPageForm(
this.xtraTabMain, typeof(FrmProduct), 0);
}
private void itemManufacturer_LinkClicked(objectsender, DevExpress.XtraNavBar.NavBarLinkEventArgs e)
{
LoadTabPageForm(
this.xtraTabMain, typeof(FrmManufacturer), 1);
}
private void itemCompetitor_LinkClicked(objectsender, DevExpress.XtraNavBar.NavBarLinkEventArgs e)
{
LoadTabPageForm(
this.xtraTabMain, typeof(FrmCompetitor), 2);
}

同时关闭事件我们需要增加代码进行处理,关闭事件的代码如下所示。

        private void xtraTabMain_CloseButtonClick(objectsender, EventArgs e)
{
XtraTabPage currentPage
= this.xtraTabMain.SelectedTabPage;
BaseForm form
= currentPage.Controls[0] asBaseForm;if (form != null)
{
form.Close();
form.Dispose();
}
this.xtraTabMain.TabPages.RemoveAt(this.xtraTabMain.SelectedTabPageIndex);
}

以上就是在TabControl控件里面,动态加载用户控件、普通窗口的
两种不同的
方式,都能为我们实现丰富的界面布局展现,希望对大家在开发Winform界面效果上有所参考。

我的该CRM系统系列的几篇随笔链接如下,供阅读。

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


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


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


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