2023年2月

在很多项目里面,对条形码和二维码的生成和打印也是一种很常见的操作,在Web项目里面,我们可以利用JS生成条形码和二维码的组件有很多。本文引入两个比较广泛使用的JS组件,用来处理条形码和二维码的生成处理,并介绍如何利用CLODOP组件实现内容的打印输出。
生成条形码使用组件JsBarcode,生成二维码使用组件qrcodejs。

1、条形码的生成

条码的作用一般在一些商品标签上,方便使用条码枪快速、准确录入信息。
如下所示是一种条形码


这里条形码生成使用了JsBarcode组件进行处理,它支持很多格式的条码格式,如下所示。


它的简单代码例子如下所示。

//HTML代码<svgid="barcode"></svg>
<!--or-->
<canvasid="canvas"></canvas>
<!--or-->
<imgid="barcode"/>//JS代码
JsBarcode("#barcode", "Hi!");
// or with jQuery
$("#barcode").JsBarcode("Hi!");

生成的图片格式如下所示。


JsBarcode组件支持多种选项参数的设置,如下所示的代码

JsBarcode("#barcode", "1234", {
format:
"pharmacode",
lineColor:
"#0aa",
width:
4,
height:
40,
displayValue:
false});

JsBarcode(
"#barcode")
.options({font:
"OCR-B"}) //Will affect all barcodes .EAN13("1234567890128", {fontSize: 18, textMargin: 0})
.blank(
20) //Create space between the barcodes .EAN5("12345", {height: 85, textPosition: "top", fontSize: 16, marginTop: 15})
.render();

通过该组件的案例代码介绍,我们可以了解到生成常规二维码的使用方式。
例如我们在界面上添加了以下HTML代码。

<divclass="row"id="barDiv1">
    <divclass="col-md-4 col-sm-4 col-xs-4">条形码(770 145 896 701):<br/>
        <imgalt=""id="barcode1" />
    </div>
    <divclass="col-md-4 col-sm-4 col-xs-4">条形码(770 145 896 702):<br/>
        <imgalt=""id="barcode2" />
    </div>
    <divclass="col-md-4 col-sm-4 col-xs-4">条形码(770 145 896 703):<br/>
        <imgalt=""id="barcode3" />
    </div>
</div>

然后通过JS代码实现二维码的生成。

//使用JsBarcode生成条形码
for (var i = 1; i < 10; i++) {var barcodeValue = "77014589670" + i;//前缀 + 数值
    JsBarcode("#barcode" +i, barcodeValue, {
format:
"CODE128",
displayValue:
true,
fontSize:
24,
lineColor:
"#0cc"});
}

最后我们可以看到具体的生成效果如下所示。


从这里我们可以看到,使用JS实现条形码的生成是非常方便简洁的,同事速度也是很不错的。

2、二维码的生成

二维码实现可以通过使用组件qrcodejs进行生成,二维码也可以使用组件jquery-qrcode进行生成,也相对比较简洁,不过打印二维码文档的时候,jquery-qrcode没有显示二维码图片,而组件qrcodejs则工作正常,因此推荐使用组件qrcodejs。
这个qrcodejs组件使用方式也很简单,基础使用代码如下所示。

<divid="qrcode"></div>
<scripttype="text/javascript">
newQRCode(document.getElementById("qrcode"),"http://jindo.dev.naver.com/collie");</script>

复杂一点的使用JS代码如下所示。

<divid="qrcode"></div>

<scripttype="text/javascript">
varqrcode= newQRCode(document.getElementById("qrcode"), {
text:
"http://jindo.dev.naver.com/collie",
width:
128,
height:
128,
colorDark :
"#000000",
colorLight :
"#ffffff",
correctLevel : QRCode.CorrectLevel.H
});
</script>

通过了解该组件的使用,我们可以在项目中增加一个实际的代码进行测试,如下所示。

<tableborder='0'width='100%'>
    <tr>
        <td>设备二维码(9001):<br/>
            <divid="imgDevice9001"></div>
        </td>
        <td>设备二维码(9002):<br/>
            <divid="imgDevice9002"></div>
        </td>
        <td>设备二维码(9003):<br/>
            <divid="imgDevice9003"></div>
        </td>
    </tr>
    <trstyle="height:20px"><tdcolspan="3"></td></tr>......</table>

具体我们可以利用JS动态生成相关的二维码。

//设备图片二维码
for (var i = 9001; i < 9010; i++) {var url = "http://www.iqidi.com/H5/device?devicecode=" +i;//使用jquery.qrcode的做法
    //$("#imgDevice" + i).qrcode({ width: 100, height: 100, text: url });
    //$("#imgDevice" + i).css("height", "100px");

    //使用qrcodejs的做法
    var qrcode = new QRCode(document.getElementById("imgDevice" +i), {
text: url,
width:
128,
height:
128,
colorDark:
"#000000",
colorLight:
"#ffffff",
correctLevel: QRCode.CorrectLevel.H
});
}

最后界面生成的二维码如下所示。

3、条形码和二维码的打印处理

介绍我二维码和条形码的生成,关于它们的打印,可以利用我介绍过的CLODOP进行打印处理,需要了解可以参考下随笔《
基于MVC+EasyUI的Web开发框架经验总结(16)--使用云打印控件C-Lodop打印页面或套打报关运单信息

我们在这里介绍的打印,也是基于这个控件的打印处理的。
当然,如果打印,也是可以利用PrintThis这个组件进行处理的(详细可以参考随笔《
基于Metronic的Bootstrap开发框架经验总结(9)--实现Web页面内容的打印预览和保存操作
》),不过总体效果没有上面的CLODOP的打印效果佳。
打印JS代码如下所示,可以采用下面两个函数的其中之一进行处理。

