分类 其它 下的文章

在很多项目中,可能会有要求对一些数据的阅读状态进行记录,用户阅读过或者未阅读过,都做不同的标识,方便了解数据的状态。如在我的客户关系管理系统中,对于客户的状态进行跟踪,如果最近联系时间超过配置天数的,那么特别显示出来。类似的应用场景,还有很多地方应用到,如对于通知公告、流程记录、内部信息等状态查看都可能是这样的类型。那么如何解决这些通用的需求呢,是需要每个都设置一个表来记录这些状态吗?

1、应用需求场景

前面说了,我们可能在一些数据上需要记录不同用户的阅读状态,如下面是我客户关系管理系统里面,对于最近没有联系的客户列表,其中对他们的查看状态进行特别显示。

当然,在我们业务系统里面,可能还有其他类似的场景。

对于这些相似的需求我们把这些应用场景的状态,用一个表来存储它的数据变化就可以做到了,我们设计一个表TB_InformationStatus来存储这些数据的状态。

上面的Information_ID就是对应不同表数据的ID,Status为我们需要记录的状态,User_ID为对应使用人员,这样对于不同业务表,不同的人员都可以把他们的数据记录起来,供我们处理显示了。

2、功能实现

对于这个信息状态的记录表,我们需要定义几个接口来进行信息的处理。

        /// <summary>
        ///设置状态/// </summary>
        /// <param name="UserID">用户ID</param>
        /// <param name="InfoType">信息类型</param>
        /// <param name="InfoID">信息主键ID</param>
        /// <param name="Status">状态:0未读 1已读</param>
        void SetStatus(string UserID, InformationCategory InfoType, string InfoID, intStatus);/// <summary>
        ///匹配状态/// </summary>
        /// <param name="UserID">用户ID</param>
        /// <param name="InfoType">信息类型</param>
        /// <param name="InfoID">信息主键ID</param>
        /// <param name="Status">状态:0未读 1已读</param>
        /// <returns></returns>
        bool CheckStatus(string UserID, InformationCategory InfoType, string InfoID, intStatus);/// <summary>
        ///查看指定的记录是否已读/// </summary>
        /// <param name="UserID">用户ID</param>
        /// <param name="InfoType">信息类型</param>
        /// <param name="InfoID">信息主键ID</param>
        /// <returns></returns>
        bool IsReadedStatus(string UserID, InformationCategory InfoType, string InfoID);

我们设计了上面的辅助表TB_InformationStatus来存储这些数据的状态,但并没有改变主表的字段数据,但是我们在显示主表的数据的时候,联合处理一下就可以了。

以客户信息为例,我们联合处理,获得的数据,依旧是客户信息的列表,如下代码所示。

        /// <summary>
        ///获取用户的最近未联系客户列表/// </summary>
        /// <returns></returns>
        private List<CustomerInfo>GetUnContactList()
{
string KeyName = "FollowExpireDays";int FollowExpireDays =config.AppConfigGet(KeyName).ToInt32();if (FollowExpireDays < 1)
{
FollowExpireDays
= 1;
}
List
<CustomerInfo> list = BLLFactory<Customer>.Instance.GetUnContactList(FollowExpireDays, LoginUserInfo.ID.ToString());returnlist;
}

在业务层,我们只需要构造我们的过滤条件获取到用户的数据,并处理它状态就可以了。

                condition.AddCondition("LastContactDate", today.AddDays(-1 *FollowExpireDays), SqlOperator.LessThanOrEqual);
condition.AddCondition(
"Deleted", 0, SqlOperator.Equal);//不显示删除的 condition.AddCondition("Creator", userId, SqlOperator.Equal);//仅仅选择该用户的记录 string where = condition.BuildConditionSql().Replace("Where", "");

List
<CustomerInfo> list = baseDal.Find(where);foreach (CustomerInfo info inlist)
{
bool readed = BLLFactory<InformationStatus>.Instance.IsReadedStatus(userId, InformationCategory.客户联系, info.ID);
info.Data1 = readed ? "已读" : "未读"
;
}

上面代码的Data1是我们实体类基类的属性,这里我们很方面用它来记录状态,否则我们需要把实体类集合转换为DataTable类型了。

这样我们返回的数据就带有这个记录的阅读状态,我们只需要在显示的时候,把Data1属性的别名修改一下就可以了。

        /// <summary>
        ///绑定列表数据/// </summary>
        private voidBindData()
{
this.winGridViewPager1.DisplayColumns =displayColumns;this.winGridViewPager1.ColumnNameAlias = BLLFactory<Customer>.Instance.GetColumnNameAlias();//字段列显示名称转义 this.winGridViewPager1.AddColumnAlias("Data1", "查看状态");

List
<CustomerInfo> list =GetUnContactList();this.winGridViewPager1.DataSource = new WHC.Pager.WinControl.SortableBindingList<CustomerInfo>(list);this.winGridViewPager1.PrintTitle = "未联系客户信息列表";
}

