2023年2月

在实际业务系统的开发中,往往需要几个系统协同一起出来同一个数据库的数据,数据库可以是同一个数据库,也可以根据业务拆分的多个数据库,如我们企业微信的应用、后台业务管理系统、以及专门为数据提供服务的API服务接口等,这样可以极大程度上给我们的业务数据提供支撑,并根据不同的特性进行分开管理和维护。

1、几个系统的协作关系

就上面的示意图来说,我们分为了三个部分,一个是微信管理后台,一个是业务管理后台,一个是对外API接口服务,不同的系统负责不同的功能。

微信管理系统,我以微信开发框架为基础进行扩展开发,满足多账户的管理、多类型账户管理的需求,如可以配置管理公众号、企业微信、小程序等信息,实现菜单和业务接口的对接关系。

业务管理系统,我以Winform混合框架或者Bootstrap框架为桥梁,构建特定的业务管理系统,这些数据都是来源于云端同一份数据库,这样有利于数据库的集中和生产环境数据的控制管理。

1)Web API模式介绍

对外接口API服务,我以Web API接口服务,发布可以用于多个系统、或者对外的API服务,提供数据查询或者业务数据处理的服务。

如果考虑Web API优先,我们可以所有系统都基于Web API 基础进行开发。

Web API层作为一个公共的接口层,我们就很好保证了各个界面应用层的数据一致性,不过这样可能没有直接开发来的快捷。

2)混合框架模式介绍

混合框架是Winform界面的分布式系统。

混合框架的核心是以相对独立的方式,整合各个可重用的模块,如权限管理、字典管理、附件管理、人员管理等模块,我们可以遵循一定的基础上,快速构建统一的应用平台。这个是基于WInform构建的分布式应用,它是通过Web API和服务端进行交互的。

大致来说,系统的界面效果如下所示,根据业务模块的不同,我们可以动态增加很多菜单模块,以方便对业务数据的管理。

例如我们基于混合框架基础上开发的CRM客户关系管理系统。

其中权限管理模块是通用的,字典管理模块也是一样,直接调用就可以了。

3)Bootstrap的Web框架介绍

我们开发的基于MVC的Bootstrap的Web开发框架,可以快速实现后台框架代码和界面代码的生成,能够快速应用于实际项目的开发中去,同样也是模块的整合,和混合框架的权限、字典等基础模块通用,并重用同一个数据库架构信息。

根据不同的业务需要,我们可以开发不同的业务系统管理应用。

这个系统基于MVC的Web业务快速开发,可以通过代码生成工具进行后台代码的开发,界面快速生成等处理,可以快速生成分页,查询,打印,导入,导出、编辑、增加、查看等处理功能。

4)微信开发框架介绍

微信开发框架也是基于Bootstrap开发框架基础上进行开发的,采用基于
MVC + Bootstrap + Ajax + Enterprise Library
的技术路线,界面层采用Boostrap + Metronic组合的前端框架,数据访问层支持Oracle、SQLServer、MySQL、PostgreSQL等数据库。

微信开发框架整合了公众号、企业微信、小程序等不同类型的账号管理,并对这几个类型的微信账号提供服务端接口的封装,以及各种业务案例。

企业微信的菜单管理界面

为了有效的开发不同的应用开发,我们底层数据库完全兼容,各个模块底层的逻辑也是相同的,这样就包含Winform框架、Winform混合框架、Bootstrap业务系统、微信后台管理系统这些系统模块的重用,以及数据库的重用,特别是菜单模块、权限模块、字典模块等基础信息完全一致的使用,管理起来更加方便。

当我们需要结合企业微信和业务系统的时候,我们需要建立一个企业微信应用,然后在微信管理后台中绑定对应参数,这样可以利用企业微信的服务端API对接相关的功能,包括提交菜单,以及获取对应的企业微信组织机构、发送消息等常规操作;而业务系统则可以把对应的业务流程和企业微信进行对接,包括消息的推送,以及利用JSSDK对相关业务数据的展示和处理等。本篇随笔介绍在利用企业微信前的相关处理步骤,如创建企业应用,绑定企业应用信息,以及相关的业务处理交互等。

1、创建企业微信应用

首先我们需要注册申请一个企业微信的账号,注册申请需要提交相关的企业资质信息,这里就不再赘述。

有了企业微信账号后,我们扫码进入企业微信的管理后台,在【应用与小程序】模块中创建一个应用,这个就是我们对应的业务应用了,企业微信可以创建多个不同场景的业务应用,其本身也自带了很多相关的应用在里面。

创建应用,我们需要录入相关的应用信息和Logo图标信息,如下界面所示。

例如我创建的一个企业微信应用,我们记住它的应用ID和它的秘钥,这个是一个很重要的信息,需要绑定在微信系统里面,我们后面对相关的企业微信服务端API接口调用,都需要利用到这些参数的。

如果我们的企业微信应用设计到支付信息的,需要打开企业支付的应用获得对应的秘钥信息的,如下所示。

打开【企业支付】就可以看到支付对应的密钥了,这个很重要,如我们需要利用企业微信发送红包或者直接付款给员工的时候,这个就需要用到了,如我前面随笔介绍的《
企业微信支付的发送红包及相关接口使用

2、绑定企业微信相关参数

在企业微信后台创建对应的应用,并获得对应的应用ID、密钥等信息后,我们可以在微信后台管理系统里面录入我们获得的应用信息了。

一般来说,微信后台管理系统能够管理不同类型:公众号、企业微信、小程序等类型的账号信息,我们在对应的分类里面录入相关的企业应用信息,如下录入界面所示。