//使用表格格式化输出
functionPrintBarcodeWithTable() {
LODOP
=getLodop();
LODOP.PRINT_INIT(
"条码_格式化输出");var strHTML = "<table border='0' width='100%' >";
strHTML
= strHTML + "<tr><td>";
strHTML
= strHTML + $("#barDiv1").html();
strHTML
= strHTML + "</td></tr>";
strHTML
= strHTML + "<tr><td>";
strHTML
= strHTML + $("#barDiv2").html();
strHTML
= strHTML + "</td></tr>";
strHTML
= strHTML + "<tr><td>";
strHTML
= strHTML + $("#barDiv3").html();
strHTML
= strHTML + "</td></tr>";
LODOP.ADD_PRINT_TABLE(
88, 200, 700, 900, strHTML);

LODOP.SET_SHOW_MODE(
"PREVIEW_IN_BROWSE", 1);
LODOP.PREVIEW();
}
//使用分页输出 functionPrintBarcodeWithPaging() {
LODOP
=getLodop();
LODOP.PRINT_INIT(
"条码_分页输出");
LODOP.ADD_PRINT_HTM(
100, 100, 600, 800, $("#barDiv1").html());
LODOP.NewPage();
LODOP.ADD_PRINT_HTM(
100, 100, 600, 800, $("#barDiv2").html());
LODOP.NewPage();
LODOP.ADD_PRINT_HTM(
100, 100, 600, 800, $("#barDiv3").html());
LODOP.NewPage();

LODOP.SET_SHOW_MODE(
"PREVIEW_IN_BROWSE", 1);
LODOP.PREVIEW();
}

条码的打印效果如下所示。

而打印二维码的JS代码如下所示。

//打印二维码
functionPrintQrcode2() {
CreatePrintData($(
"#qrcode").html());
LODOP.SET_SHOW_MODE(
"PREVIEW_IN_BROWSE", 1);
LODOP.PREVIEW();
}
//打印数据构建 functionCreatePrintData(html) {
LODOP
=getLodop();

LODOP.PRINT_INIT(
"");//var strBodyStyle = "<link type='text/css' rel='stylesheet' href='/Content/Themes/Default/style.css' />"; var strBodyStyle = "<style><!--table { border:1;background-color: #CBCBCC } td {border: 1; }";
strBodyStyle
+= " th { background-color:#F1F1F3;padding-left:5px;border:1}--></style>";var strFormHtml = strBodyStyle + "<body>" + html + "</body>";
LODOP.ADD_PRINT_HTM(
20, 40, 710, 900, strFormHtml);
LODOP.PREVIEW();
}

得到的二维码打印效果如下所示。


最终完成了这个关于条形码、二维码的动态生成,以及图形打印的相关操作了。整个模块的界面如下所示。


关于条形码、二维码的处理,我们这里引入的条形码组件JsBarcode和二维码组件qrcodejs是非常不错的开源JS组件,满足了我们大多数的要求,而且使用方便、简洁,希望这些内容能够给你的项目提供灵感及用处。

在之前开发的很多Web API项目中,为了方便以及快速开发,往往把整个Web API的控制器放在基目录的Controllers目录中,但随着业务越来越复杂,这样Controllers目录中的文件就增加很快,难以管理,而且如果有不同业务模块有重复的控制器名的话,还需要尽量避免。引入Area的作用就是把控制器按照不同的业务模块进行区分,方便管理,而且控制器名称可以重名。

1、Web API项目引入Area进行分类

Area在项目中可以称之为区域,每个Area代表应用程序的不同功能模块,Area 使每个功能模块都有各自的文件夹,文件夹中有自己的Controller、View和Model,但对于管理也增加了一定的难度。如果是Web API项目,我们可以把不必要的目录移除即可,简化对目录的管理。
引入Area可以是我们不同的业务模块可以重名,而且各个业务模块管理起来也更加方便,在原先的Web API项目里面,它们的目录是这样的。


虽然我们把它们的目录归类,但是它们还是存放在一个命名空间下的。

namespace MyWebApi.Controllers


这样使用虽然也没有什么问题,但是还是存在一些弊端,因此引入Area的方式对不同业务模块的控制器进行管理,以达到我们分类管理的目的。
引入Area前,我们的API路径如下所示

http://localhost:9001/api/User

引入Area后,我们把常规的权限管理、字典管理等基础模块放到Framework的Area里面,那么这个时候API路径和具体的Area相关,地址则变成了如下:

http://localhost:9001/api/Framework/User

我们再来看看具体的项目目录,Web API项目中使用Area后,Controller的目录如下所示。


除了在各个不同Area下有不同的控制器,而且也增加了一个
**
AreaRegistration.cs的文件,如对应Framework的Area,有一个FrameworkAreaRegistration.cs文件
这样对应下面的控制器,它的命名空间如下所示。

namespace WebAPI.Areas.Framework.Controllers

2、Web API项目对Area控制器的路径映射

上面小节介绍了使用Area来对Web API控制器的分类管理,并介绍了引入Area后对控制器位置、命名空间、Web API的URL等方面的不同。这样如果我们要解析对应地址的Web API,那么也需要做一定的处理,否则是无法找到对应的控制器,从而出现错误信息。
首先我们需要修改Web API里面WebApiConfig的配置信息,如下所示。


上面指定了默认的Web API映射,并指定结果只做JSON格式的输出(移除XML输出)。
为了对不同的Area实现API的地址处理,我们先设计一个基类,然后让不同的Area注册类继承它,方便统一处理。


其中基类Area注册类的CustomAreaRegistration类代码如下所示。


有了上面的基类映射 RegisterArea函数,我们只需要在子类设置对应的AreaName基类实现不同Area子类的正确映射API路径处理了。

/// <summary>
///框架基础Area的注册类/// </summary>
public classFrameworkAreaRegistration : CustomAreaRegistration 
{
public override stringAreaName
{
get{return "Framework";
}
}
}

当然为了实现对Area的Web API控制器的URL正确解析,获取属于Action、Controller、以及对应命名空间的对象,那么还需要在global.asa.cs里面添加一行代码,如下所示。

 //对Web API的Area进行支持
GlobalConfiguration.Configuration.Services.Replace(typeof(IHttpControllerSelector),  new AreaHttpControllerSelector(GlobalConfiguration.Configuration));

其中AreaHttpControllerSelector是我们自定义的HTTP控制器地址解析器,需要根据我们的地址提取出具体的控制器、Area名称、程序集类型等,方便构建对应的解析器。

privateHttpControllerDescriptor GetApiController(HttpRequestMessage request)
{
var controllerName = base.GetControllerName(request);var areaName =GetAreaName(request);if (string.IsNullOrEmpty(areaName))
{
return null;
}
var type =GetControllerTypeByArea(areaName, controllerName);if (type == null)
{
return null;
}
return newHttpControllerDescriptor(_configuration, controllerName, type);
}

有了这些基础的管理,我们就可以定义好我们所需要Area,然后构建具体业务范畴下的控制器接口即可。