3、功能扩展

为了更有效展示不同类型客户的记录,我们可能需要设置普通客户7天需要跟进,VIP客户5天跟进,高级VIP客户3天跟进的时效,也就是对于同一个记录,不同属性类型,可能要求不同。

我们如果要实现这个需求,那么就需要再另外一个表里面记录客户类型和间隔天数的数据了。

然后在业务逻辑层处理返回未联系客户的时候,对他们进行分别处理,获取数据后进行合并,如下代码所示。

        /// <summary>
        ///获得指定间隔时间内未联系的客户列表/// </summary>
        /// <param name="unContactDays">和最后联系日期的间隔天数</param>
        /// <param name="userId">当前用户</param>
        /// <returns></returns>
        public List<CustomerInfo> GetUnContactList(int unContactDays, stringuserId)
{
List
<CustomerInfo> listAll = new List<CustomerInfo>();//根据用户配置的信息进行逐条处理,然后合并记录 List<CustomerAlarmInfo> alarmList = BLLFactory<CustomerAlarm>.Instance.FindByUser(userId);foreach (CustomerAlarmInfo alarmInfo inalarmList)
{
//如果存在高级查询对象信息,则使用高级查询条件,否则使用主表条件查询 SearchCondition condition = newSearchCondition();
DateTime today
= Convert.ToDateTime(DateTime.Now.ToString("yyyy-MM-dd"));int FollowExpireDays =alarmInfo.Days;if (FollowExpireDays < 1)
{
FollowExpireDays
= 1;
}

condition.AddCondition(
"Grade", alarmInfo.Grade, SqlOperator.Equal);
condition.AddCondition(
"LastContactDate", today.AddDays(-1 *FollowExpireDays), SqlOperator.LessThanOrEqual);
condition.AddCondition(
"Deleted", 0, SqlOperator.Equal);//不显示删除的 condition.AddCondition("Creator", userId, SqlOperator.Equal);//仅仅选择该用户的记录 string where = condition.BuildConditionSql().Replace("Where", "");

List
<CustomerInfo> list = baseDal.Find(where);foreach (CustomerInfo info inlist)
{
bool readed = BLLFactory<InformationStatus>.Instance.IsReadedStatus(userId, InformationCategory.客户联系, info.ID);
info.Data1
= readed ? "已读" : "未读";
}

listAll.AddRange(list);
}
returnlistAll;
}

对于混合架构上的应用,我们注意到接口的地方,需要使用一个枚举的参数(信息类别名称),我们在接口定义的时候,需要特别声明几个地方,否则容易出现错误。

枚举的对象需要声明一下的。

    /// <summary>
    ///信息分类/// </summary>
[DataContract]public enumInformationCategory 
{
[EnumMember]
客户联系,

[EnumMember]
通知公告,

[EnumMember]
其他
};

定义的WCF接口,用到了枚举类型的参数,也需要特别声明枚举的类型

[ServiceContract]
[ServiceKnownType(
typeof(InformationCategory))]public interface IInformationStatusService : IBaseService<InformationStatusInfo>{/// <summary> ///设置状态/// </summary> /// <param name="UserID">用户ID</param> /// <param name="InfoType">信息类型</param> /// <param name="InfoID">信息主键ID</param> /// <param name="Status">状态:0未读 1已读</param> [OperationContract]void SetStatus(string UserID, InformationCategory InfoType, string InfoID, intStatus);/// <summary> ///匹配状态/// </summary> /// <param name="UserID">用户ID</param> /// <param name="InfoType">信息类型</param> /// <param name="InfoID">信息主键ID</param> /// <param name="Status">状态:0未读 1已读</param> /// <returns></returns> [OperationContract]bool CheckStatus(string UserID, InformationCategory InfoType, string InfoID, intStatus);/// <summary> ///查看指定的记录是否已读/// </summary> /// <param name="UserID">用户ID</param> /// <param name="InfoType">信息类型</param> /// <param name="InfoID">信息主键ID</param> /// <returns></returns> [OperationContract]bool IsReadedStatus(string UserID, InformationCategory InfoType, stringInfoID);
}

注意上这些,使用枚举就一切都顺利了。

前阵子有空发现淘宝的API挺有意思的,就留意学习了一下,淘宝从2008.6月开始公测以来,API渐趋稳定,文档的相关说明也比较细致,不过基于淘宝API做应用的软件目前应该还不是很多,不过由于马云的影响力,淘宝API终会发光发热,给我们做开发的程序员多一份选择,多一个摆弄的东西。

在学习中,发现有一些比较好玩,比较有趣的地方,如果有空继续的话,准备开一个系列,总结介绍一下相关的知识,希望能够给博客多一份人气,多一份积累。