有了这些对应的信息后,我们可以对企业微信应用编辑相关的菜单信息,然后通过接口提交到微信服务端上去,这样我们的应用就具有了我们自定义的业务菜单了,如下是微信管理系统里面对菜单的管理。

菜单列表管理界面如下所示。

这样配置好菜单并设置了相关的处理事件或者对应的JSSDK页面路径后,我们就可以提交到服务端上去,马上就可以看到企业应用的菜单变化了。

以上就是实际应用的菜单界面效果,这样我们企业应用就有了相关的处理入口了,有些是扫码事件,有些是自定义事件,有些则是JSSDK编写的业务入口,如资产录入、盘点任务等。

3、业务处理对接

有了具体的菜单入口,我们需要处理我们入口的处理逻辑了,如果是扫码,我们需要在后台进行一定的事件响应,如在资产查看里面扫码后发送一个文本信息,供跳转到相关的查看资产信息界面上去。

在之前随笔介绍过对这些事件的处理,如《
C#开发微信门户及应用(21)-微信企业号的消息和事件的接收处理及解密
》,如果需要了解企业微信发送消息的过程,可以参考下《
企业微信发送应用消息的实现
》随笔的介绍即可。

例如对于扫码入口,企业微信的后台对这些事件进行捕捉,并匹配到对应的处理模块上去处理,如下代码所示。

        /// <summary>
        ///扫码推事件且弹出“消息接收中”提示框的事件推送的处理/// </summary>
        /// <param name="info">扫描信息</param>
        /// <returns></returns>
        public stringHandleEventScancodeWaitmsg(RequestEventScancodeWaitmsg info, AccountInfo accountInfo)
{
string result = "";try{var handler = AutoFactory.Instatnce.Container.Resolve<IQRCodeHandler>();if (handler != null)
{
result
=handler.HandleScancodeWaitmsg(info, accountInfo);
}
}
catch(Exception ex)
{
LogHelper.Error(ex);
}
returnresult;
}

通过 AutoFactory.Instatnce.Container.Resolve<IQRCodeHandler>(); 我们可以看到业务的流程调整到了IOC的一个配置处理模块上去了,这里利用了Autofac的配置信息自动加载对应的处理模块。

对于扫码处理逻辑,我们显示根据资产信息,构建一个文本消息发给企业微信客户端,然后引导用户打开响应的连接就可以跳转到对应的资产信息查看界面上去了,如下所示。

        /// <summary>
        ///处理扫码结果/// </summary>
        /// <param name="info"></param>
        /// <returns></returns>
        private stringHandleScanResult(RequestEventScanCode info, AccountInfo accountInfo)
{
ResponseText response
= newResponseText(info);if (info.ScanCodeInfo != null)
{
response.Content
= string.Format("您的信息为:{0},可以结合后台进行数据查询。", info.ScanCodeInfo.ScanResult);var isUrl =ValidateUtil.IsURL(info.ScanCodeInfo.ScanResult);if (!isUrl)
{
string assetcode =info.ScanCodeInfo.ScanResult;if (!string.IsNullOrEmpty(assetcode))
{
response.Content
=ConvertAssetInfo(info, accountInfo, assetcode);
}
else{
response.Content
= string.Format("扫码内容为空。");
}
}
}
var result =response.ToXml();returnresult;
}
/// <summary> ///转换资产信息为文本消息/// </summary> /// <returns></returns> private string ConvertAssetInfo(RequestEventScanCode info, AccountInfo accountInfo, stringassetcode)
{
StringBuilder sb
= newStringBuilder();
BLLFactory
<Asset>.Instance.SetConfigName("workflow");var assetInfo = BLLFactory<Asset>.Instance.FindByCode(assetcode);if (assetInfo != null)
{
//~资产代码~、资产名称、管理部门、使用部门、使用人、存放地点、数量、资产动态、在用类型 sb.AppendFormat("资产代码:{0}", assetInfo.Code).Append("\n");
sb.AppendFormat(
"资产名称:{0}", assetInfo.Name).Append("\n");
sb.AppendFormat(
"管理部门:{0}", assetInfo.ChargeDept).Append("\n");
sb.AppendFormat(
"使用部门:{0}", assetInfo.CurrDept).Append("\n");
sb.AppendFormat(
"使用人:{0}", assetInfo.UsePerson).Append("\n");
sb.AppendFormat(
"存放地点:{0}", assetInfo.KeepAddr).Append("\n");
sb.AppendFormat(
"数量:{0}", assetInfo.Qty).Append("\n");
sb.AppendFormat(
"资产动态:{0}", assetInfo.Status).Append("\n");
sb.AppendFormat(
"在用类型:{0}", assetInfo.UseType).Append("\n");

sb.AppendFormat(
"<a href='{0}/QyH5/AssetInfo?code={1}' >点击查看详细信息</a>", WebsiteDomain, assetInfo.Code);
}
else{
sb.AppendFormat(
"资产代码【{0}】不存在。", assetcode).Append("\n");
sb.AppendFormat(
"<a href='{0}/QyH5/Asset?devicecode={1}' >点击添加设备信息</a>", WebsiteDomain, assetcode);
}
returnsb.ToString();
}

其他的部分入口是通过编写JSSDK页面的方式实现业务数据的处理的,在JSSDK编写里面,我们有时候需要获取当前登录的用户身份信息,如企业微信的userid,那么我们就可以通过跳转的方式获取code,然后根据对应的code解析为userid即可。

            //获取企业微信账号信息
            var accountInfo =GetAccount(ConfigData.CorpAccountNo);//如果传入了userid,使用传入的参数
            if (string.IsNullOrEmpty(userid))
{
//通过重定向的code获取对应的UserId userid =GetUserId(accountInfo.AppID, accountInfo.AppSecret);
}