3、Web API在客户端的接口调用

所有的Web API地址,都是与具体的Area有关系,例如在Framework业务下的字典模块,它们Web API配置的地址如下所示。

<!--字典Web API模块配置-->

<add key="DictType" value="http://localhost:27206/api/Framework/DictType"/>

<add key="DictData" value="http://localhost:27206/api/Framework/DictData"/>

<add key="CorpDictData" value="http://localhost:27206/api/Framework/CorpDictData"/>

<add key="City" value="http://localhost:27206/api/Framework/City"/>

<add key="District" value="http://localhost:27206/api/Framework/District"/>

<add key="Province" value="http://localhost:27206/api/Framework/Province"/>

<add key="UserParameter" value="http://localhost:27206/api/Framework/UserParameter"/>

我们在客户端,只需要对Web API进行封装即可,这个部分可以使用Database2Sharp代码生成工具进行统一的生成,所有继承关系统一处理好,我们所做的就是进行新增接口的处理即可。
例如对于字典模块DictData的处理,它对于Web API的封装类如下所示。

/// <summary>
///DictData, 基于API服务的Facade接口实现类/// </summary>
public class DictDataCaller : BaseApiService<DictDataInfo>, IDictDataService

这个基类,默认封装了对常规数据表业务Web API接口方式的增删改查以及各种复杂的接口处理。
如果对于一般的Web API(非数据表业务),那么只需要继承的基类做调整即可。

/// <summary>
///基于API服务的Facade接口实现类/// </summary>
public class TestCaller : NormalApiService, ITestService

这个NormalApiService基类,默认只是封装了对token和签名的读取处理,没有特殊的业务接口,具体特定的接口我们来实现处理。

对于WebAPI客户端的调用,我们主要就是需要构建对应的URL,然后通过GET传递或者POST传递一些参数,并读取HTML结果,把它解析为对应的类型数据即可,如下代码所示。

/// <summary>
///根据字典类型名称获取对应的字典记录/// </summary>
/// <param name="dictTypeName">字典类型名称</param>
/// <returns></returns>
public List<DictDataInfo> FindByDictType(stringdictTypeName)
{
var action =System.Reflection.MethodBase.GetCurrentMethod().Name;string url = GetTokenUrl(action) + string.Format("&dictTypeName={0}", dictTypeName);

List
<DictDataInfo> result = JsonHelper<List<DictDataInfo>>.ConvertJson(url);returnresult;
}

通过GetTokenUrl(action) 函数获取对应的URL地址,由于传入一个参数,接口这里没有发生数据修改,是GET方式提交参数数据,因此把参数附加在URL即可。
也就是下面代码实现了完整Web API地址的构建。

string url = GetTokenUrl(action) + string.Format("&dictTypeName={0}", dictTypeName);

构建好这些URL地址后,我们通过获取对应Web API的结果并进行序列号到具体对象即可。如下代码所示。

List<DictDataInfo> result = JsonHelper<List<DictDataInfo>>.ConvertJson(url);

关于Web API接口的设计文章,可以参考我的随笔。

通过以上的封装处理,那么对于业务表的Web API接口调用,具体使用客户端的代码如下所示。

var dictType = CallerFactory<IDictTypeService>.Instance.GetTree();
Console.WriteLine(dictType.ToJson());
var dictData = CallerFactory<IDictDataService>.Instance.GetAllDict();
Console.WriteLine(dictData.ToJson());

如果对于非数据表业务的Web API接口调用,具体使用客户端的代码如下所示。

var testAction = CallerFactory<ITestService>.Instance.TestAction();
Console.WriteLine(testAction.ToJson());
var test = CallerFactory<ITestService>.Instance.Test("123");
Console.WriteLine(test.ToJson());

这样,不管是在Web项目里面,还是在Winform项目里面,或者在跨平台的IOS项目里面(或者安卓项目),都可以以相同的方式消费Web API,这样我们所有的数据入口在一个地方,可以集中业务接口的统一开发,并且可以有效管理我们的数据提供的性能问题,如统一缓存处理,统一权限处理...
感谢大家对本文章的细心阅读,希望对您的开发有所启发或帮助。

在我之前介绍的混合式开发框架中,其界面是基于Winform的实现方式,后台使用Web API、WCF服务以及直接连接数据库的几种方式混合式接入,在Web项目中我们也可以采用这种方式实现混合式的接入方式,虽然Web API或者WCF方式的调用,相对直接连接数据库方式,响应效率上略差一些,不过扩展性强,也可以调动更多的设备接入,包括移动应用接入,网站接入,Winfrom客户端接入,这样可以使得服务逻辑相对独立,负责提供接口即可。这种方式中最有代表性的就是当前Web API的广泛应用,促进了各个接入端的快速开发和独立维护,极大提高了并行开发的速度和效率。在企业中,我们可以合理规范好各种业务服务的Web API接口,各个应用接入端可以独立开发,也可以交给外包团队进行开发即可。

1、Winform混合式接入方式回顾

从一开始,我们的Web API 的设计目的就是为了给各种不同的应用进行接入的,例如需要接入Winform客户端、APP程序、网站程序、以及微信应用等等,由于Web API层作为一个公共的接口层,我们就很好保证了各个界面应用层的数据一致性。

上图介绍了各种应用在Web API的接口层之上,一般情况下,我们这层的接口都是提供标准的各种接口,以及对身份的认证处理等等,在Web API层更多考虑的业务范畴的相关接口,而在各个界面层,考虑的是如何对Web API进行进一步的封装,以方便调用,虽然Winform和Web调用Web API的机制有所不同,不过我们还是可以对Web API的客户端封装层进行重用的。

在Winfrom界面调用混合式接入的接口方式,它的示意图如下所示,主要的思路是通过一个统一的门面层Facade接口层进行服务提供,以及客户端调用的封装处理接口。

随着Web API层的广泛使用,这种方式带来了非常大的灵活性,通过在框架层面对各个层的基类进行封装,可以大大简化所需的编码,以及提供统一、丰富的基础接口供调用。

由于Winform调用Web API的时候,客户端对Web API层进行了一个简单的包装,这种方式可以简化对Web API接口的使用,只需要通过调用封装类,并传入相关的参数就可以获得序列化后的对象(包括基础对象和自定义类对象)。