淘宝开放平台(Taobao Open Plateform,又叫TOP),地址是
http://open.taobao.com/
,我们看看其官方对TOP的描述:

淘宝开放平台(即TOP,无特别说明下文中TOP即指淘宝开放平台)对各类合作伙伴所提供的产品可以概括为两个平台,三条支撑主线。两个平台指的是
Open.taobao.com

App.taobao.com
;三条支撑主线是开放数据和业务流程、开放淘宝自有插件平台、开放对外接入标准,关于这两个产品和三条业务支撑线。


和众多平台一样,如Google,淘宝一样需要开发者注册一个账号,淘宝每个应用需要注册一个应用程序键(App Key),淘宝就是基于应用程序键来控制用户的访问频率和流量的,另外可以通过应用程序键,让使用者登陆确认,获取到相关的授权码,然后获取SessionKey,作为访问使用者淘宝资源(如买入卖出等私人记录的信息)。



淘宝的授权码是在用户确认后产生,并且只能使用一次,用来生成SessionKey,一般来说,SessionKey间隔的调用API事件不能超过10分钟,否则就会失效,需要重新获取用户的授权码,然后再次生成SessionKey。这样的做法虽然比较麻烦,但是对保证使用者的淘宝资源确是比较不错的做法。大概的界面如下所示:

淘宝的论坛有各种语言开发的介绍,相对来说还是比较详细,开发者有什么问题,在这里都可以问问,论坛地址是:
http://open.taobao.com/bbs/forum.php

淘宝的API是以REST服务提供了,通过HTTP访问,返回相关的信息,格式可以是Json格式的,也可以是XML格式的,各取所需吧。

有一位能人把淘宝的API做了进一步的封装,就是TOP4NET的类库,封装了各种API资源的访问,并且把信息封装为实体类对象进行传递,使用起来还是不错的,本篇后面会进一步介绍一下基于TOP4NET的基础上做淘宝的应用开发。

TOP4NET 是作为开源组件提供的,其SVN地址是
http://code.google.com/p/top4net/source/browse/#svn/trunk/Top4Net
, 大家可以下载下来研究研究,做一定的修改补充,代码风格写得不错,赞一个。

在前面随笔《
ABP开发框架前后端开发系列---(9)ABP框架的权限控制管理
》中介绍了基于ABP框架服务构建的Winform客户端,客户端通过Web API调用的方式进行获取数据,从而实现了对组织机构、角色、用户、权限等管理,其中没有涉及菜单部分,本篇随笔介绍在ABP框架中实现菜单的管理,菜单是作为Winform或者Web动态构建界面的一个重要元素,同时也是作为角色权限控制的部分资源。

1、菜单的列表展示和管理

一般情况下,菜单的树形列表的显示可以分为多个节点,节点可以收缩也可以展开,当然节点是有不同的图标的了。这样就可以把很多功能点整合在一个树列表里面了,树的节点也可以分为很多级别,很多层次


如果我们想按照业务的范畴来区分,也可以分为多个模块展示,类似选项卡的方式,一个模块的功能菜单列表集合在一起展示,如下所示。

上面是我Winform开发框架和混合式开发框架的Winform界面中呈现菜单的界面,对于ABP开发框架来说,我们也只是获取数据方式不同,业务范畴的管理也没有什么不一样,我们依旧可以在服务器端配置好系统的菜单记录,然后基于ABP的Winform界面,同样管理这些内容即可。

下面是ABP框架中对于菜单资源管理的列表界面。

左边我们通过TreeList列表进行展示,右侧通过分页控件列表的方式进行展示,还是比较标准的Winform界面展示。

编辑或者创建菜单的界面如下所示。

菜单对于角色来说,应该是一种界面资源,可以通过配置进行管理对应角色用户的菜单。

2、菜单模块的实现逻辑

为了开发菜单模块,我们需要先定义好菜单的存储数据表,定义菜单表和角色菜单的中间关系表如下所示。

这个菜单模块定位为Web和Winform都通用的,因此菜单表中增加多了一些字段信息。

在数据库里增加这两个表后,就可以使用代码生成工具进行框架代码的生成和Winform界面代码的生成了。

生成框架后,对应的应用服务层类代码如下所示。

这个生成的类,默认具有基类的增删改查分页等接口方法,同时我们也会生成对应的Web API Caller层的类代码,代码如下所示。

Winform界面生成标准界面后进行布局的一定调整,左侧增加TreeList控件,设计界面如下所示。