而为了避免反复的解析code参数导致出错(code只能被用一次,后续再用会出错的),那么我们可以把用户的userid存放在session里面,这样判断如果这个Session不存在了,我们再解析code就没问题了。

        /// <summary>
        ///根据当前的Code获取对应的openid(获取获取openid的参数)/// </summary>
        /// <param name="accountInfo">登陆账号信息</param>
        /// <returns></returns>
        protected virtual string GetUserId(string appid, stringappsecret)
{
string userId = Request.QueryString["userid"];if (string.IsNullOrEmpty(userId))
{
var user_id = Session["user_id"];if (user_id != null)
{
userId
=user_id.ToString();
}
else{//如果没有传递userId,那么就根据code参数获取userId string code = Request.QueryString["code"];if (!string.IsNullOrEmpty(code))
{
string accessToken =baseApi.GetAccessToken(appid, appsecret);var result =baseApi.GetUserInfo(accessToken, code);if (result != null)
{
userId
=result.UserId;
Session[
"user_id"] = userId;//存储在Session }
}
}
}
returnuserId;
}

下面是一个资产信息录入的界面效果,利用JSSDK进行编写的页面。

其中的一些选择框,我们可以填写相关的字典数据,或者列表信息供选择。

这些数据最终可以提交到业务管理系统里面,从而实现了企业微信和业务管理系统的数据流对接。

还有一个如盘点任务一样的工作,我们可以交给企业微信端进行处理,通过手机进行移动端的数据处理,更加方便。

以上就是企业微信和业务管理系统的交互过程的一部分,我们具体可以根据自己的业务需要,扩展很多相关的处理页面。

通过整合企业微信和业务系统的数据流,我们可以更加方便的了解企业业务信息,也更加方便的利用手机终端进行一些快捷的查询或者处理业务操作。

在一些场合里面,我们需要对一些记录进行颜色标注处理,以标记记录的信息重要性或者进行类别区分,如在客户关系管理系统里面,我们需要对客户信息进行不同颜色的标注,方便对不同类别的管理,毕竟颜色区分还是比较直观快速的一种。本随笔介绍在我们的CRM客户关系管理系统中,对客户信息的标注处理和显示区分的实现。

1、客户信息的分类

在一个CRM客户关系管理系统里面,客户信息的种类比较多,用来标记不同类别,同时也是方便我们对不同类别信息进行的快速筛选查询,如下是客户关系管理系统里面对于客户信息的展示,左边树形列表里面,列出很多客户属性的类别。

如客户状态、客户类型、客户级别、客户行业等等不同的属性分类,除了这些分类,我们还可以对客户记录进行颜色标注,这样可以更加直观区分这个客户属于那种特定的类型,为了方便,我们预设几种比较好区分的颜色进行处理即可,不需要设计太多的颜色,以便以7个为标准就可以了,太多也没有意义。

在编辑客户信息的时候,我们可以设置该记录的颜色选项,如下面界面所示。

设置了不同类别颜色的客户信息后,我们可以在记录的行里面用不同的颜色进行区分显示,这样看起来就更加直观了。

下面我们来看看具体的实现过程代码。

2、颜色标注的实现过程

在编辑界面添加相关的标签控件和一个下拉按钮的控件,如下界面所示。

其中标注颜色的下拉按钮是DevExpress的DropDownButton控件。

初始化创建下拉按钮的代码如下所示。

        /// <summary>
        ///创建标记颜色的下拉按钮/// </summary>
        /// <returns></returns>
        privateDXPopupMenu CreateDXPopupMenu()
{
var menu = newDXPopupMenu();var dict =ColorHelper.ColorDict;foreach(string key indict.Keys)
{
menu.Items.Add(CreateMenuItem(key, dict[key]));
}
//menu.Items.Add(CreateMenuItem("橙色", Color.Orange));//menu.Items.Add(CreateMenuItem("黄色", Color.Yellow));//menu.Items.Add(CreateMenuItem("绿色", Color.Green));//menu.Items.Add(CreateMenuItem("蓝色", Color.Blue));//menu.Items.Add(CreateMenuItem("紫色", Color.Purple));//menu.Items.Add(CreateMenuItem("黑色", Color.Black)); returnmenu;
}

其中我们在重构的时候,移除了硬编码的颜色记录,改用一个辅助类来处理颜色字典,这样可以方便修改。

    internal static classColorHelper
{
private static Dictionary<string, Color> colorDict = new Dictionary<string, Color>();staticColorHelper()
{
colorDict.Add(
"红色", Color.Red);
colorDict.Add(
"橙色", Color.Orange);//colorDict.Add("黄色", Color.Yellow); colorDict.Add("绿色", Color.Green);
colorDict.Add(
"蓝色", Color.Blue);
colorDict.Add(
"紫色", Color.Purple);
colorDict.Add(
"黑色", Color.Black);
colorDict.Add(
"", Color.Empty);
}
/// <summary> ///颜色字典/// </summary> public static Dictionary<string, Color>ColorDict
{
get { returncolorDict; }
}
}

创建按钮并添加对应的单击事件代码

        private DXMenuItem CreateMenuItem(stringtext, Color color)
{
var item = newDXMenuItem(text, OnItemClick);
item.Appearance.BackColor
=color;returnitem;
}
private void OnItemClick(objectsender, EventArgs e)
{
DXMenuItem item
= sender asDXMenuItem;if(item != null)
{
this.lblMarkColor.BackColor =item.Appearance.BackColor;//更新颜色 string color = ColorTranslator.ToHtml(this.lblMarkColor.BackColor);if(!string.IsNullOrEmpty(ID))
{
BLLFactory
<Customer>.Instance.MarkColor(ID, color);
}
}
}

其中我们注意到了,我们使用

ColorTranslator.ToHtml

函数来转换对应的颜色代码到一个字符串来存储数据库里面,这个函数是系统内辅助类的函数。

当我们需要展示字符串的颜色信息,需要把它转换为对应的颜色枚举,如下所示。

                    //标记颜色
                    if (!string.IsNullOrEmpty(info.MarkColor))
{
this.lblMarkColor.BackColor =ColorTranslator.FromHtml(info.MarkColor);
}

在列表界面 里面,如果我们需要设置某个单元格颜色信息,那么我们通过添加这个实现就可以。

            this.winGridViewPager1.gridView1.RowCellStyle += new DevExpress.XtraGrid.Views.Grid.RowCellStyleEventHandler(gridView1_RowCellStyle);
       void gridView1_RowCellStyle(objectsender, DevExpress.XtraGrid.Views.Grid.RowCellStyleEventArgs e)
{
string columnName =e.Column.FieldName;if (columnName == "Name")
{
bool deleted = (bool)this.winGridViewPager1.gridView1.GetRowCellValue(e.RowHandle, "Deleted");
Color color
=Color.White;if(deleted)
{
e.Appearance.BackColor
=Color.Red;
e.Appearance.BackColor2
=Color.LightCyan;
}
//根据用户选择的标记颜色,设置单元格的颜色信息 string markcolor = string.Concat(this.winGridViewPager1.gridView1.GetRowCellValue(e.RowHandle, "MarkColor"));if(!string.IsNullOrEmpty(markcolor))
{
color
=ColorTranslator.FromHtml(markcolor);
e.Appearance.BackColor
=color;
e.Appearance.BackColor2
=Color.White;
}
}
}

在客户信息的左侧树形列表里面,我们初始化了一个标记颜色的树形列表,这里也是根据颜色信息进行一个条件的处理即可。

初始化树形列的代码如下所示。

            //标记颜色的树形列表展示
            var colorNode = new TreeNode("标记颜色", 0, 0);this.treeView1.Nodes.Add(colorNode);var dict =ColorHelper.ColorDict;foreach (string key indict.Keys)
{
TreeNode subNode
= new TreeNode(key, 9, 9);var color =ColorTranslator.ToHtml(dict[key]);string filter = "";if (string.IsNullOrEmpty(color))
{
filter
+= "(MarkColor ='' or MarkColor is null)";
}
else{
filter
= string.Format("{0}='{1}'", "MarkColor", color);
}
subNode.Tag
=filter;//增加数值//如果过滤条件不为空,那么需要进行过滤 if (!string.IsNullOrEmpty(this.ShareUserCondition))
{
filter
= string.Format("{0} AND {1}", this.ShareUserCondition, filter);
}
int count = BLLFactory<Customer>.Instance.GetRecordCount(filter);
subNode.Text
+= string.Format("({0})", count);//避免透明不显示字体 subNode.ForeColor =dict[key];
colorNode.Nodes.Add(subNode);
}

这样我们就可以根据树节点的条件来进行过滤数据了。

我们再来回顾下列表界面的整体情况。

之前一直使用各种报表工具,如RDLC、DevExpress套件的XtraReport报表,在之前一些随笔也有介绍,最近接触锐浪的Grid++报表,做了一些测试例子和辅助类来处理报表内容,觉得还是很不错的,特别是它的作者提供了很多报表的设计模板案例,功能还是非常强大的。试着用来做一些简单的报表,测试下功能,发现常规的二维表、套打、条形码二维码等我关注的功能都有,是一个比较强大的报表控件,本篇随笔主要介绍在Winform开发中使用Grid++报表设计报表模板,以及绑定数据的处理过程。

1、报表模板设计

这个报表系统,报表模板提供了很多案例,我们可以大概浏览下其功能。

它对应在相应的文件目录里面,我们可以逐一查看了解下,感觉提供这么多报表还是很赞的,我们可以参考着来用,非常好。

整个报表主要是基于现有数据进行一个报表的模板设计的,如果要预览效果,我们一般是需要绑定现有的数据,可以从各种数据库提供数据源,然后设计报表模板,进行实时的数据和格式查看及调整。

空白的报表模板大概如下所示,包含页眉页脚,以及明细表格的内容。

根据它的教程,模仿着简单的做了一个报表,也主要是设计报表格式的调整,和数据源的处理的关系,我们做一个两个报表就可以很快上手了。

为了动态的加入我们表格所需要的列,我们可以通过数据库里面的字段进行加入,首先提供数据源,指定我们具体的表即可(如果是自定义的信息,则可以手工添加字段)

这个里面就是配置不同的数据库数据源了

如SQLServer数据库的配置信息如下。

为了方便,我们可以利用案例的Access数据库,也就是Northwind.mdb来测试我们的报表,弄好这些我们指定对应的数据表数据即可。

这里面配置好数据库表信息后,我们就可以用它生成相关的字段和对应的列信息了

修改列的表头,让它符合中文的表头列,如下所示。

我们在页脚出,加入了打印时间,页码的一些系统变量,具体操作就是添加一个综合文本,然后在内容里面插入指定的域内容即可,如下所示

预览报表,我们就可以看到具体的报表格式显示了。

通过上面的操作,感觉生成一个报表还是很方便的,接着我有根据需要做了一个二维码的报表显示,方便打印资产标签。

绑定数据源显示的报表视图如下所示,看起来还是蛮好的。

2、数据绑定

一般我们绑定数据源,有的时候可以直接指定数据库连接,有时候可以绑定具体的数据列表,如DataTable或者List<T>这样的数据源,不同的方式报表控件的代码绑定不同。

直接绑定数据表的路径如下所示。

        /// <summary>
        ///普通连接数据库的例子-打印预览/// </summary>
        private void btnNormalDatabase_Click(objectsender, EventArgs e)
{
Report
= newGridppReport();string reportPath = Path.Combine(Application.StartupPath, "Reports\\testgrid++.grf");string dbPath = Path.Combine(Application.StartupPath, "Data\\NorthWind.mdb");//从对应文件中载入报表模板数据 Report.LoadFromFile(reportPath);//设置与数据源的连接串,因为在设计时指定的数据库路径是绝对路径。 if (Report.DetailGrid != null)
{
string connstr =Utility.GetDatabaseConnectionString(dbPath);
Report.DetailGrid.Recordset.ConnectionString
=connstr;
}

Report.PrintPreview(
true);
}

而如果需要绑定和数据库无关的动态数据源,那么就需要通过控件的FetchRecord进行处理了,如下代码所示。

Report.FetchRecord += new _IGridppReportEvents_FetchRecordEventHandler(ReportFetchRecord);

通过这样我们增加每一个对应的列单元格信息,如下是随带案例所示

        //在C#中一次填入一条记录不能成功,只能使用一次将记录全部填充完的方式
        private voidReportFetchRecord()
{
//将全部记录一次填入 Report.DetailGrid.Recordset.Append();
FillRecord1();
Report.DetailGrid.Recordset.Post();

Report.DetailGrid.Recordset.Append();
FillRecord2();
Report.DetailGrid.Recordset.Post();

Report.DetailGrid.Recordset.Append();
FillRecord3();
Report.DetailGrid.Recordset.Post();
}
private voidFillRecord1()
{
C1Field.AsString
= "A";
I1Field.AsInteger
= 1;
F1Field.AsFloat
= 1.01;
}
private voidFillRecord2()
{
C1Field.AsString
= "B";
I1Field.AsInteger
= 2;
F1Field.AsFloat
= 1.02;
}
private voidFillRecord3()
{
C1Field.AsString
= "C";
I1Field.AsInteger
= 3;
F1Field.AsFloat
= 1.03;
}

这样处理肯定很麻烦,我们常规做法是弄一个辅助类,来处理DataTable和List<T>等这样类型数据的动态增加操作。

        /// <summary>
        ///绑定实体类集合的例子-打印预览/// </summary>
        private void btnBindList_Click(objectsender, EventArgs e)
{
Report
= newGridppReport();//从对应文件中载入报表模板数据 string reportPath = Path.Combine(Application.StartupPath, "Reports\\testList.grf");
Report.LoadFromFile(reportPath);
Report.FetchRecord
+=ReportList_FetchRecord;

Report.PrintPreview(
true);
}
/// <summary> ///绑定DataTable的例子-打印预览/// </summary> private void btnBindDatatable_Click(objectsender, EventArgs e)
{
Report
= newGridppReport();//从对应文件中载入报表模板数据 string reportPath = Path.Combine(Application.StartupPath, "Reports\\testList.grf");
Report.LoadFromFile(reportPath);
Report.FetchRecord
+=ReportList_FetchRecord2;

Report.PrintPreview(
true);
}
private voidReportList_FetchRecord()
{
List
<ProductInfo> list = BLLFactory<Product>.Instance.GetAll();
GridReportHelper.FillRecordToReport
<ProductInfo>(Report, list);
}
private voidReportList_FetchRecord2()
{
var dataTable = BLLFactory<Product>.Instance.GetAllToDataTable();
GridReportHelper.FillRecordToReport(Report, dataTable);
}

其中辅助类 GridReportHelper 代码如下所示。

    /// <summary>
    ///Gird++报表的辅助类/// </summary>
    public classGridReportHelper
{
private structMatchFieldPairType
{
publicIGRField grField;public intMatchColumnIndex;
}
/// <summary> ///将 DataReader 的数据转储到 Grid++Report 的数据集中/// </summary> /// <param name="Report">报表对象</param> /// <param name="dr">DataReader对象</param> public static voidFillRecordToReport(IGridppReport Report, IDataReader dr)
{
MatchFieldPairType[] MatchFieldPairs
= newMatchFieldPairType[Math.Min(Report.DetailGrid.Recordset.Fields.Count, dr.FieldCount)];//根据字段名称与列名称进行匹配,建立DataReader字段与Grid++Report记录集的字段之间的对应关系 int MatchFieldCount = 0;for (int i = 0; i < dr.FieldCount; ++i)
{
foreach (IGRField fld inReport.DetailGrid.Recordset.Fields)
{
if (string.Compare(fld.RunningDBField, dr.GetName(i), true) == 0)
{
MatchFieldPairs[MatchFieldCount].grField
=fld;
MatchFieldPairs[MatchFieldCount].MatchColumnIndex
=i;++MatchFieldCount;break;
}
}
}
//将 DataReader 中的每一条记录转储到Grid++Report 的数据集中去 while(dr.Read())
{
Report.DetailGrid.Recordset.Append();
for (int i = 0; i < MatchFieldCount; ++i)
{
var columnIndex =MatchFieldPairs[i].MatchColumnIndex;if (!dr.IsDBNull(columnIndex))
{
MatchFieldPairs[i].grField.Value
=dr.GetValue(columnIndex);
}
}
Report.DetailGrid.Recordset.Post();
}
}
/// <summary> ///将 DataTable 的数据转储到 Grid++Report 的数据集中/// </summary> /// <param name="Report">报表对象</param> /// <param name="dt">DataTable对象</param> public static voidFillRecordToReport(IGridppReport Report, DataTable dt)
{
MatchFieldPairType[] MatchFieldPairs
= newMatchFieldPairType[Math.Min(Report.DetailGrid.Recordset.Fields.Count, dt.Columns.Count)];//根据字段名称与列名称进行匹配,建立DataReader字段与Grid++Report记录集的字段之间的对应关系 int MatchFieldCount = 0;for (int i = 0; i < dt.Columns.Count; ++i)
{
foreach (IGRField fld inReport.DetailGrid.Recordset.Fields)
{
if (string.Compare(fld.Name, dt.Columns[i].ColumnName, true) == 0)
{
MatchFieldPairs[MatchFieldCount].grField
=fld;
MatchFieldPairs[MatchFieldCount].MatchColumnIndex
=i;++MatchFieldCount;break;
}
}
}
//将 DataTable 中的每一条记录转储到 Grid++Report 的数据集中去 foreach (DataRow dr indt.Rows)
{
Report.DetailGrid.Recordset.Append();
for (int i = 0; i < MatchFieldCount; ++i)
{
var columnIndex =MatchFieldPairs[i].MatchColumnIndex;if (!dr.IsNull(columnIndex))
{
MatchFieldPairs[i].grField.Value
=dr[columnIndex];
}
}
Report.DetailGrid.Recordset.Post();
}
}
/// <summary> ///List加载数据集/// </summary> /// <typeparam name="T"></typeparam> /// <param name="Report">报表对象</param> /// <param name="list">列表数据</param> public static void FillRecordToReport<T>(IGridppReport Report, List<T>list)
{
Type type
= typeof(T); //反射类型 MatchFieldPairType[] MatchFieldPairs= newMatchFieldPairType[Math.Min(Report.DetailGrid.Recordset.Fields.Count, type.GetProperties().Length)];//根据字段名称与列名称进行匹配,建立字段与Grid++Report记录集的字段之间的对应关系 int MatchFieldCount = 0;int i = 0;
MemberInfo[] members
=type.GetMembers();foreach (MemberInfo memberInfo inmembers)
{
foreach (IGRField fld inReport.DetailGrid.Recordset.Fields)
{
if (string.Compare(fld.Name, memberInfo.Name, true) == 0)
{
MatchFieldPairs[MatchFieldCount].grField
=fld;
MatchFieldPairs[MatchFieldCount].MatchColumnIndex
=i;++MatchFieldCount;break;
}
}
++i;
}
//将 DataTable 中的每一条记录转储到 Grid++Report 的数据集中去 foreach (T t inlist)
{
Report.DetailGrid.Recordset.Append();
for (i = 0; i < MatchFieldCount; ++i)
{
object objValue =GetPropertyValue(t, MatchFieldPairs[i].grField.Name);if (objValue != null)
{
MatchFieldPairs[i].grField.Value
=objValue;
}
}
Report.DetailGrid.Recordset.Post();
}
}
/// <summary> ///获取对象实例的属性值/// </summary> /// <param name="obj">对象实例</param> /// <param name="name">属性名称</param> /// <returns></returns> public static object GetPropertyValue(object obj, stringname)
{
//这个无法获取基类//PropertyInfo fieldInfo = obj.GetType().GetProperty(name, bf);//return fieldInfo.GetValue(obj, null);//下面方法可以获取基类属性 object result = null;foreach (PropertyDescriptor prop inTypeDescriptor.GetProperties(obj))
{
if (prop.Name ==name)
{
result
=prop.GetValue(obj);
}
}
returnresult;
}
}

绑定数据的报表效果如下所示

导出报表为PDF也是比较常规的操作,这个报表控件也可以实现PDF等格式文件的导出,如下所示。

        private void btnExportPdf_Click(objectsender, EventArgs e)
{
List
<ProductInfo> list = BLLFactory<Product>.Instance.GetAll();//从对应文件中载入报表模板数据 string reportPath = Path.Combine(Application.StartupPath, "Reports\\testList.grf");
GridExportHelper helper
= newGridExportHelper(reportPath);string fileName = "d:\\my.pdf";var succeeded =helper.ExportPdf(list, fileName);if(succeeded)
{
Process.Start(fileName);
}
}

以上就是利用这个报表控件做的一些功能测试和辅助类封装,方便使用。

之前在随笔《
在Winform开发中使用Grid++报表
》介绍了在Winform环境中使用Grid++报表控件,本篇随笔介绍在Bootstrap开发框架中使用Grid++报表,也就是Web环境中使用Grid++报表,对于我上篇提到的二维码条形码的资产信息表,我系统通过Web方式进行呈现,或者展示其报表的PDF也可以,本篇随笔一步步介绍如何使用Web报表的展示出来。

1、Grid++报表在Web端的处理

Grid++在Web端,提供了插件的方式展示,不过由于插件是采用COM方式处理的,这种模式限制了它支持浏览器有限。

通过上面表格,也就是说基本上我们应用比较广泛的Chrome和FireFox最新的都不支持了,由于我项目需求,我希望客户能够在不同的浏览器上实现报表的展示,那这种插件方式显示的方式就不太适合了。

不过Grid++报表还是提供了另外一种方式,也就是HTML5的方式展示,HTML5报表运行在所有支持HTML5的浏览器(目前较新版本的各种浏览器都支持HTML5)。

例如在Chrome浏览器里面展示HTML报表,效果如下所示。

这种方式还是挺棒的,不过我在做二维码打印的时候,发现和我设计的报表不太一样,我设计的报表样式效果如下所示。

而HTML报表给我的效果是如下所示。

也就是没能按我所需要的排列来展示,因此我希望在服务端生成PDF格式展示,因为PDF的效果还是最好的,不会变形,而且我可以通过PDFJS的插件实现在线预览效果,需要的时候,下载PDF打印内容即可。

2、Grid++报表在Web端的实现过程

前面介绍了我需要的报表效果和具体选型过程,在得出最终结果前,我们来一步步看看具体的实现效果。

对于一些简单的二维表,如果使用直接报表展示的方式还是很棒的,下面我们先来介绍通过HTML报表直接呈现的方式实现过程。

由于我这个Bootstrap开发框架是采用MVC开发的,我想把报表的展示放在Report.cshtml视图里面,那么我需要创建一个视图Action,如下所示。

        publicActionResult Report()
{
return View("Report");
}

这样,我们再创建一个Report.cshtml视图页面,如下所示。首先引入我们需要的JS,一般来说,引入grhtml5-6.6-min.js文件就可以了,由于我们需要使用Jquery的一些处理方式,因此JQueryJS也需要引入的。

@*此处添加一些Jquery相关的脚本,方便开发时刻自定义脚本的智能提示*@<scriptsrc="~/Content/metronic/assets/global/plugins/jquery.min.js"type="text/javascript"></script>
    <scriptsrc="~/Content/metronic/assets/global/plugins/jquery-migrate.min.js"type="text/javascript"></script>
    <scriptsrc="~/Content/metronic/assets/global/plugins/jquery-ui/jquery-ui.min.js"type="text/javascript"></script>

    <!--Grid++报表需要的JS文件-->
    <scriptsrc="~/Content/JQueryTools/GridReport/grhtml5-6.6-min.js"></script>

这样我们调用报表对象进行数据的处理即可,如下面我们测试一个案例的报表内容处理。

    <scriptlanguage="javascript"type="text/javascript">
        functionloadReport() {

rubylong.grhtml5.barcodeURL
= "/Barcode.ashx";//创建报表显示器,参数指定其在网页中的占位标签的ID,报表模板URL与报表数据URL不指定,而是在后面的AJAX操作中提供相关数据 varreportViewer=window.rubylong.grhtml5.insertReportViewer("report_holder");
reportViewer.reportPrepared
= false;//指定报表生成需要加载报表模板 reportViewer.dataPrepared= false;//指定报表生成需要加载报表数据 //报表在模板与数据都载入后会自动生成 //测试报表 $.ajax({
type:
"GET",
url:
"/Report/1a.SimpleList.grf",
data:
"",
success:
function(msg) {
reportViewer.loadReport(msg);
}
});
$.ajax({
type:
"GET",//url: "/Report/xmlCustomer.txt", //xml格式 url:"/Report/jsonCustomer.txt",//json格式 data:"",
success:
function(msg) {
reportViewer.loadData(msg);
}
});
//页面初始化 $(function() {
loadReport();
});
</script>