这种封装的方式,由于对基类的统一实现,以及提供对URL地址、参数的组装等处理,非常利于Winform界面后台代码进行调用 ,加快Winfom界面功能的开发。例如我们从进一步细化的架构图上,可以看到整体各个层的一些基类(绿色部分)。

在基于Web API层的构建上,我们提供了Web API服务层的提供了BaseApiController和BusinessController<B, T> 的Web API控制器基类对常规的业务处理进行封装;在Web API服务调用层上,我们提供了BaseApiService<T>的基类进行封装常规接口;

同时提供IBaseService<T>的Facade门面层的统一接口,以及CallerFactory<T>的调用方式供Winform后台代码进行接口调用。

这种在Web API的基础上进行接口的封装,可以极大简化接口的调用,同时也可以提供给Web端的后台控制器使用,非常便于使用,下面就介绍在Web项目中进行混合式接入的实现过程。

2、Web混合式接入介绍

参照Winform混合式接入的方式,我们也可以利用这种方式应用于Web框架上,具体的分层关系如下所示。

上图整合了两种非常常用的接入方式:Web API服务接入、直接连接数据库的接入,一种具有非常强大的特性,一种具有快速的访问效率,各有其应用场景,我们在不同的业务环境进行配置,使其适应我们实际的应用即可,一般情况下,我们建议采用Web API方式进行构建整个业务系统的生态链。

Web API的接口调用,可以通过两种方式进行,一种是采用纯JS框架,类似AngularJS的方式,通过其控制器进行相关接口的调用;还有一种方式,采用Asp.NET的MVC方式,前端界面通过JS调用后端的控制器实现数据处理,具体逻辑有后端逻辑控制器进行Web API的处理,我们这里采用后者,以实现较为弹性的处理。

相对Winform来说,Web上的混合方式接入相对复杂一些,虽然Winform的界面类似Web的MVC中的视图HTML代码,Winform后台逻辑代码类似视图的控制器对象,但是确实麻烦一些,相当于我们还需要在Web界面的后台控制器Controller上在封装下相关的处理接口。

在整个基于混合式接入方式的Web 项目中,对于Web API接口的使用,整个项目的结构如下所示。

有了这些图示的说明,我们应该对整体有一个大概的了解,对于进一步的细节问题,我们可能依然不是太清楚,需要以具体的项目代码工程进行介绍。

1)对于数据库层

我们可以考虑的是多种数据库的接入支持,如SQLServer、Oracle、SQLite、Access,或者PostgreSQL的支持,这些都是基于关系型数据库的支持,具有很好的可替代性和标准一致性。

它们可以通过遵循统一的SQL或者部分自定义的SQL语句进行,或者通过存储过程实现,均可以实现相应的功能。

对于数据库不同的支持方案,我这里采用了Enterprise Library的数据库访问组件进行一致性的支持,这样可以降低各个不同数据库模型的处理,统一使用这种组件访问方式,实现不同数据库的访问。

2)对于业务逻辑层

业务逻辑层,是有几个不同的层进行综合的使用。如项目中的核心层如下所示,包括了业务逻辑层BLL、数据访问层DAL(不同的实现层)、数据访问接口层IDAL、以及传递数据的Entity实体层。

这些模块,在各个层上都有标准的基类用来实现对接口或者功能的封装处理。

如BLL层的继承关系如下

    /// <summary>
    ///基于BootStrap的图标/// </summary>
    public class BootstrapIcon : BaseBLL<BootstrapIconInfo>

如IDAL层的继承关系如下

    /// <summary>
    ///基于BootStrap的图标/// </summary>
    public interface IBootstrapIcon : IBaseDAL<BootstrapIconInfo>

基于Oracle的数据访问层在DALOracle里面,我们看到起继承关系如下。

    /// <summary>
    ///基于BootStrap的图标/// </summary>
    public class BootstrapIcon : BaseDALOracle<BootstrapIconInfo>, IBootstrapIcon

实体层继承关系如下所示。

    /// <summary>
    ///基于BootStrap的图标/// </summary>
    public class BootstrapIconInfo : BaseEntity

这些模块,由于有了基类的封装处理,多数逻辑不用再重写代码,关于它们具体的内容,可以参考之前的开发框架介绍文章了解,这里不再赘述,主要用来介绍其他模块层的继承关系。

3)对于Web API服务层

Web API如果业务模块比较多,可以参考我上篇随笔《
Web API项目中使用Area对业务进行分类管理
》使用Area区域对业务进行分类管理,一般情况下,我们为每个Web API的接口类提供了基类的管理,和我们其他模块的做法一样。

    /// <summary>
    ///所有接口基类/// </summary>
[ExceptionHandling]public class BaseApiController : ApiController

以及

    /// <summary>
    ///本控制器基类专门为访问数据业务对象而设的基类/// </summary>
    /// <typeparam name="B">业务对象类型</typeparam>
    /// <typeparam name="T">实体类类型</typeparam>
    public class BusinessController<B, T>: BaseApiControllerwhere B : class
        where T : WHC.Framework.ControlUtil.BaseEntity, new()

这样,基本的增删改查等常规接口,我们就可以在基类里面直接调用业务逻辑类实现数据的处理,具体的业务子类这不需要重写这些接口实现了。

        /// <summary>
        ///查询数据库,检查是否存在指定ID的对象/// </summary>
        /// <param name="id">对象的ID值</param>
        /// <returns>存在则返回指定的对象,否则返回Null</returns>
[HttpGet]public virtual T FindByID(string id, stringtoken)
{
//如果用户token检查不通过,则抛出MyApiException异常。//检查用户是否有权限,否则抛出MyDenyAccessException异常 base.CheckAuthorized(AuthorizeKey.ViewKey, token);

T info
=baseBLL.FindByID(id);returninfo;
}

对于HttpGet和HttpPost的约定,我们对于常规的获取数据,使用前者,如果对数据发生修改,或者需要复杂类型的参数,使用POST方式处理。

子类的继承关系如下所示

    /// <summary>
    ///权限系统中用户信息管理控制器/// </summary>
    public class UserController : BusinessController<User, UserInfo>

这样这个UserController就具有了基类的一切功能,只需要实现一些特定的接口处理即可。

例如我们可以定义一个新的Web API接口,如下所示。

        /// <summary>
        ///通过用户名称获取用户对象/// </summary>
        /// <param name="userName">用户名称</param>
        /// <param name="token">访问令牌</param>
        /// <returns></returns>