获取列表数据的函数定义在GetData函数里面,函数代码如下所示。

        /// <summary>
        ///获取数据/// </summary>
        /// <returns></returns>
        private async Task<IPagedResult<MenuDto>>GetData()
{
MenuPagedDto pagerDto
= null;if (advanceCondition != null)
{
pagerDto
= new MenuPagedDto(this.winGridViewPager1.PagerInfo);
pagerDto
=dlg.GetPagedResult(pagerDto);
}
else if(!IsNormalSearch && this.tree.FocusedNode != null)
{
//构建分页的条件和查询条件 pagerDto = new MenuPagedDto(this.winGridViewPager1.PagerInfo)
{
PID
= string.Concat(this.tree.FocusedNode.GetValue(Id_FieldName))
};
}
else{//构建分页的条件和查询条件 pagerDto = new MenuPagedDto(this.winGridViewPager1.PagerInfo)
{
//添加所需条件 Name = this.txtName.Text.Trim(),
WinformType
= this.txtWinformType.Text.Trim()
};
}
var result = awaitMenuApiCaller.Instance.GetAll(pagerDto);returnresult;
}

分页控件的数据绑定代码如下所示,这些都是根据Winform界面配置自动生成的代码。

            this.winGridViewPager1.DisplayColumns = "EmbedIcon,Name,Seq,Visible,Expand,WinformType,Tag,CreationTime";this.winGridViewPager1.ColumnNameAlias = await MenuApiCaller.Instance.GetColumnNameAlias();//字段列显示名称转义//获取分页数据列表
            var result = awaitGetData();//设置所有记录数和列表数据源
            this.winGridViewPager1.PagerInfo.RecordCount = result.TotalCount; //需先于DataSource的赋值,更新分页信息
            this.winGridViewPager1.DataSource = result.Items;

而TreeList列表是我们后来增加上去的,需要额外进行数据的绑定和处理,初始化树列表处理代码如下所示。

        /// <summary>
        ///初始化树控件/// </summary>
        private voidInitTree()
{
this.tree.Columns.Clear();//控件扩展函数封装处理 this.tree.CreateColumn("Name", "菜单名称", 160, true);this.tree.InitTree("Id", "PID", null, false, false);//设置树的图标集合及逐级图标 this.tree.SelectImageList = this.imageCollection1;this.tree.CustomDrawNodeImages += (object sender, CustomDrawNodeImagesEventArgs e) =>{int maxCount = this.imageCollection1.Images.Count;var index = e.Node.Level < maxCount ? e.Node.Level : 0;
e.SelectImageIndex
=index;
};
//初始化树节点选择事件 this.tree.FocusedNodeChanged += delegate (objectsender, FocusedNodeChangedEventArgs e)
{
this.FocusedNodeChanged();
};
}

获取列表数据并绑定树列表的数据源如下所示

        /// <summary>
        ///绑定树的数据源/// </summary>
        private asyncTask BindTree()
{
var pageDto = new MenuPagedDto();var result = awaitMenuApiCaller.Instance.GetAll(pageDto);this.tree.DataSource =result.Items;this.tree.ExpandAll();
}

而界面显示的时候,加载并显示左侧树列表数据如下代码所示。

        private async void FrmMenu_Load(objectsender, EventArgs e)
{
//列表信息 InitTree();
InitSearchControl();
awaitBindTree();
}

删除菜单的时候,我们一般想把当前菜单和下面的子菜单一并级联删除,实现这个方法,我们需要在服务端自定义实现,如下是应用服务层的实现方法。

        /// <summary>
        ///移除节点和子节点/// </summary>
        /// <param name="input"></param>
        /// <returns></returns>