报表内容呈现在HTML元素里面,如下代码所示。

<body>
    <divid="report_holder">
    </div>
</body>

通过上面我们了解到它的报表内容的赋值处理,我们首先需要确定报表模板文件,以及动态生成报表呈现的数据,代替 jsonCustomer.txt 文件内容。

我们来看看这个文件的格式如下所示。

也就是我们如果需要动态生成数据,则构建这一个JSON格式输出即可。测试报表界面效果如下所示,二维表格展示效果还是很好的。

实际报表处理的时候,由于我需要动态展示数据,因此在JS接收一个参数,并根据参数动态构建数据即可。

如下是我实际的项目报表数据展示。

        functionloadReport(ids) {

rubylong.grhtml5.barcodeURL
= "/Barcode.ashx";//创建报表显示器,参数指定其在网页中的占位标签的ID,报表模板URL与报表数据URL不指定,而是在后面的AJAX操作中提供相关数据 var reportViewer = window.rubylong.grhtml5.insertReportViewer("report_holder");
reportViewer.reportPrepared
= false; //指定报表生成需要加载报表模板 reportViewer.dataPrepared = false; //指定报表生成需要加载报表数据 //报表在模板与数据都载入后会自动生成 //正式报表 $.ajax({
type:
"GET",
url:
"/Report/barcode.grf",
data:
"",
success:
function(msg) {
reportViewer.loadReport(msg);
}
});
$.ajax({
type:
"GET",//url: "/Report/jsonbarcode.txt", //json文件格式 url: "/Asset/GetRepotData?ids=" + ids,//动态json格式 data: "",
success:
function(msg) {
reportViewer.loadData(msg);
}
});
};