[HttpGet]public UserInfo GetUserByName(string userName, stringtoken)
{
//令牌检查,不通过则抛出异常 CheckResult checkResult =CheckToken(token);return BLLFactory<User>.Instance.GetUserByName(userName);
}

这样对于Web API架构来说,控制器的整个继承关系大概如下所示。

如果使用Area区域来对业务模块进行分类,那么整个Web API项目的结构如下所示,各个业务区域分开,有利于对业务模块代码的维护,其中BaseApiController和BusinessController则是对常规Web API接口的封装处理。

4)对于Web API封装层

为了实现Winform混合式框架和Web混合式框架的共同使用Web API服务的封装层,那么我们需要独立一个Web API封装层,也就是***Caller层,包含了直接访问数据库方式、Web API服务接口访问方式,或者加上WCF服务访问方式等的封装层。

这个层的目的是动态读取Web API 接口的URL地址,以及封装对Web API接口访问的繁琐细节,是调用者能够简单、快速的访问Web API接口。

整个Web API封装层的架构,就是基于Facade接口层进行不同的适配,如直接访问数据库方式、Web API服务访问方式的适配处理,以便在客户端调用的时候,自动从不同的接口实现实例化对象,从不同方式来获取所需要的接口数据。

对于用户User对象来说,我们来举一个例子来说明Caller层之间的继承关系。

在Facade层的接口定义如下所示。

    public interface IUserService : IBaseService<UserInfo>

在WebAPI的Caller层实现类代码如下所示。

    /// <summary>
    ///基于WebAPI方式的Facade接口实现类/// </summary>
    public class UserCaller : BaseApiService<UserInfo>, IUserService

对于直接连接方式,实现类的代码如下所示。

    /// <summary>
    ///基于传统Winform方式,直接访问本地数据库的Facade接口实现类/// </summary>
    public class UserCaller : BaseLocalService<UserInfo>, IUserService

这样我们整理下它们关系如下图所示。

对于不同的业务模块,我们基于对应不同的Facade层接口实现不同的Caller层,这样即使有很多项目模块,我们单独维护起来也方便很多,在Winform客户端或者Web端调用Caller层的时候,需要引入对应的Caller层项目,以及业务核心层Core。

例如我们需要在使用的时候,同时引入Core层和Caller层,如下是项目中的部分引用关系。

5)对于Web 界面层

这个Web界面层,主要就是消费Facade层接口实现,用来获取数据展示在界面上的,我们界面上通过HTML + JS Ajax的方式,实现从MVC控制器接口获取数据,那么我们为了方便,依旧在控制器层进行抽象,以便对常规的方法抽到基类里面,这样子类代码就不用重复了。

这样的改变,对于我们已有的MVC项目来说,视图处理代码不需要任何改变,只需要控制器对数据访问的处理调整即可,从而实现MVC普通方式获取数据的界面层,顺利转换到基于Web API +直接访问数据库两者合一的混合式方式上。

原先直接访问数据库的MVC视图控制器的设计,基本上类似于Web  API 中控制器的设计过程,如下所示。

而对于MVC的Web界面层,以混合式方式来访问数据,我们需要引入一个新的控制器来实现适配处理。

这样构建出来的继承关系图,和上面Web的MVC控制器类似。

不同的是,里面调用的任何访问数据的方法,从原来BLLFactory<T>到CallerFactorry<T>的转换了,这样就实现了从简单的直接访问数据库方式,切换到混合式访问数据的方式,在Web框架里面,可以配置为直接访问数据库,也可以配置为通过Web API方式访问数据,非常方便。

例如继承关系类的代码如下所示。

    /// <summary>
    ///基于混合访问方式的用户信息控制器类/// </summary>
    public class UserController : ApiBusinessController<IUserService, UserInfo>

其中对于Web 界面端的控制器,使用混合式访问方式的后台控制器代码如下所示。

        /// <summary>
        ///根据角色获取对应的用户/// </summary>
        /// <param name="roleid">角色ID</param>
        /// <returns></returns>
        public ActionResult GetUsersByRole(stringroleid)
{
ActionResult result
= Content("");if (!string.IsNullOrEmpty(roleid) &&ValidateUtil.IsValidInt(roleid))
{
List
<UserInfo> roleList = CallerFactory<IUserService>.Instance.GetUsersByRole(Convert.ToInt32(roleid));
result
=ToJsonContent(roleList);
}
returnresult;
}

也就是从传统的BLLFactory<User>转换为了CallerFactory<IUserService>,整体性的接口变化很小,很容易过渡到混合式方式的访问。

在Web界面端的视图里面,我们基本上就是根据HTML + Ajax的Javascript方式实现数据的交互处理的,包括显示数据,提交修改等等操作。

同样我们可以通过JS的函数进行抽象,把基本的处理函数,放到一个类库里面,方便界面层使用,然后引入JS文件即可。

@*脚本引用放在此处可以实现自定义函数自动提示*@<scriptsrc="~/Scripts/CommonUtil.js"></script>

如下面所示,是调用JS自定义函数实现列表数据的绑定操作。

            $("#Dept_ID").on("change", function(e) {var deptid = $("#Dept_ID").val();
BindSelect(
"PID", "/User/GetUserDictJson?deptId="+deptid);
});

或者删除的JS代码如下所示

                    var postData ={ ids: ids };
$.post(
"/User/ConfirmDeleteByIds", postData, function(json) {var data =$.parseJSON(json);if(data.Success) {
showTips(
"删除选定的记录成功");
Refresh();
//刷新页面数据 }else{
showTips(data.ErrorMessage);
}
});

以及对一些JS列表树,以及下拉列表,都可以采用JS函数实现快速的处理,如下所示。

                var treeUrl = '/Function/GetFunctionJsTreeJsonByUser?userId=' +info.ID;
bindJsTree(
"jstree_function", treeUrl);

$(
'#lbxRoles').empty();
$.getJSON(
"/Role/GetRolesByUser?r=" + Math.random() + "&userid=" + info.ID, function(json) {
$.each(json,
function(i, item) {
$(
'#lbxRoles').append('<option value="' + item.ID + '">' + item.Name + '</option>');
});
});