[UnitOfWork]public virtual async Task DeleteWithSubNode(EntityDto<string>input)
{
var children = await _repository.GetAllListAsync(ou => ou.PID ==input.Id);foreach (var child inchildren)
{
await DeleteWithSubNode(new EntityDto<string>(child.Id));//递归删除 }await_repository.DeleteAsync(input.Id);
}

我们这里显示声明了UnitOfWork标记,说明这个操作的原子性,全部成功就成功,否则失败的处理。

而客户端的Web API 封装调用类,对这个Web API接口的封装,根据上篇随笔《
ABP开发框架前后端开发系列---(10)Web API调用类的简化处理
》简化后的处理代码如下所示。

3、角色菜单管理

菜单的管理整体操作和常规的业务表处理一样,没有太多特殊的地方,下面介绍一下角色包含菜单的管理操作。

前面介绍了角色包含菜单的管理界面如下所示。

界面主要是列出所有菜单,并勾选上该角色可以使用的菜单。这个角色包含的菜单和角色包含的权限处理上比较相似。

首先我们需要定义一个角色DTO对象中的菜单集合属性,如下所示。

在界面上获取勾选上的权限和菜单ID集合,存储在对应的列表里面。

        /// <summary>
        ///编辑或者保存状态下取值函数/// </summary>
        /// <param name="info"></param>
        private voidSetInfo(RoleDto info)
{
info.DisplayName
=txtDisplayName.Text;
info.Name
=txtName.Text;
info.Description
=txtDescription.Text;

info.Permissions
= GetNodeValues(this.tree, "Name");
info.Menus
= GetNodeValues(this.treeMenu, "Id");
}

在应用服务层的RoleAppService类里面,我们创建或者更新角色的时候,需要更新它的权限和菜单资源,如下代码是创建角色的函数。

        /// <summary>
        ///创建角色对象/// </summary>
        /// <param name="input"></param>
        /// <returns></returns>
        public override async Task<RoleDto>Create(CreateRoleDto input)
{
CheckCreatePermission();
var role = ObjectMapper.Map<Role>(input);
role.SetNormalizedName();

CheckErrors(
await_roleManager.CreateAsync(role));await CurrentUnitOfWork.SaveChangesAsync(); //It's done to get Id of the role. awaitUpdateGrantedPermissions(role, input.Permissions);awaitUpdateGrantedMenus(role, input.Menus);returnMapToEntityDto(role);
}

同理,更新角色一样处理这两个部分的资源

        /// <summary>
        ///更新角色对象/// </summary>
        /// <param name="input"></param>
        /// <returns></returns>
        public override async Task<RoleDto>Update(RoleDto input)
{
CheckUpdatePermission();
var role = await_roleManager.GetRoleByIdAsync(input.Id);

ObjectMapper.Map(input, role);

CheckErrors(
await_roleManager.UpdateAsync(role));awaitUpdateGrantedPermissions(role, input.Permissions);awaitUpdateGrantedMenus(role, input.Menus);returnMapToEntityDto(role);
}

以上就是菜单的管理,和角色包含菜单的维护操作,整个开发过程主要就是使用代码生成工具来快速生成框架各个层的代码,以及Winform界面的代码,这样在进行一定的函数扩展以及界面布局调整,就可以非常方便、高效的完整一个业务模块的开发工作了。

关于数据权限的控制,可能我们在做很多大型一点的系统都会碰到过,可能每个人设计和解决问题的思路都有所不同,本文介绍我自己框架里面的解决思路。从上一篇《
如何在应用系统中实现数据权限的控制功能
》里面我们可能对权限控制和数据权限的控制有了一个初步的了解,本文接着进一步介绍在应用系统中,如何集成数据权限的控制功能。

1、数据权限实现思路分析

为了实现数据权限的控制,我们需要在通用的权限系统里面保存好对应角色具有哪些组织机构的数据权限,然后在应用系统中调用API进行过滤数据处理即可。

为了实现以上的功能需求,我们需要在权限系统里面,角色控制哪里增加一个数据权限的数据存储。

实际的应用系统,当用户登陆成功后,我们获取并记录好其可以管理的公司或者部门,如果是主管的角色,可能有多个公司的数据可以管理,那么可以在程序的顶部,让用户选择管理那个公司的数据即可,如果切换公司,那么刷新现有的界面数据显示就可以了。

在用户成功登陆后,我们可以记录用户的相关权限控制信息,如他所能控制数据的公司或者部门,把它记录下来。

                        Portal.gc.CompanyList = BLLFactory<RoleData>.Instance.GetBelongCompanysByUser(info.ID);
List
<int> deptList = BLLFactory<RoleData>.Instance.GetBelongDeptsByUser(info.ID);
Portal.gc.DeptList
= deptList;

然后存储用户默认的公司ID,并根据用户是否为管理员(超级管理员、公司管理员),然后构造一个通用的过滤条件,放到全局缓存里面,方便各个模块使用,如下代码所示。

                        //设置选定的公司ID(默认为用户所在公司的ID)
                        Cache.Instance["SelectedCompanyID"] =info.Company_ID;//设置过滤条件给界面基类使用
                        string filterCondition = string.Format("Company_ID = '{0}'", info.Company_ID);if (!Portal.gc.IsAdmin)
{
if (deptList.Count > 0)
{
filterCondition
+= string.Format("AND Dept_ID IN ({0})", string.Join(",", deptList));
}
else{
filterCondition
+= string.Format("AND Creator = '{0}'", info.ID);
}
}
Cache.Instance[
"DataFilterCondition"] = filterCondition;

在主界面的时候,我们可以根据用户所能管理的公司数据,在顶部初始化公司列表,方便切换选择,以下是初始化的代码。

            //添加受管理的公司机构//判断如果用户管理的公司数据多于一个,那么就显示选择单位列表,并绑定公司数据
            if (Portal.gc.CompanyList.Count > 1)
{
this.repositoryCompanyItem.Items.Clear();foreach (int company inPortal.gc.CompanyList)
{
OUInfo companyInfo
= BLLFactory<OU>.Instance.FindByID(company);if (companyInfo != null)
{
this.repositoryCompanyItem.Items.Add(newCListItem(companyInfo.Name, companyInfo.ID.ToString()));
}
}
//多于一个显示公司下拉列表 this.barCompanyItem.Visibility =DevExpress.XtraBars.BarItemVisibility.Always;
}
else{//只有一个公司时候,屏蔽公司选择列表 this.barCompanyItem.Visibility =DevExpress.XtraBars.BarItemVisibility.Never;
}

如果多于一个公司,那么正常的需求是可以切换公司来查看其它公司的数据的,要实现这个功能,那么就需要修改登陆的那个全局的过滤条件:Cache.Instance["DataFilterCondition"]了。

我们来看看代码的实现,其主要的逻辑就是获取用户选择的公司ID,然后根据公司、部门信息,重新构建一个全局的过滤条件,并重新缓存到对应的键值里面去,供后面的窗体实现数据的过滤更新。

                CListItem item = this.barCompanyItem.EditValue asCListItem;if (item != null)
{
//设置选定的公司ID Cache.Instance["SelectedCompanyID"] =item.Value;
SetSelectedCompanyName();
//设置过滤条件给界面基类使用 string filterCondition = string.Format("Company_ID = '{0}'", item.Value);if (!Portal.gc.IsAdmin)
{
if (Portal.gc.DeptList.Count > 0)
{
filterCondition
+= string.Format("AND Dept_ID IN ({0})", string.Join(",", Portal.gc.DeptList));
}
else{
filterCondition
+= string.Format("AND Creator = '{0}'", Portal.gc.UserInfo.ID);
}
}
Cache.Instance[
"DataFilterCondition"] = filterCondition;

如果需要对已有的窗体实现数据更新,那么遍历窗体,并统一实现数据刷新即可。

                    //遍历全部窗口,更新
                    foreach (WHC.Framework.BaseUI.BaseDock form in this.MdiChildren)
{
form.SelectedCompanyID
=item.Value;
form.DataFilterCondition
=filterCondition;
form.FormOnLoad();
}
string message = string.Format("您已经切换数据显示:{0}", item.Text);
MessageDxUtil.ShowTips(message);

2、窗体数据过滤的实现

从上面的步骤代码,我们可以看到如何构建一个全局的过滤条件,但是我们获取数据的时候,如何才能实现数据权限的控制,让用户所能看到的数据在可控的范围内呢?

我们知道,一般窗体数据列表的绑定操作类似如下代码所示

        /// <summary>
        ///绑定列表数据/// </summary>
        private voidBindData()
{
//entity this.winGridViewPager1.DisplayColumns =displayColumns;this.winGridViewPager1.ColumnNameAlias = CallerFactory<ICustomerService>.Instance.GetColumnNameAlias();//字段列显示名称转义 string where =GetConditionSql();
PagerInfo pagerInfo
= this.winGridViewPager1.PagerInfo;
List
<CustomerInfo> list = CallerFactory<ICustomerService>.Instance.FindWithPager(where, refpagerInfo);this.winGridViewPager1.DataSource = new WHC.Pager.WinControl.SortableBindingList<CustomerInfo>(list);this.winGridViewPager1.PrintTitle = "客户信息列表";
}

所以主要的数据控制,就在函数GetConditionSql()里面了,那么这个里面,我们如何整合前面的过滤条件呢?

下面是一个案例代码。

        /// <summary>
        ///根据查询条件构造查询语句/// </summary> 
        private stringGetConditionSql()
{
//如果存在高级查询对象信息,则使用高级查询条件,否则使用主表条件查询 SearchCondition condition =advanceCondition;if (condition == null)
{
condition
= newSearchCondition();if(customGridLookUpEdit1.EditValue != null)
{
condition.AddCondition(
"ID", customGridLookUpEdit1.EditValue.ToString(), SqlOperator.Equal);
}
condition.AddCondition(
"Deleted", 0, SqlOperator.Equal);//不显示删除的 }string where = condition.BuildConditionSql().Replace("Where", "");//如果是单击节点得到的条件,则使用树列表的,否则使用查询条件的 if (!string.IsNullOrEmpty(treeConditionSql))
{
where = treeConditionSql + "AND Deleted = 0";//不显示删除的 }//数据权限的过滤:过滤规则,如果指定公司,以公司过滤,如果进一步指定部门,以公司+部门进行过滤;否则以个人的数据展示//如果过滤条件不为空,那么需要进行过滤
if (!string.IsNullOrEmpty(this.DataFilterCondition))
{
where += string.Format(" AND {0}", this.DataFilterCondition);
}
return where;
}

我们主要关注下上面红色部分即可,因为我们已经加上了标准的过滤条件了,这样我们就可以看到自己管理的数据了。

为了实现统一的数据控制,我们要求整个业务表的设计,需要引入下面几个标准的字段,这样就能很好使用过滤条件进行数据的过滤了。

前面也介绍到了,窗体可以统一刷新,其奥秘就是它们遵循统一的一个数据加载接口,我们初始化窗体数据的函数代码如下所示。

        /// <summary>
        ///编写初始化窗体的实现,可以用于刷新/// </summary>
        public override voidFormOnLoad()
{
InitDictItem();

BindData();
InitCustomerPage();
}

所以它们就能够统一调用FormOnLoad来统一刷新数据,就是这个道理。

                    //遍历全部窗口,更新
                    foreach (WHC.Framework.BaseUI.BaseDock form in this.MdiChildren)
{
form.SelectedCompanyID
=item.Value;
form.DataFilterCondition
=filterCondition;
form.FormOnLoad();
}

以上就是我对数据权限控制的一些心得和实现思路,希望大家能够体会其中的思路,并批判性的提出意见和建议。

在做淘宝API的学习过程中,发现一个API的封装类库Top4NET对操作封装的比较好,试用了一下,效果非常不错,另外由于淘宝的API对象参数很多,多数是英文的,另一方面Top4NET的实体类对象备注信息很详细,因此可以考虑吧实体类的属性描述信息作为列表的表头说明使用。

我们看到他的Domain里面的实体类信息描述很详细,代码如下所示:



代码


///

<summary>


///
商品结构

///

</summary>


[Serializable]
[JsonObject]
[XmlRoot(

"
item
"
)]

public

class
Item : BaseObject
{

///

<summary>


///
商品编号

///

</summary>


[JsonProperty(
"
iid
"
)]
[XmlElement(

"
iid
"
)]

public

string
Iid {
get
;
set
; }


///

<summary>


///
商品地址

///

</summary>


[JsonProperty(
"
detail_url
"
)]
[XmlElement(

"
detail_url
"
)]

public

string
DetailUrl {
get
;
set
; }


///

<summary>


///
商品数字编号

///

</summary>


[JsonProperty(
"
num_iid
"
)]
[XmlElement(

"
num_iid
"
)]

public

string
NumIid {
get
;
set
; }


///

<summary>


///
商品标题

///

</summary>


[JsonProperty(
"
title
"
)]
[XmlElement(

"
title
"
)]

public

string
Title {
get
;
set
; }


///

<summary>


///
卖家昵称

///

</summary>


[JsonProperty(
"
nick
"
)]
[XmlElement(

"
nick
"
)]

public

string
Nick {
get
;
set
; }

我们看看Top4NET类库生成的字段描述信息,它是一个叫做Top4Net.xml的文件(VS自动生成出来的),结构如下所示:

这些是很详细的类库备注信息,如果能在代码中读取对应的属性备注信息出来,那么各项内容的显示,就不需要太多的手工字段的转义工作了,虽然在性能上有点点损失,但是开发效率上会提高不少。

现在的问题就是该如何才能正确读取到相关的信息,这个操作可以通过XML文档的检索实现的,我在测试的时候,发现可以通过3种方式来读取到正确节点的信息的。其中上面的Member中的Name第一个字符有T、P、M等字样,其中的T是类,P是属性,M是方法的意思。



代码


///

<summary>


///
根据类属性名称,获取对应的备注信息(如果键名不存在,返回空)

///

</summary>


///

<param name="classPropertyName">
类全局名称(带命名空间)
</param>


///

<returns></returns>



public

static

string
GetPropertySummary(
string
classPropertyName)
{

string
filePath
=
Path.Combine(Application.StartupPath,
"
Top4Net.xml
"
);

string
keyName
=

string
.Format(
"
//doc/members/member[@name='P:{0}']/summary
"
, classPropertyName);

XPathDocument doc

=

new
XPathDocument(filePath);
XPathNavigator nav

=
doc.CreateNavigator();
XPathNodeIterator iterator

=
nav.Select(keyName);


string
result
=

""
;

try

{

if
(iterator.MoveNext())
{
XPathNavigator nav2

=
iterator.Current.Clone();
result

+=
nav2.Value;
}
}

catch
(Exception ex)
{
;
}


return
result.Trim();
}


///

<summary>


///
根据类属性名称,获取对应的备注信息(如果键名不存在,返回空)

///

</summary>


///

<param name="classPropertyName">
类全局名称(带命名空间)
</param>


///

<returns></returns>



public

static

string
GetPropertySummary2(
string
classPropertyName)
{

string
filePath
=
Path.Combine(Application.StartupPath,
"
Top4Net.xml
"
);

string
keyName
=

string
.Format(
"
//doc/members/member[@name='P:{0}']/summary
"
, classPropertyName);

XmlDocument document

=

new
XmlDocument();
document.Load(filePath);


string
result
=

""
;
XmlNode node

=
document.SelectSingleNode(keyName);

if
(node
!=

null
)
{
result

=
node.FirstChild.Value;
}


return
result.Trim();
}


///

<summary>


///
根据类属性名称,获取对应的备注信息(如果键名不存在,返回空)

///

</summary>


///

<param name="classPropertyName">
类全局名称(带命名空间)
</param>


///

<returns></returns>



public

static

string
GetPropertySummary3(
string
classPropertyName)
{

string
strReturn
=

string
.Empty;

try

{

string
keyName
=

string
.Format(
"
P:{0}
"
, classPropertyName);

string
filePath
=
Path.Combine(Application.StartupPath,
"
Top4Net.xml
"
);
XmlDocument document

=

new
XmlDocument();
document.Load(filePath);

XmlNodeList nodes

=
document.GetElementsByTagName(
"
member
"
);

for
(
int
i
=

0
; i
<
nodes.Count; i
++
)
{

//
获得将当前元素的key属性


XmlAttribute attribute
=
nodes[i].Attributes[
"
name
"
];

//
根据元素的第一个属性来判断当前的元素是不是目标元素



if
(attribute
!=

null

&&
(attribute.Value
==
keyName))
{
XmlNode node

=
nodes[i].ChildNodes[
0
];

if
(node
!=

null
)
{
strReturn

=
node.FirstChild.Value;

break
;
}
}
}
}

catch

{
;
}


return
strReturn.Trim();
}

由于上述的方法参数classPropertyName是类的全称名字,因此我们需要通过反射方式获取对应的属性名称信息,由于属性名称的简称对我们也有用,因此编写一个API函数,返回Dictionary<string,string>类型即可。



代码


///

<summary>


///
获取类属性的名称和全称字典列表

///

</summary>


///

<param name="obj"></param>


///

<returns></returns>



public

static
Dictionary
<
string
,
string
>
GetPropertyFullNameDict(
object
obj)
{
Dictionary

<
string
,
string
>
dict
=

new
Dictionary
<
string
,
string
>
();
Type t

=
obj.GetType();

PropertyInfo[] pi

=
t.GetProperties();

foreach
(PropertyInfo info
in
pi)
{

if
(
!
dict.ContainsKey(info.Name))
{
dict.Add(info.Name,

string
.Format(
"
{0}.{1}
"
, t.FullName, info.Name));
}
}


return
dict;
}

最后我们在进一步封装,把实体类对象的备注信息取出来放到字典对象中,如下所示:



代码


///

<summary>


///
获取类属性名称和描述信息的对照字典

///

</summary>


///

<param name="obj">
实体类对象
</param>


///

<returns></returns>



public

static
Dictionary
<
string
,
string
>
GetPropertyAliasDict(
object
obj)
{
Dictionary

<
string
,
string
>
dict
=

new
Dictionary
<
string
,
string
>
();
Dictionary

<
string
,
string
>
propertyNameDict
=
XmlHelper.GetPropertyFullNameDict(obj);
StringBuilder sb

=

new
StringBuilder();

foreach
(
string
simpleName
in
propertyNameDict.Keys)
{

string
summary
=
XmlHelper.GetPropertySummary(propertyNameDict[simpleName]);

if
(
string
.IsNullOrEmpty(summary))
{
summary

=
simpleName;
//
如果找不到对象的名称,那么取其属性名称作为Summary信息


}


if
(
!
dict.ContainsKey(simpleName))
{
dict.Add(simpleName, summary);
}
}

return
dict;
}

有了这个API函数,我们就可以随意的在界面中,通过极少的代码,实现表头中文化的自动转义了。下面我给我参考代码:



代码


//
查询商品信息(不含类别)


ItemsGetRequest itemReq
=

new
ItemsGetRequest();
itemReq.Fields

=

"
iid,title,nick,pic_url,cid,price,type,delist_time,post_fee,score,volume,location.city,location.state
"
;
itemReq.Query

=

"
笔记本
"
;

//
itemReq.Cid = "14";


itemReq.OrderBy
=

"
volume:desc
"
;
itemReq.PageNo

=

1
;
itemReq.PageSize

=

40
;


#region
给显示控件添加别名解析



//
特殊字段采用手工修改



this
.winGridView1.AddColumnAlias(
"
Created
"
,
"
创建时间
"
);

this
.winGridView1.AddColumnAlias(
"
Modified
"
,
"
修改时间
"
);


//
其他字段代码自动转义


Dictionary
<
string
,
string
>
dictAlias
=
XmlHelper.GetPropertyAliasDict(
new
Item());

foreach
(
string
simpleName
in
dictAlias.Keys)
{

this
.winGridView1.AddColumnAlias(simpleName, dictAlias[simpleName]);
}

#endregion



//
显示列表信息


ResponseList
<
Item
>
itemRsp
=
client.Execute(itemReq,
new
ItemListJsonParser());

this
.winGridView1.DataSource
=
itemRsp.Content;

this
.winGridView1.PrintTitle
=

"
查询商品信息(不含类别)
"
;

最后给出一个测试程序的界面截图,供参考:

取实体类备注信息作为界面展示用途,还可以应用在很多其他地方,淘宝Top4NET只是一个恰当的应用场景而已,对于大量备注完整的实体类,使用这些资源应该还是比较不错的选择。