我通过 Asset / GetRepotData 的方法实现数据的动态生成。

        /// <summary>
        ///获取Grid++的JSON格式报表数据/// </summary>
        /// <returns></returns>
        public ActionResult GetRepotData(stringids)
{
ActionResult result
= Content("");if (!string.IsNullOrEmpty(ids))
{
var list = BLLFactory<Asset>.Instance.FindByIDs(ids);//构建一个合法格式的对象,进行序列号 var table = new{
Table
=list
};
result
=ToJsonContent(table);
}
returnresult;
}

其实就是构建一个顶级节点的Table对象,然后序列化为JSON即可。

这样我们就可以输出所需要的报表了,如下所示。

不过这样的报表,没有按照我需要的两列排列(估计也是报表的一个Bug吧),没有达到我的期望,因此我希望通过展示PDF的方式,实现严格格式的输出报表。

3、Grid++报表在Web端的PDF处理

报表控件,后台通过PDF的生成方式,我在之前那篇随笔《
在Winform开发中使用Grid++报表
》介绍了如果在Winform下直接导出PDF方式,具体如下所示。

        private void btnExportPdf_Click(objectsender, EventArgs e)
{
List
<ProductInfo> list = BLLFactory<Product>.Instance.GetAll();//从对应文件中载入报表模板数据 string reportPath = Path.Combine(Application.StartupPath, "Reports\\testList.grf");
GridExportHelper helper
= newGridExportHelper(reportPath);string fileName = "d:\\my.pdf";var succeeded =helper.ExportPdf(list, fileName);if(succeeded)
{
Process.Start(fileName);
}
}