以上就是我从整个基于混合式访问的Web项目进行讲解介绍,贯穿了整个数据传输的路线和调用路线,当然其中还有很多细节方面有待细讲,以及需要一些比较巧妙的整合封装处理,整个目的就是希望借助混合式的访问思路,实现多种数据接入方式的适配整合,以及最大程度简化子类代码的编写,并且通过利用代码生成工具对整体框架的各个层代码的生成,我们关心的重点转移到如何实现不同业务的接口上来,从而使得我们能够快速开发复杂的应用,而且又能合理维护好各个项目的代码。

一句话总结整个开发:简单、统一、高效。

在我们开发很多项目中,数据访问都是必不可少的,有的需要访问Oracle、SQLServer、Mysql这些常规的数据库,也有可能访问SQLite、Access,或者一些我们可能不常用的PostgreSQL、IBM DB2、或者国产达梦数据库等等,这些数据库的共同特点是关系型数据库,基本上开发的模型都差不多,不过如果我们基于ADO.NET的基础上进行开发的话,那么各种数据库都有自己不同的数据库操作对象,微软企业库Enterprise Library是基于这些不同数据库的操作做的抽象模型,适合多数据库的支持项目。本文介绍基于微软企业库Enterprise Library 4.1的基础进行的多种数据库的处理。

1、企业库Enterprise Library版本的选择

在选择Enterprise Library版本的时候,我一直都是相对谨慎,因为我们开发的项目涉及很多不同的系统,有的需要XP的支持、有的需要Win7的支持或者Win10等等,需要考虑不同系统之家的兼容问题,由于微软企业库中的数据库访问模块相对比较稳定,因此也基本沿用使用稳定的版本,虽然目前Enterprise Library版本为6.0,但是之前一直在项目中使用的是3.1,这个版本可以在.NET 2.0的项目上运行,而且扩展类库也比较不错,因此一直保留着。

随着框架版本的升级,在XP上最高可以运行.NET 4.0的版本,因此可以考虑使用Enterprise Library 4.1或者5.0的版本(Enterprise Library 6.0版本需要.NET 4.5的支持,无法再XP上运行),相对来说,Enterprise Library4.1的扩展类库支持非常不错(
http://entlibcontrib.codeplex.com/releases/view/38988
),支持了SQLServer、DB2、MySql、ODP.NET(Oracle)、PostgreSQL、SQLite、SqlEx等数据库,而Enterprise Library 5.0版本扩展类库的支持还没有完整提供,需要自己处理。

因此综合上面的原因,我们为了照顾XP、Win7/Win8/Win10等不同系统的兼容性,可以从目前的Enterprise Library 3.1升级到Enterprise Library 4.1,这样可以利用较好的扩展类库的支持(支持支持了SQLServer、DB2、MySql、ODP.NET(Oracle)、PostgreSQL、SQLite、SqlEx等数据库),也相对提高下该数据访问模块的版本。

在我以前绘制的多数据库支持里面调整一下,把我们采用微软企业库后,支持的数据库作为数据层,示意图如下所示。

这样基本上常规的关系型数据库我们都支持了,我们需要开发任何数据库应用,都是统一数据模型,开发起来方便很多了,同时也方便在不同数据库管理系统中进行配置切换。

2、采用微软企业库进行架构设计

采用了微软企业库Enterprise Library作为我们底层的数据库访问模块后,对于多种数据库的访问操作,就会统一采用这个企业库的数据库访问对象,操作起来非常一致,为了对不同数据库的常规增删改查等一些操作进行进一步的封装,已达到简化代码的目的,因此我们可以为每个不同的数据库定义一个数据访问操作基类,以便实现一些不同数据库差异性的处理,但是它们还是有一个共同的数据访问基类。

采用不同的数据库,我们需要为不同数据库的访问层进行生成处理,如为SQLServer数据的表生成相关的数据访问层DALSQL,里面放置各个表对象的内容,不过由于采用了相关的继承类处理和基于数据库的代码生成,需要调整的代码很少。

针对数据访问层,我们需要设计好对应的继承关系,以便使得我们的基类能够封装大多数的操作,并给子类相对的弹性处理空间,如对于客户Customer的对象,数据接口层和数据访问实现层的关系如下所示。

这样整合多种数据库支持的底层后,整个数据访问的架构设计如下所示。

关于这个架构,我在前面很多文章都有阐述,如果我们还需要扩展一些特殊的数据库支持,可以参考随笔《
基于Enterprise Library的Winform开发框架实现支持国产达梦数据库的扩展操作
》进行一些扩展定制的操作。

3、基于Enterprise Library的多种数据库支持处理

前面我们提到,使用微软企业库Enterprise Library的好处就是可以统一编程模型,实现对多种数据库的兼容处理,而微软企业库Enterprise Library最大的特点是基于配置项实现多种数据库的处理,通过对使用不同的配置项,就可以迅速切换到对应的数据库上来,代码不需要修改。

对于一般的企业库配置处理,我们增加配置项如下所示。

  <configSections>
    <sectionname="dataConfiguration"type="Microsoft.Practices.EnterpriseLibrary.Data.Configuration.DatabaseSettings, Microsoft.Practices.EnterpriseLibrary.Data"/>
  </configSections>

然后为不同的数据库添加不同的连接字符串

对于默认支持的SQLServer数据库,它的连接字符串如下所示。

<?xml version="1.0"?>
<configuration>
  <configSections>
    <sectionname="dataConfiguration"type="Microsoft.Practices.EnterpriseLibrary.Data.Configuration.DatabaseSettings, Microsoft.Practices.EnterpriseLibrary.Data"/>
  </configSections>
  <connectionStrings>
    <!--SQLServer数据库的连接字符串-->
    <addname="sqlserver"providerName="System.Data.SqlClient"connectionString="Persist Security Info=False;Data Source=(local);Initial Catalog=WinFramework;Integrated Security=SSPI"/>
  </connectionStrings>
  <dataConfigurationdefaultDatabase="sqlserver">
  </dataConfiguration>
</configuration>

不过对于一些扩展支持的数据库,我们还需要添加一些映射处理,如对于MySQL的支持,我们需要添加连接字符串:

    <!--MySQL数据库的连接字符串-->
    <addname="mysql"providerName="MySql.Data.MySqlClient"connectionString="Server=localhost;Database=WinFramework;Uid=root;Pwd=123456;"/>

还需要添加ProviderMappings的支持,如下所示的XML。

  <dataConfigurationdefaultDatabase="mysql">
    <providerMappings>
      <adddatabaseType="EntLibContrib.Data.MySql.MySqlDatabase, EntLibContrib.Data.MySql"name="MySql.Data.MySqlClient" />
    </providerMappings>
  </dataConfiguration>

