我在本系列随笔的开始,介绍了CRM系统一个重要的客户分类的展示界面,其中包含了从字典中加载分类、从已有数据中加载分类、以及分组列表中加载分类等方式的实现,以及可以动态对这些节点进行配置,实现客户分类的界面配置处理。本文主要从逻辑代码实现的角度上解说以上功能的实现,介绍常规字典模块的动态加载、客户省份城市的动态加载、客户分组管理、客户分类配置管理等模块的具体实现。
一般情况下,我们对客户的分类都需要动态加载,对这个客户分类的管理,包括下面几种分类。
1、常规字典模块的动态加载
以上节点是从字典模块的数据里面进行动态加载的,根据节点的不同,显示的内容不同。
首先我们需要在数据库里面建立一个表,用来记录需要显示的大的分类节点,如客户状态、客户类型、客户级别这些层次的节点,如下所示。
根据这个表的内容指引,我们在动态加载里面的子节点。
TreeNode topNode = new TreeNode("全部客户", , );
this.treeView1.Nodes.Add(topNode); List<SystemTreeNodeInfo> propList = BLLFactory<SystemTree>.Instance.GetTree("客户属性分类");
foreach (SystemTreeNodeInfo nodeInfo in propList)
{
if (ContainTree(nodeInfo.ID))
{
TreeNode subNode = new TreeNode(nodeInfo.TreeName, , );
AddSystemTree(nodeInfo.Children, subNode, );
this.treeView1.Nodes.Add(subNode);
}
}
this.treeView1.ExpandAll(); for (int i = ; i < this.treeView1.Nodes.Count; i++)
{
TreeNode node = this.treeView1.Nodes[i];
AddDictData(node, );
}
其中使用递归函数进行创建树节点,也就是树节点可以是多层级的。
/// <summary>
/// 从系统树形表里面获取数据,绑定客户属性分类和客户状态分类
/// </summary>
private void AddSystemTree(List<SystemTreeNodeInfo> nodeList, TreeNode treeNode, int i)
{
foreach (SystemTreeNodeInfo nodeInfo in nodeList)
{
if (ContainTree(nodeInfo.ID))
{
TreeNode subNode = new TreeNode(nodeInfo.TreeName, i, i);
subNode.Tag = nodeInfo.SpecialTag;//用来做一定的标识
treeNode.Nodes.Add(subNode); AddSystemTree(nodeInfo.Children, subNode, i + );
}
}
}
上面代码首先从一个SystemTree的业务对象里面加载列表信息,然后通过一个递归函数AddSystemTree实现节点的加载。
加载大的树节点完毕后,我们就从字典中获取对应的字典项目属性进行加载了,我们不管上面的树节点是集成,我们只需要知道,上面每一个节点都从数据库获取对应的项目进行绑定即可,从字典加载子节点的代码逻辑如下所示。
List<DictDataInfo> dict = BLLFactory<DictData>.Instance.FindByDictType(treeNode.Text);
foreach (DictDataInfo info in dict)
{
if (ContainTree(info.ID))
{
TreeNode subNode = new TreeNode(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, int i)
{
string nodeText = treeNode.Text;
if (nodeText == "客户省份")
{
List<string> provinceList = BLLFactory<Customer>.Instance.GetCustomersProvince();
foreach (string province in provinceList)
{
TreeNode subNode = new TreeNode(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 in cityList)
{
TreeNode subNode = new TreeNode(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(object sender, 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("个人分组", , );
List<CustomerGroupNodeInfo> groupList = BLLFactory<CustomerGroup>.Instance.GetTree(LoginUserInfo.Name);
AddCustomerGroupTree(groupList, myGroupNode, );
//添加一个未分类和全部客户的组别
myGroupNode.Nodes.Add(new TreeNode("未分组客户", , ));
myGroupNode.Nodes.Add(new TreeNode("全部客户", , )); this.treeView1.Nodes.Add(myGroupNode);
myGroupNode.ExpandAll();
/// <summary>
/// 获取客户分组并绑定
/// </summary>
private void AddCustomerGroupTree(List<CustomerGroupNodeInfo> nodeList, TreeNode treeNode, int i)
{
foreach (CustomerGroupNodeInfo nodeInfo in nodeList)
{
if (ContainTree(nodeInfo.ID))
{
TreeNode subNode = new TreeNode(nodeInfo.Name, i, i);
treeNode.Nodes.Add(subNode); AddCustomerGroupTree(nodeInfo.Children, subNode, i);
}
}
}
然后在AfterSelect事件中处理即可实现对应数据的查询操作了。
else if (e.Node.FullPath.IndexOf("个人分组") >= )
{
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(string groupName)
{
//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 void InitTree()
{
userTreeList = BLLFactory<UserTreeSetting>.Instance.GetTreeSetting(treeCategory, LoginUserInfo.ID.ToString());
然后我们编写一个函数,用来判断是否需要勾选上去。刚才说到,默认如果没有保存,则需要勾选上去。
/// <summary>
/// 如果列表为空或包含指定ID,则认为包含
/// </summary>
/// <param name="id">树ID节点</param>
/// <returns></returns>
private bool ContainTree(string id)
{
bool result = false;
if (userTreeList == null || userTreeList.Count == || userTreeList.Contains(id))
{
result = true;
}
return result;
}
然后我们添加每个树节点的时候,使用这个函数判断是否勾选上去即可,注意每个节点的Tag使用了一个GUID作为记录,方便保存。
List<SystemTreeNodeInfo> propList = BLLFactory<SystemTree>.Instance.GetTree("客户属性分类");
foreach (SystemTreeNodeInfo nodeInfo in propList)
{
TreeNode subNode = new TreeNode(nodeInfo.TreeName, , );
subNode.Tag = nodeInfo.ID;
subNode.Checked = ContainTree(nodeInfo.ID); AddSystemTree(nodeInfo.Children, subNode, );
this.treeView1.Nodes.Add(subNode);
}
this.treeView1.ExpandAll();
最后,保存节点的时候,我们遍历每个节点的Tag的GUID内容,然后把它保存到用户配置表里面即可。
private void btnOK_Click(object sender, 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 in propList)
{
if (ContainTree(nodeInfo.ID))
{
TreeNode subNode = new TreeNode(nodeInfo.TreeName, , );
AddSystemTree(nodeInfo.Children, subNode, );
this.treeView1.Nodes.Add(subNode);
}
}
以上就是我的CRM系统模块里面的一些常用界面元素具体实现逻辑,希望对大家分析学习有帮助。
本CRM系统主要是基于我的《Winform开发框架》基础上进行的模块开发,其中整合了整个框架体系里面的权限管理模块、字典管理模块、Winform分页控件、公用类库、自动更新模块、附件管理模块、人员管理模块,以及后续可能需要整合的流程管理模块、邮件收发服务模块、信息通知模块等一系列内容,希望开发出一个高效、易用的客户管理系统,同时也希望藉此系统的开发实践,进一步改进我的代码生成工具,以及进一步完善Winform开发框架各模块的内容,达到新的一个高度。
《Winform开发框架》的主要功能概览如下图所示。
我的该CRM系统系列的几篇随笔链接如下,供阅读。
Winform开发框架之客户关系管理系统(CRM)的开发总结系列1-界面功能展示
Winform开发框架之客户关系管理系统(CRM)的开发总结系列2-基于框架的开发过程