而其中辅助类的ExportPdf方法就是直接利用报表客户端控件对象导出PDF的,如下代码所示。

        /// <summary>
        ///导出PDF,并返回路径/// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="list"></param>
        /// <returns></returns>
        public bool ExportPdf<T>(List<T> list, stringfilePath)
{
//从对应文件中载入报表模板数据 Report.LoadFromFile(this.ReportPath);
Report.FetchRecord
+= () =>{
FillRecordToReport
<T>(Report, list);
};
Report.ExportBegin
+= (IGRExportOption option) =>{
option.AbortOpenFile
= true; //导出后不用关联程序打开导出文件,如导出Excel文件之后不用Excel打开 option.FileName =filePath;switch(option.ExportType)
{
caseGRExportType.gretPDF:
option.AsE2PDFOption.Author
= "My Author";
option.AsE2PDFOption.Subject
= "My Subject";break;
}
};
var succeeded = Report.ExportDirect(GRExportType.gretPDF, filePath, false, false);returnsucceeded;
}

报表导出的效果和报表预览效果一致

不过我使用前面报表客户端的对象处理报表导出,在测试后台生成PDF的时候,提示内存损坏,应该就是COM处理的问题,不支持这样的方式,咨询作者后说需要使用 WEB报表服务器 方式来生成PDF。