下面我列出所有不同数据库的连接字符串以及映射关系的一个完整版本,供参考。

前面我们提到了,基于配置实现不同数据库的统一处理,我们为了测试不同数据库的连接,我们可以使用下面的简单案例代码来获取数据进行展示。

其实现的代码如下所示。

我们看到,上面的代码没有针对具体的数据库,因此也是非常通用的处理,我们可以直接获取数据并展示出来,我上面案例在SQLServer、Oracle、PostgreSQL、MySQL、SQLite、Access、IBM DB2数据库均测试通过。

具体开发项目的时候,不同数据库有一些不同的处理,如分页操作、获取指定记录的处理等等,这些我们就需要发挥上面提到的数据库基类的功能了,通过基类功能的封装,我们可以除了可以使用所有数据库的共性外,还可以使用它的一些特定处理操作,这样我们就可以充分利用各种不同数据库的特点,但是又统一到一个开发模型上来,降低了各种不同数据库之间开发的成本,同时也减少不同数据库之间的迁移难度,提高代码的可阅读性和可扩展性。

上面关于数据库访问模块的框架构建,已经在我众多的Winform项目、Web开发项目,以及一些后台服务项目上运行良好,并使用了多年,为我们开发各种不同数据库,或者升级到不同数据库版本的处理工作上立下了汗马功劳。

这几天一直在研究TX Text Control的使用,由于这方面的资料相对比较少,主要靠下载版本的案例代码进行研究,以及官方的一些博客案例进行学习,使用总结了一些心得,特将其总结出来,供大家分享学习。本篇随笔主要介绍TX Text Control V20的相关使用心得。

1、TX Text Control控件介绍

TX Text Control是一款功能类似于 MS Word 的文字处理控件,包括文档创建、编辑、打印、邮件合并、格式转换、拆分合并、导入导出、批量生成等功能。广泛应用于企业文档管理,网站内容发布,电子病历中病案模板创建、病历书写、修改历史、连续打印、病案归档等功能的实现。

这个控件主要的功能就是可以作为Word以及其他文档的编辑器使用,虽然展示WORD内容的控件也有一些,如我们可以利用DevExpress里面的RTF文档编辑器来实现,同样运行的很好,结合Aspose.Word后台的文档处理,我们可以做到类似报表的数据生成,而且可以把生成后的文档进行显示、编辑等操作处理。

TX Text Control虽然作为文档编辑各方面都表现不错,不过其MailMerge邮件合并功能还是经常使用的一个功能,就是把我们的数据和文档模板来一个合并,然后显示最终的文档内容,这种可以用来做一些类似发票、邮件、员工信息等的数据处理和显示,MailMerge邮件合并可以绑定主从表的数据,能够符合大多数的要求。

我本来想用它做一个类似电子病历一样的功能模块,不说在文档里面,我们很难做到一些下拉列表的处理( 官方博客里面有一个简单的案例,不过不好用),一般情况下,如果我们只是做文档展示、数据合并等常规的操作,还是很不错的。

这个控件的功能介绍,可以参考葡萄城里面的网页介绍(
http://www.gcpowertools.com.cn/products/textcontrol_winform_features.htm
),这个控件的相关开发人员使用然后分享经验的文章很少,能在网上搜到的大多数是葡萄城人员对这个控件的Demo代码进行一个简单粘贴说明,没有进一步的深入介绍和应用场景的介绍。虽然葡萄城列举了几个电子病历的公司产品案例,不过这几家公司的电子病历产品是很难下载到,也无从知道真假或者使用情况。

这几天我把这个控件的各种特性做了一些学习,并重新把官网的文档编辑例子进行了全新开发,参考着做了一个完全一样的编辑器版本,也基本上对它的各个属性、方法处理有了一个更加深入的了解。

我们先通过一个软件界面来了解整个软件的一些功能(这个是我仿照官方案例做的一个程序)。

这个控件默认安装后,会带有很多Demo案例,具体可以参考目录C:\Users\Administrator\Documents\TX Text Control 20.0.NET for Windows Forms\Samples\ 进行了解。

2、TX Text Control控件的汉化

这个控件界面默认是英文版本的,控件的相关菜单以及提示都是英文,因此我们需要对资源做一些中文本地化处理才能正确显示。

官方没有提供中文汉化包,只提供一个标准的英文资源,如下所示。

我们需要做的就是将它们进行中文翻译,然后重新编译(使用buildres.bat脚本编译)为中文资源dll。

我们先使用VS编辑工具,把这些英文资源记录转换为英文(这是一个比较繁琐的工作,官方网站上有一些旧版本的中文包可供参考,以及最新的V20软件(编辑器软件)下载下来运行参考)。

我们逐一进行中文处理,可以使用百度、Google的翻译,以及软件界面的参考哦。

以管理员方式运行VS的命令行,然后执行命令进行编译资源即可。

buildres.bat zh-CN

编译成功后,在目录里面,会增加两个资源程序集。

txdocumentserver.resources.dll

txtextcontrol.resources.dll

然后我们把它复制到运行目录下,并放在zh-CN的目录里面即可。有了这些中文化的资源程序集,我们就可以利用它进行对控件的内置菜单提示进行中文化了。

中文化操作和其他常规的做法一样,我们在Main函数里面,添加如下代码即可。

Thread.CurrentThread.CurrentUICulture = new System.Globalization.CultureInfo("zh-CN");
Thread.CurrentThread.CurrentCulture
= new System.Globalization.CultureInfo("zh-CN");

运行程序,我们使用右键菜单,发现里面的资源都已经正常汉化了,其他相关的内置菜单和界面也都可以看到正常汉化。

3、TX Text Control的使用

有了汉化,只是我们正常使用控件的第一步,我们需要在程序里面整合控件,那么就需要对它进行使用,以及对控件的属性、事件进行处理,才能得到最佳的应用效果。

我们在VS工具栏里面加入对应的控件,可以看到有以下相关的控件对象可供使用,一般情况下我们使用TextControl,然后在其基础上创建其他RulerBar、ButtonBar、StatusBar即可,而如果我们需要合并数据(很常用)就需要加入MailMerge控件对象。

添加控件后,我们可以对控件的相关基础的复制、粘贴、剪切等操作可以直接利用控件的API即可实现。

        private void menuEdit_Undo_Click(objectsender, EventArgs e)
{
_textControl.Undo();
}
private void menuEdit_Redo_Click(objectsender, EventArgs e)
{
_textControl.Redo();
}
private void menuEdit_Cut_Click(objectsender, EventArgs e)
{
_textControl.Cut();
}

其中查找、替换对话框也是可以通过API进行调出。

        private void menuEdit_Find_Click(objectsender, EventArgs e)
{
_textControl.Find();
}
private void menuEdit_Replace_Click(objectsender, EventArgs e)
{
_textControl.Replace();
}

利用这些最基础的API是常规的操作。

而利用插入相关的对象,如图片、文本框等,就需要做一些简单的编码,方便把对象加入到TextControl对象里面。

        private void menuInsert_Image_Click(objectsender, EventArgs e)
{
TXTextControl.Image imageNew
= newTXTextControl.Image();
_textControl.Images.Add(imageNew, TXTextControl.HorizontalAlignment.Left,
-1, TXTextControl.ImageInsertionMode.DisplaceText);
}
private void menuInsert_TextFrame_Click(objectsender, EventArgs e)
{
try{//Force Exception if standard version: _textControl.TextFrames.GetItem();
Size sizeTextFrame
= new Size(2268, 2268); //4 x 4 cm TXTextControl.TextFrame textFrameNew= newTXTextControl.TextFrame(sizeTextFrame);
_textControl.TextFrames.Add(textFrameNew, TXTextControl.HorizontalAlignment.Left,
-1, TXTextControl.TextFrameInsertionMode.DisplaceCompleteLines);
}
catch(Exception ex)
{
MessageBox.Show(ex.Message, ProductName);
}
}

这个控件最常见的就是MailMerge进行合并数据的操作了,这个也是我们利用它来处理很多模板化文档的目的。

MailMerge对象合并数据的操作,主要是接受集合对象或者是DataTable对象,所以我们必须将我们的数据转换为这种格式,否则合并数据得不到要的结果。

合并数据的处理方式,最开始就是需要设计好模板,这点很重要,模板的设计还是沿用了常规Word文档域对象的概念,需要添加一些域来做后续数据替换的对象占位符,如下是我测试的一个模板。

这个里面主要是主从表整合的一个模板,我们需要绑定常规的主表记录,也需要绑定明细表的集合记录,不过最后我们都需要把数据对象转换为集合(如DataSet),然后才能绑定到文档对象上去。

在上面的文档里面,你知识看到了域对象,而没有看到一个隐藏的一个集合记录的开始和结束的书签设置。关于书签的作用和如何操作,可以了解我之前的随笔文章《
利用Aspose.Word控件实现Word文档的操作
》、《
利用Aspose.Word控件和Aspose.Cell控件,实现Word文档和Excel文档的模板化导出

书签的作用很重要,否则无法正常解析集合的记录并绑定在WORD界面上的,我们打开书签管理对话框,可以看到上述文档里面有两个位置,书签标记的开始和结束位置。

这样我们设计好模板后,第二步就是通过代码生成相关对象,然后和文档进行合并就可以了。

例如我构建一个主表和一个从表的记录,统一把它们生成一个DataSet对象供使用。

        public staticDataSet CreateDataSet()
{
DataSet ds
= newDataSet();

DataTable dtMain
= DataTableHelper.CreateTable("Company,HandNo,Creator,CreateTime|DateTime");
dtMain.TableName
= "main";
DataRow dr
=dtMain.NewRow();
dr[
"Company"] = "广州爱奇迪软件科技有限公司";
dr[
"HandNo"] = "123456";
dr[
"Creator"] = "伍华聪";
dr[
"CreateTime"] =DateTime.Now;
dtMain.Rows.Add(dr);

DataTable dt
= DataTableHelper.CreateTable("ID,ProductName,Description,Price|decimal,Quantity|int");
dt.TableName
= "ProductInfo";
dr
=dt.NewRow();
dr[
"ID"] = "1";
dr[
"ProductName"] = "海飞丝洗发水";
dr[
"Description"] = "海飞丝洗发水, 550ml";
dr[
"Price"] = 19.8M;
dr[
"Quantity"] = 100;
dt.Rows.Add(dr);

dr
=dt.NewRow();
dr[
"ID"] = "2";
dr[
"ProductName"] = "联想品牌电脑";
dr[
"Description"] = "联想Y700-15ISK-ISE 旗舰版";
dr[
"Price"] =6500M;
dr[
"Quantity"] = 10;
dt.Rows.Add(dr);

dr
=dt.NewRow();
dr[
"ID"] = "3";
dr[
"ProductName"] = "IPhone7 128G";
dr[
"Description"] = "苹果IPhone7, 128G";
dr[
"Price"] =5800M;
dr[
"Quantity"] = 10;
dt.Rows.Add(dr);

ds.Tables.Add(dtMain);
ds.Tables.Add(dt);
returnds;
}

先加载模板文档

if (setting == null)
{
setting
= newTXTextControl.LoadSettings();
setting.ApplicationFieldFormat
=TXTextControl.ApplicationFieldFormat.MSWord;
}
_textControl.Load(Application.StartupPath
+ "\\Template\\template1.docx", TXTextControl.StreamType.WordprocessingML, setting);

整合合并数据

DataSet ds =PurchaseInfoHelper.CreateDataSet();
mailMerge1.MergeBlocks(ds);
mailMerge1.Merge(ds.Tables[
"main"], true);

最后就可以看到我们所需要的结果了。

当然,如果很熟悉Aspose.Word控件的使用,我们其实也可以利用Aspose.Word控件来做后台的数据整合处理,Aspose.Word控件支持很多变量定义,以及更加复杂的处理,如我把原来在框架模块里面的人员信息导出Word功能抽取出来,这个模块原先是利用Aspose.Word来处理数据合并的,我不修改其中的逻辑,只是把合并后的数据展示在TX Text Control即可,如下代码所示。

var saveFile =StaffHelper.GenerateDoc();//加载文档
_textControl.Load(saveFile, StreamType.MSWord);

最后就生成了我们开始介绍的软件界面效果。

这个控件目前使用起来还算不错,不过对于一些数据源的处理方面,以后希望继续增加更多的接口,继续保持观察,希望能将研究的成果用在具体的项目上。