改变一下处理过程,实际使用的是GridppReportServer对象来处理,实际代码如下所示。

        /// <summary>
        ///导出PDF/// </summary>
        /// <typeparam name="T">列表对象类型</typeparam>
        /// <param name="list">列表对象</param>
        /// <param name="filePath">存储路径</param>
        /// <param name="context"></param>
        /// <returns></returns>
        public bool ExportPdf<T>(List<T> list, stringfilePath, HttpContextBase context)
{
//从对应文件中载入报表模板数据 Report.LoadFromFile(this.ReportPath);//构建一个合法格式的对象,进行序列化 var table = new{
Table
=list
};
var json =JsonConvert.SerializeObject(table, Formatting.Indented);
Report.LoadDataFromXML(json);

IGRExportOption ExportOption
=Report.PrepareExport(GRExportType.gretPDF);var exportPdf =Report.ExportToBinaryObject();
Report.UnprepareExport();
var succeeded =exportPdf.SaveToFile(filePath);returnsucceeded;
}

有了这个辅助方法,我们就可以封装一下处理获得数据,并导出PDF文件的操作了

        /// <summary>
        ///根据选中的ID记录,生成对应的PDF报表,返回路径/// </summary>
        /// <param name="ids">选中的ID记录,逗号分开</param>
        /// <returns></returns>
        public ActionResult ExportPdf(stringids)
{
ActionResult result
= Content("");if (!string.IsNullOrEmpty(ids))
{
//利用接口获取列表数据 var list = BLLFactory<Asset>.Instance.FindByIDs(ids);//报表文件路径 string reportPath ="/Report/barcode.grf";//转换为物理路径 reportPath =Server.MapPath(reportPath);//导出PDF的文件路径 string exportPdfPath = string.Format("/GenerateFiles/{0}/AssetReport.pdf", CurrentUser.Name);//转换为物理路径 string realPath =Server.MapPath(exportPdfPath);//确保目录生成 string parentPath =Directory.GetParent(realPath).FullName;
DirectoryUtil.AssertDirExist(parentPath);
//生成PDF报表文档到具体文件 GridExportHelper helper = newGridExportHelper(reportPath);bool success =helper.ExportPdf(list, realPath, HttpContext);if(success)
{
result
= Content(exportPdfPath);//返回Web相对路径 }
helper.Dispose();
//销毁对象 }returnresult;
}

这些是后端的代码,要想在前端里面处理并预览PDF文件,需要加入前端的HTML代码

如下我们加入两个JS函数

        //查看明细信息
        functionViewPDF(ids) {
$.ajax({
type:
"GET",
url:
"/Asset/ExportPdf?ids=" +ids,
data:
"",
success:
function(filePath) {var baseUrl = "/Content/JQueryTools/pdfjs/web/viewer.html";var url = baseUrl + "?file=" + filePath;//实际地址 window.open(url);
}
});
};
functionAssetPrint() {var rows = $table.bootstrapTable('getSelections');if (rows.length == 0) {
showTips(
"请选择你要打印编码的记录");return;
}
//使用Grid++报表,构建数据显示 var ids = '';for (var i = 0; i < rows.length; i++) {
ids
+= rows[i].ID + ',';
}
ids
= ids.substring(0, ids.length - 1);//loadReport(ids); ViewPDF(ids);
}

这里利用了PDFJS的插件来在线展示生成的PDF,需要了解具体可以参考我之前的随笔《
实现在线预览PDF的几种解决方案

然后在HTML页面里面加入对应的处理按钮即可。

<buttontype="button"onclick="AssetPrint()"class="btn btn-circle btn-sm red">
    <iclass="fa fa-plus"></i>资产编码打印</button>

如下页面的界面效果所示

单击【资产编码打印】,首先在后台动态生成一个PDF文件,成功后前端弹出一个新的预览PDF界面,预览我们的二维码报表,效果比HTML5界面效果标准,和我们原来的设计报表初衷一直,两列排列。

以上就是我在实际项目中,在Bootstrap开发框架中使用Grid++报表的几个处理过程,希望对你使用有帮助,报表确实具有很好的应用场景,使用起来还是很方便的。