2023年2月

我曾经在系列文章中的《
C#开发微信门户及应用(11)--微信菜单的多种表现方式介绍
》中介绍了微信菜单里面的重定向操作,通过这个重定向操作,我们可以获取一个code值,然后获取用户的openID,进而就能获取到更多的用户信息,这个在会员信息的场景里面用的很多,本篇介绍在网站中迅速配置这样的菜单链接,并介绍如何在后台获取相关的用户信息,实现页面数据个性化的展现操作。

我们知道,微信的自定义菜单分为两大类,分别对应Click类型和View类型的,而重定向属于View类型的一种,如下所示。

1、微信重定向菜单的配置

微信重定向的菜单,就是通过传入一个地址参数,让微信服务器进行跳转,它的主要规则如下所示。

对于scope=snsapi_base方式的链接如下:

https://open.weixin.qq.com/connect/oauth2/authorize?appid=wx3d81fc2886d86526&redirect_uri=
http%3A%2F%2Fwww.iqidi.com%2Ftestwx.ashx
&response_type=code&
scope=snsapi_base
&state=123#wechat_redirect

而对于scope=snsapi_userinfo方式的链接如下:

https://open.weixin.qq.com/connect/oauth2/authorize?appid=wx3d81fc2886d86526&redirect_uri=
http%3A%2F%2Fwww.iqidi.com%2Ftestwx.ashx
&response_type=code&scope=
snsapi_userinfo
&state=123#wechat_redirect

这两个菜单链接主要就是对我们给定的链接地址进行UrlEncode处理,然后把它赋值给参数redirect_uri实现的。

由于链接地址比较长,如果每次需要在配置菜单的时候,都复制过来修改,非常不方便,我们可以在自定义菜单的配置界面里面,增加一个按钮功能,对内容进行处理,以便实现我们需要的地址转换,我的门户应用平台对自定义菜单的操作就是基于这个思路实现。

默认我们只需要填写一个需要重定向的url地址就可以了,如下所示。

如果需要配置成重定向的菜单链接地址,那么调用【转换重定向菜单】按钮操作,使用脚本函数进行转换就可以了,转换后的结果如下所示。

原来就是利用后台的javascript实现参数的URL转码,还需要获取后台的AppId,这样才能构造成完整的地址连接。

2、脚本转换操作的实现代码

前面说了,第一是需要实现URL转码,第二是获取后台的AppId,然后生成一个完整的URL就可以了。为了避免大家的重复研究,我把这部分代码贴出来一起学习下。

在使用前,我们还需要注意一个问题,就是重定向到指定页面后,这个页面会带有一个code的参数,这个参数非常重要,我们需要获取出来,当然也是通过javascript来获取对应的code参数了。

这个逻辑可以用一个脚本函数来实现,如下所示

        functiongetUrlVars(){var vars =[], hash;var hashes = window.location.href.slice(window.location.href.indexOf('?') + 1).split('&');for(var i = 0; i < hashes.length; i++)
{
hash
= hashes[i].split('=');
vars.push(hash[
0]);
vars[hash[
0]] = hash[1];
}
returnvars;
}

定义了这个函数后,我们在重定向的页面里面,可以获取code参数的操作如下所示。

var code = getUrlVars()["code"];

先放下这些,我们先来讨论如何把链接地址转换为需要的链接地址操作。

我们为了实现链接地址的互相转换(为了方便),我们可以判断链接地址是否含有qq的域名就可以了。

if (url.indexOf("https://open.weixin.qq.com/connect/oauth2/authorize?") == 0) {var redirect_uri = getUrlVars(url)["redirect_uri"];if (redirect_uri != "") {var newUrl =decodeURIComponent(redirect_uri);
$(
"#" +ctrlName).val(newUrl);
}
}

而如果是我们输入的正常链接,那么就应该把它转换为重定向的链接地址,如下所示。

else{var newUrl =encodeURIComponent(url);var reNewUrl = "https://open.weixin.qq.com/connect/oauth2/authorize?appid=@ViewBag.appid&redirect_uri=" + newUrl + "&response_type=code&scope=snsapi_base&state=123#wechat_redirect";
$(
"#" +ctrlName).val(reNewUrl);
}

其中重定向链接需要带有一个当前微信开发用户的appId,这个不是固定的,是不同的开发人员都不一样的东西,这里使用了MVC的动态对象进行绑定:@ViewBag.appid。

在对应的MenuController控制器里面,给它赋值就可以了。

        /// <summary>
        ///默认的视图控制方法/// </summary>
        /// <returns></returns>
        public overrideActionResult Index()
{
ViewBag.appid
=GetAppId();returnView();
}

这样配置后的重定向菜单地址列表就如下所示了,我们打开对应的记录详细页面,可以通过页面里面的功能按钮,随时对重定向菜单的地址进行转换,方便了解详细的链接内容。

3、重定向页面的设计及处理

配置了上面的链接地址后,我们需要在网站里面增加这样的一个页面进行处理用户的信息,一般情况下,我们可能是为了方便用户查看自己的微信基础信息,也为了给用户绑定用户个人数据使用的用途的,如用户可以绑定手机、Email邮箱等操作,还可以绑定和业务系统相关的用户名。这样用户就可以快速注册会员或者和后台的系统进行关联了。

我设计的两个用户信息展示界面如下所示。

这两个界面主要使用了Jquery Mobile的相关内容,对界面进行了处理,整个模块结合了短信验证码的方式,对用户的手机进行验证处理,这样能够更高效的实现信息准确的绑定操作,当然,还可以结合外部系统,绑定用户的账号密码,这样用户可以在微信进入微网站平台进行购物、数据维护、业务管理等操作了,其实一旦绑定外部系统的ID,也就是提供了一个快速进行外部系统的入口了。


具体的内容在下一篇继续介绍了。

如果对这个《C#开发微信门户及应用》系列感兴趣,可以关注我的其他文章,系列随笔如下所示:

C#开发微信门户及应用(20)-微信企业号的菜单管理

C#开发微信门户及应用(19)-微信企业号的消息发送(文本、图片、文件、语音、视频、图文消息等)

C#开发微信门户及应用(18)-微信企业号的通讯录管理开发之成员管理

C#开发微信门户及应用(17)-微信企业号的通讯录管理开发之部门管理

C#开发微信门户及应用(16)-微信企业号的配置和使用

C#开发微信门户及应用(15)-微信菜单增加扫一扫、发图片、发地理位置功能

C#开发微信门户及应用(14)-在微信菜单中采用重定向获取用户数据

C#开发微信门户及应用(13)-使用地理位置扩展相关应用

C#开发微信门户及应用(12)-使用语音处理

C#开发微信门户及应用(11)--微信菜单的多种表现方式介绍

C#开发微信门户及应用(10)--在管理系统中同步微信用户分组信息

C#开发微信门户及应用(9)-微信门户菜单管理及提交到微信服务器

C#开发微信门户及应用(8)-微信门户应用管理系统功能介绍

C#开发微信门户及应用(7)-微信多客服功能及开发集成

C#开发微信门户及应用(6)--微信门户菜单的管理操作

C#开发微信门户及应用(5)--用户分组信息管理

C#开发微信门户及应用(4)--关注用户列表及详细信息管理

C#开发微信门户及应用(3)--文本消息和图文消息的应答


C#开发微信门户及应用(2)--微信消息的处理和应答


C#开发微信门户及应用(1)--开始使用微信接口

Web开发上有很多HTML的编辑控件,如CKEditor、kindeditor等等,很多都做的很好,本文主要介绍在MVC界面里面,CKEditor的配置和使用。CKEditor
的前身是FCKEditor,随着它的更新,上传图片的功能被分离出去了,现在如果需要实现上传图片,要么自己写代码或者采用其他上传控件(如Uploadify),还有一种方法是使用CKFinder,这两者的合并使用,能给我们带来更多的方便。

1、CKEditor的使用

CKEditor的下载地址是
http://ckeditor.com/download
,里面可以根据需要进行样式的定制,本文主要介绍当前较新的版本4.4.1的继承使用。而CKFinder的下载地址是:
http://ckfinder.com/download

CKEditor的使用比较简单,一般情况下根据官方的指引选择适当的样式下载就可以了,使用的时候,基本不需要怎么样修改配置文件。在MVC的视图页面里面,添加相关的引用文件就可以了。

@*添加对ckeditor的支持*@<scriptsrc="~/Content/JQueryTools/ckeditor/ckeditor.js"></script>
    <scriptsrc="~/Content/JQueryTools/ckeditor/adapters/jquery.js"></script>

然后在界面添加需要编辑HTML内容的textarea控件,由于我的使用的是EasyUI的控件,因此设置了控件样式class="easyui-validatebox",也可以不管这里。

<tr>
                        <th>
                            <labelfor="Content">内容:</label>
                        </th>
                        <td>
                            <textareaclass="easyui-validatebox"id="Content"name="Content"style="width:1024px"></textarea>
                        </td>
                    </tr>

我们一般使用的时候,需要一段javascript脚本进行初始化对应的控件,初始化代码如下所示。

    <script>
        functioninitEditor() {
$(
'#Content').ckeditor();//控件1
$(
'#Content1').ckeditor();//控件2
}
</script>

添加相应的脚本和控件初始化代码后,就可以在界面中呈现出需要的效果了。

而之后的控件使用,就和普通的使用没有两样了,如赋值语句如下所示。

$('#Content1').val(info.Content);//ckeditor赋值

获取控件的值,也和普通给的一样

var content = $("#Content1").val();

2、CKFinder的集成使用

虽然CKFinder已经独立出来,但是它也提供了完美的整合效果,我们把它下载后,在压缩包里面的bin目录里面找到相应的 ckFinder.dll,把它添加我们项目工程的引用里面去,我们才能正常使用文件上传功能。

然后修改config.ascx文件里面的一些设置,使得我们能够顺利使用。

第一步设置CheckAuthentication函数返回True。

public override bool CheckAuthentication()
{
return true;
}

第二步是设置SetConfig函数里面的BaseURL,这个基础地址需要设置和我们项目的地址一致,否则上传文件有问题。

    public override voidSetConfig()
{
//The base URL used to reach files in CKFinder through the browser. BaseUrl = "/Content/JQueryTools/ckfinder/userfiles/";

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

第三步,整合CKFinder到CKEditor,前面说了CKEditor默认是没有文件上传的功能操作的,需要添加CKFinder并进行配置才可以使用。

这步骤需要在CKEditor里面的config.js文件里面修改下面的配置参数(红色部分就可以了)。

CKEDITOR.editorConfig =function( config ) {//Define changes to default configuration here.//For complete reference see:// http://docs.ckeditor.com/#!/api/CKEDITOR.config
.......................

config.filebrowserBrowseUrl
= '/Content/JQueryTools/ckfinder/ckfinder.html'; //上传文件时浏览服务文件夹
config.filebrowserImageBrowseUrl = '/Content/JQueryTools/ckfinder/ckfinder.html?Type=Images'; //上传图片时浏览服务文件夹
config.filebrowserFlashBrowseUrl = '/Content/JQueryTools/ckfinder/ckfinder.html?Type=Flash'; //上传Flash时浏览服务文件夹
config.filebrowserUploadUrl = '/Content/JQueryTools/ckfinder/core/connector/aspx/connector.aspx?command=QuickUpload&type=Files'; //上传文件按钮(标签)
config.filebrowserImageUploadUrl = '/Content/JQueryTools/ckfinder/core/connector/aspx/connector.aspx?command=QuickUpload&type=Images'; //上传图片按钮(标签)
config.filebrowserFlashUploadUrl = '/Content/JQueryTools/ckfinder/connector/aspx/connector.aspx?command=QuickUpload&type=Flash'; //上传Flash按钮(标签)
};

3、集成效果展示

通过以上代码进行整合,在插入图片的操作页面里面,会增加一个浏览服务器按钮,上传操作选项卡等功能,方便对文件的浏览和上传操作,具体效果如下所示。


以上就是我在我的Web框架里面整合的HTML编辑控件和CKFinder文件上传组件,这两个组合起来,能够非常方便构建图文并茂的文章内容。

4、MVC的处理

这里需要注意的是由于textarea中有特殊字符,出于安全原因,默认情况mvc框架不允许提交的,应在相应方法上加上[ValidateInput(false)]属性。

如我为了提交HTML内容,需要在控制器对象里面,重写了内容的写入和更新操作函数,如下所示。

[ValidateInput(false)]public overrideActionResult Insert(InformationInfo info)
{
info.Editor
=CurrentUser.Name;
info.EditTime
=DateTime.Now;return base.Insert(info);
}

[ValidateInput(
false)]public override ActionResult Update(stringid, FormCollection formValues)
{
return base.Update(id, formValues);
}

如果对这个系列的其他内容总结有兴趣,也可以看看下面的连接:

基于MVC4+EasyUI的Web开发框架的系列文章:

基于MVC4+EasyUI的Web开发框架形成之旅--总体介绍


基于MVC4+EasyUI的Web开发框架形成之旅--MVC控制器的设计

基于MVC4+EasyUI的Web开发框架形成之旅--界面控件的使用

基于MVC4+EasyUI的Web开发框架形成之旅--附件上传组件uploadify的使用

基于MVC4+EasyUI的Web开发框架形成之旅--框架总体界面介绍

基于MVC4+EasyUI的Web开发框架形成之旅--基类控制器CRUD的操作

基于MVC4+EasyUI的Web开发框架形成之旅--权限控制

基于MVC4+EasyUI的Web开发框架经验总结(1)-利用jQuery Tags Input 插件显示选择记录

基于MVC4+EasyUI的Web开发框架经验总结(2)- 使用EasyUI的树控件构建Web界面

基于MVC4+EasyUI的Web开发框架经验总结(3)- 使用Json实体类构建菜单数据

基于MVC4+EasyUI的Web开发框架经验总结(4)--使用图表控件Highcharts

基于MVC4+EasyUI的Web开发框架经验总结(5)--使用HTML编辑控件CKEditor和CKFinder

基于MVC4+EasyUI的Web开发框架经验总结(6)--在页面中应用下拉列表的处理

基于MVC4+EasyUI的Web开发框架经验总结(7)--实现省份、城市、行政区三者联动

基于MVC4+EasyUI的Web开发框架经验总结(8)--实现Office文档的预览

基于MVC4+EasyUI的Web开发框架经验总结(9)--在Datagrid里面实现外键字段的转义操作

基于MVC4+EasyUI的Web开发框架经验总结(10)--在Web界面上实现数据的导入和导出

基于MVC4+EasyUI的Web开发框架经验总结(11)--使用Bundles处理简化页面代码

基于MVC4+EasyUI的Web开发框架经验总结(12)--利用Jquery处理数据交互的几种方式

基于MVC4+EasyUI的Web开发框架经验总结(13)--DataGrid控件实现自动适应宽带高度

基于MVC4+EasyUI的Web开发框架经验总结(14)--自动生成图标样式文件和图标的选择操作

在很多应用系统里面,虽然一般采用一种数据库运行,但是由于各种情况的需要,可能业务系统会部署在不同类型的数据库上,如果开发的系统能够很方便支持多种数据库的切换,那可以为我们减少很多烦恼,同时提高系统的适应性和强壮型。还有一种情况,由于业务数据库的不断膨胀或者方便数据库的切割隔离,有时候也会把不同的业务数据库进行分拆,如权限提供数据库,客户关系管理数据库,工作流程数据库,企业营运数据库等等,因此在一个系统里面,同时使用2个或者以上的数据库的情况也是有的。针对这两种情况,本文介绍在我的Winform开发框架(也使用我的其他框架),如何具体处理这两个问题的。

在我的各种开发框架里面,底层都是采用同一种数据库的访问方式,就是采用了Enterprise Library的数据库访问模块,这个是微软开源的企业应用模块,里面各种应用模块,都堪称是开发的最佳实践。当然利用里面的数据库访问模块,是很广泛的,这个数据库访问模块,可以通过配置的方式支持多种数据库的变化。因此整合到我的Winform开发框架里面,也很容易实现这种多数据库的方式处理。

Winform开发框架,常见的分层模式,可以分为UI层、BLL层、DAL层、IDAL层、Entity层、公用类库层等等,B/S的Web开发框架也是提供类似的架构模式,它们只是Web界面层有所不同,这样就给我们提供了一个统一性的开发模式,使得开发起来更加高效,统一性更好。框架界面层以下的架构设计图如下所示。

1、支持多数据库的设置

1)数据库访问基类的了解

为了介绍支持多种数据库的模式,我们需要先来了解下整个框架的层次结构。

AbstractBaseDAL是抽象了所有数据库实现的超级基类。

BaseDALSQL是针对SqlServer数据库部分调整基类(很小的调整)。

BaseDALSQLite是针对Sqlite数据库的部分调整基类(很小的调整)。

BaseDALMySql是针对MySqlite数据库的部分调整基类(很小的调整)。

BaseDALAccess是针对Access数据库的部分调整基类(很小的调整)。

IBaseDAL是所有基础数据库访问类的接口。

数据访问接口实现层(如Customer)和接口定义层(ICustomer)一样,都有一个基类,如基于SqlServer实现的基类为BaseDALSQL,这个基于SqlServer的数据访问基类,它也是继承自一个超级基类(大多数的实现在这里)AbstractBaseDAL。他们之间的继承关系如下所示

而我们刚才在项目工程的图里面看到,BaseDALSQL、IBaseDAL、AbstractBaseDAL这些类库由于具有很大的通用性,为了减少在不同的项目中进行复制导致维护问题,因此我们全部把这些经常使用到的基类或者接口,抽取到一个独立的类库里面,为了和普通的DotNET公用类库命名进行区分(WHC.Framework.Commons),我们把它命名为WHC.Framework.ControlUtil。

2)多数据库的代码实现

为了实现多数据库的支持,我们需要在配置文件里面读取相关的配置,看具体是构造那种该数据库的,然后进行初始化不同的程序集类,从而实现调用不同数据库类型的数据库访问类。

在BaseBLL的Init函数里面的实现代码如下所示。

            #region 根据不同的数据库类型,构造相应的DAL层AppConfig config= newAppConfig();string dbType = config.AppConfigGet("ComponentDbType");if (string.IsNullOrEmpty(dbType))
{
dbType
= "sqlserver";
}
dbType
=dbType.ToLower();string DALPrefix = "";if (dbType == "sqlserver")
{
DALPrefix
= "DALSQL.";
}
else if (dbType == "access")
{
DALPrefix
= "DALAccess.";
}
else if (dbType == "oracle")
{
DALPrefix
= "DALOracle.";
}
else if (dbType == "sqlite")
{
DALPrefix
= "DALSQLite.";
}
else if (dbType == "mysql")
{
DALPrefix
= "DALMySql.";
}
#endregion this.dalName = bllFullName.Replace(bllPrefix, DALPrefix);//替换中级的BLL.为DAL.,就是DAL类的全名 baseDal = Reflect<IBaseDAL<T>>.Create(this.dalName, dalAssemblyName);//构造对应的DAL数据访问层的对象类

在具体的业务对象的调用的时候,我们不知道它具体是调用哪个数据库的处理类进行处理的,只需要调用它的基础接口就可以了,如下是界面层的部分调用代码。

            //删除关联的附件
            if (!string.IsNullOrEmpty(ids))
{
string[] idArray = ids.Split(',');foreach (string id inidArray)
{
InformationInfo info
=BLLFactory<Information>.FindByID(id);if (info != null && !string.IsNullOrEmpty(info.Attachment_GUID))
{
BLLFactory
<FileUpload>.Instance.DeleteByAttachGUID(info.Attachment_GUID);
}
}
}

在具体的配置文件里面,我们就可以根据需要配置好相关的数据库了。

根据Enterprise Library的配置,我们只要制定了<dataConfiguration defaultDatabase="sqlserver">,那么就会获取sqlServer的节点,而代码通过解析ComponentDbType配置项,就可以构造对应的数据库访问对象了。两者合一就可以
正确
获取到处理对象,并成功处理数据库的访问。

<configuration>
  <configSections>
    <sectionname="dataConfiguration"type="Microsoft.Practices.EnterpriseLibrary.Data.Configuration.DatabaseSettings, Microsoft.Practices.EnterpriseLibrary.Data"/>
  </configSections>
  <connectionStrings>
    <addname="sqlserver"providerName="System.Data.SqlClient"connectionString="Persist Security Info=False;Data Source=(local);Initial Catalog=MVCWebMis;Integrated Security=SSPI"/>
    <addname="access"providerName="System.Data.OleDb"connectionString="Provider=Microsoft.Jet.OLEDB.4.0;Data Source=|DataDirectory|\ComponentData.mdb;User ID=Admin;Jet OLEDB:Database Password=;"/>
    <addname="sqlite"providerName="System.Data.SQLite"connectionString="Data Source=|DataDirectory|\ComponentData.db;Version=3;"/>
    <addname="oracle"providerName="System.Data.OracleClient"connectionString="Data Source=whcdb;User ID=whc;Password=whc"/>
  </connectionStrings>
  <dataConfigurationdefaultDatabase="sqlserver">
    <providerMappings>
      <adddatabaseType="EntLibContrib.Data.SQLite.SQLiteDatabase, EntLibContrib.Data.SqLite"name="System.Data.SQLite"/>
    </providerMappings>
  </dataConfiguration>
  
  <appSettings>
    <!--组件的数据库类型:access、sqlserver、sqlite等-->
    <addkey="ComponentDbType"value="sqlserver"/>

2、支持分拆不同数据库的设置

上面介绍的方式,一次性只能访问一个数据库,因此默认在代码构造数据库访问对象的时候,是通过下面的代码进行的。

Database db = DatabaseFactory.CreateDatabase();

这样每次只会获取defaultDatabase设置的数据库进行构造,如果我们在一个系统里面,同时支持多个数据库的访问,那么我们应该如何处理呢。

在框架的基类AbstractBaseDAL里面,我们对构造数据库的代码进行了封装。

        /// <summary>
        ///根据配置数据库配置名称生成Database对象/// </summary>
        /// <returns></returns>
        protected virtualDatabase CreateDatabase()
{
Database db
= null;if (string.IsNullOrEmpty(dbConfigName))
{
db
=DatabaseFactory.CreateDatabase();
}
else{
db
=DatabaseFactory.CreateDatabase(dbConfigName);
}
returndb;
}

而每个DAL层都会继承自AbstractBaseDAL,这样也就可以通过在DAL层指定dbConfigName进行使用不同的数据库的了。

但是这样问题出现了,假如我们有5个不同类型(SqlServer、Oracle、Mysql、Access、Sqlite)的数据库的DAL层,那么每个DAL层的实现类都要写一些代码,这样挺不方便,那么是否可以把它抽象到BLL层,这样只写一次配置就可以了,这个思路很好,我们具体来看看如何实现。

1)在IBaseDAL层定义接口

    /// <summary>
    ///数据访问层的接口/// </summary>
    public interface IBaseDAL<T> whereT : BaseEntity
{
/// <summary> ///设置数据库配置项名称/// </summary> /// <param name="dbConfigName">数据库配置项名称</param> void SetDbConfigName(stringdbConfigName);
.............
}

2、在AbstractBaseDAL添加默认实现代码

    /// <summary>
    ///数据访问层的超级基类,所有数据库的数据访问基类都继承自这个超级基类,包括Oracle、SqlServer、Sqlite、MySql、Access等/// </summary>
    public abstract class AbstractBaseDAL<T> where T : BaseEntity, new()
{
/// <summary> ///设置数据库配置项名称/// </summary> /// <param name="dbConfigName">数据库配置项名称</param> public virtual void SetDbConfigName(stringdbConfigName)
{
this.dbConfigName =dbConfigName;
}
....................
}

3、在BaseBLL里面的Init函数进行调用设置处理

        /// <summary>
        ///参数赋值后,初始化相关对象/// </summary>
        /// <param name="bllFullName">BLL业务类的全名(子类必须实现),子类构造函数传入this.GetType().FullName</param>
        /// <param name="dalAssemblyName">数据访问层程序集的清单文件的文件名,不包括其扩展名。设置为NULL或默认为Assembly.GetExecutingAssembly().GetName().Name</param>
        /// <param name="bllPrefix">BLL命名空间的前缀(BLL.)</param>
        /// <param name="dbConfigName">数据库配置项名称</param>
        protected void Init(string bllFullName, string dalAssemblyName = null, string bllPrefix = "BLL.")
{
.............
baseDal.SetDbConfigName(dbConfigName);
//设置数据库配置项名称 }

4、在具体
BLL层
的业务类进行初始化处理。

    /// <summary>
    ///政策法规公告动态/// </summary>
    public class Information : BaseBLL<InformationInfo>{public Information() : base()
{
base.Init(this.GetType().FullName, System.Reflection.Assembly.GetExecutingAssembly().GetName().Name, "BLL.", "annotherConfig");
}

这样设置后,我们具体调用的代码不变,但是指定业务的数据库访问已经使用了特定的配置项名称了,如果配置项不存在,那么还是会获取默认的配置项进行处理了。

通过这样的实现步骤,我们就能实现在一个业务系统里面,分拆不同的数据库,进行统一管理,而且又不会增加额外的调用难度,对于我们很多业务表,这种框架的处理方式 ,应该是不错的。

使用NVelocity也有几个年头了,主要是在我的代码生成工具Database2Sharp上使用来生成相关代码的,不过NVelocity是一个非常不错的模板引擎,可以用来生成文件、页面等相关处理,非常高效和方便。

它原先是在网站
http://nvelocity.sourceforge.net/
上维护,不过从0.41后,该网站就不再进行NVelocity更新了,现在可以在网站http://nvelocity.codeplex.com/上获得最新版本的更新,接着版本的更新操作,我们把NVelocity的几种生成文件的操作介绍一下,以便大家进行更深入的了解。

我在早期几篇文章也介绍过NVelocity的使用,主要介绍了NVelocity的语法和逻辑的和使用,还有就是如何在实际项目中进行的内容动态生成的操作,有兴趣可以翻下下面几篇文章:

强大的模板引擎开源软件NVelocity


Database2Sharp版本更新之自定义模板生成


使用NVelocity0.5实现服务器端页面自动生成

1、基于NVelocity的几种内容生成方式

从上面的图示,我们可以看到,NVelocity的模板化生成包含了3种方式,一种是从文件到文件或者字符串,一种是从字符串到字符串,他们各自的处理方式有所不同,但是都能正确解析里面的内容。

为了更好利用NVelocity的特性,我们对它进行一个初步的辅助类封装。

    /// <summary>
    ///基于NVelocity的模板文件生成辅助类/// </summary>
    public classNVelocityHelper
{
protectedVelocityContext context;protectedTemplate template;protected stringtemplateFile;/// <summary> ///存放键值的字典内容/// </summary> private Dictionary<string, object> KeyObjDict = new Dictionary<string, object>();/// <summary> ///添加一个键值对象/// </summary> /// <param name="key">键,不可重复</param> /// <param name="value"></param> /// <returns></returns> public NVelocityHelper AddKeyValue(string key, objectvalue)
{
if (!KeyObjDict.ContainsKey(key))
{
KeyObjDict.Add(key, value);
}
return this;
}
................

上面的AddKeyValue方法,主要用来为模板引擎添加一些需要绑定在页面上的变量对象,这样页面变量参数的内容就能正确解析出来了。

为了使用NVelocity的各种特性,我们需要在辅助类里面构造模板的相关信息,设置相关参数。

        /// <summary>
        ///初始化模板引擎/// </summary>
        protected virtual voidInitTemplateEngine()
{
try{//Velocity.Init(NVELOCITY_PROPERTY); VelocityEngine templateEngine = newVelocityEngine();
templateEngine.SetProperty(RuntimeConstants.RESOURCE_LOADER,
"file");

templateEngine.SetProperty(RuntimeConstants.INPUT_ENCODING,
"utf-8");
templateEngine.SetProperty(RuntimeConstants.OUTPUT_ENCODING,
"utf-8");//如果设置了FILE_RESOURCE_LOADER_PATH属性,那么模板文件的基础路径就是基于这个设置的目录,否则默认当前运行目录 templateEngine.SetProperty(RuntimeConstants.FILE_RESOURCE_LOADER_PATH, AppDomain.CurrentDomain.BaseDirectory);

templateEngine.Init();

template
=templateEngine.GetTemplate(templateFile);
}
catch(ResourceNotFoundException re)
{
string error = string.Format("Cannot find template" +templateFile);

LogTextHelper.Error(error);
throw newException(error, re);
}
catch(ParseErrorException pee)
{
string error = string.Format("Syntax error in template" + templateFile + ":" +pee.StackTrace);
LogTextHelper.Error(error);
throw newException(error, pee);
}
}

在生成内容之前,需要把相关的对象属性绑定到模板引擎的上下文对象里面。

        /// <summary>
        ///初始化上下文的内容/// </summary>
        private voidInitContext()
{
context
= newVelocityContext();foreach (string key inKeyObjDict.Keys)
{
context.Put(key, KeyObjDict[key]);
}
}

1)根据模板文件构造对应的文件内容

        /// <summary>
        ///根据模板创建输出的文件,并返回生成的文件路径/// </summary>
        public virtual stringExecuteFile()
{
string fileName = "";if (template != null)
{
string filePath =CheckEndBySlash(directoryOfOutput);
fileName
= filePath + fileNameOfOutput +fileExtension;if (!string.IsNullOrEmpty(filePath) && !Directory.Exists(filePath))
{
Directory.CreateDirectory(filePath);
}
//LogTextHelper.Debug(string.Format("Class file output path:{0}", fileName)); InitContext();using (StreamWriter writer = new StreamWriter(fileName, false))
{
template.Merge(context, writer);
}
}
returnfileName;
}

2)根据模板文件构造字符串内容

        /// <summary>
        ///根据模板输出字符串内容/// </summary>
        /// <param name="templateFile"></param>
        /// <returns></returns>
        public stringExecuteString()
{
InitContext();
System.IO.StringWriter writer
= newSystem.IO.StringWriter();
template.Merge(context, writer);
returnwriter.GetStringBuilder().ToString();
}

3)根据字符串内容构造字符串输出

        /// <summary>
        ///合并字符串的内容/// </summary>
        /// <returns></returns>
        public string ExecuteMergeString(stringinputString)
{
VelocityEngine templateEngine
= newVelocityEngine();
templateEngine.Init();

InitContext();

System.IO.StringWriter writer
= newSystem.IO.StringWriter();
templateEngine.Evaluate(context, writer,
"mystring", inputString);returnwriter.GetStringBuilder().ToString();
}

上面几种操作模板输出的方式,其调用代码如下所示。

        private void btnGenerateFile_Click(objectsender, EventArgs e)
{
string tempalte = "Template/template.htm";//相对目录 TestInfo info= newTestInfo();
info.Title
= "测试标题";
info.Content
= "测试内容,这是测试内容";
info.Datetime
=DateTime.Now;

NVelocityHelper adapter
= newNVelocityHelper(tempalte);
adapter.AddKeyValue(
"title", "This is a title")
.AddKeyValue(
"content", "This is a Content")
.AddKeyValue(
"datetime", System.DateTime.Now)
.AddKeyValue(
"TestInfo", info);

adapter.FileNameOfOutput
= "testTemplate";string filePath =adapter.ExecuteFile();if (!string.IsNullOrEmpty(filePath))
{
Process.Start(filePath);
}
}
        private void btnGenerate_Click(objectsender, EventArgs e)
{
string tempalte = "Template/template.htm";//相对目录 TestInfo info= newTestInfo();
info.Title
= "测试标题";
info.Content
= "测试内容,这是测试内容";
info.Datetime
=DateTime.Now;

NVelocityHelper adapter
= newNVelocityHelper(tempalte);
adapter.AddKeyValue(
"title", "This is a title")
.AddKeyValue(
"content", "This is a Content")
.AddKeyValue(
"datetime", System.DateTime.Now)
.AddKeyValue(
"TestInfo", info);this.txtCode.Text =adapter.ExecuteString();
}
        private void btnMergeString_Click(objectsender, EventArgs e)
{
System.Text.StringBuilder builder
= newSystem.Text.StringBuilder();
builder.Append(
"${Title}\r\n" + "$Content\r\n" + "$Digest\r\n" + "$Author\r\n" + "$Keyword\r\n" + "$DateTime\r\n");
NVelocityHelper adapter
= newNVelocityHelper();
adapter.AddKeyValue(
"Title", "标题").
AddKeyValue(
"Content", "内容").
AddKeyValue(
"Digest", "摘要").
AddKeyValue(
"Author", "作者").
AddKeyValue(
"Keyword", "关键词").
AddKeyValue(
"DateTime", DateTime.Now);this.txtCode.Text =adapter.ExecuteMergeString(builder.ToString());
}

2、模板引擎NVelocity的几种应用场景

上面的几种操作模板内容的方式,能够在绝大多数情况下满足我们的应用要求,如可以在代码生成工具里面,定义一些自定义的内容模板,然后结合数据库的元数据信息,实现丰富逻辑的代码生成操作。

也可以在一些内容管理的应用上(如文章管理方面),根据输入的内容,实现文章内容的文件生成操作,这个生成后,我们就直接使用文章的文件链接地址就可以了。

或者根据数据信息生成具体的页面,用于套打操作,如下是Winform里面的套打处理。

在开发.NET项目的时候,包括Winform项目和Web方面的项目,编译和部署的时候,都需要考虑到是32位的X86方式,还是64位的方式,有时候还需要进行调试,如果没有合理设置好这些关系,还可能出现无法调试的问题,本文基于我对Winform的开发总结,介绍在.NET项目中对项目工程的设置操作。

1、设置编译平台的属性

一般情况下,我们为了同时兼容32位和64位操作系统,我们需要对设置好.NET项目的编译属性,一般设置为X86的方式。在VS的IDE里面,选择生成-》配置管理器菜单。

如我的混合框架的项目设置如下所示,统一设置为了X86的兼容模式,请确保所有的项目均为X86的平台设置。

2、WCF服务或者Web项目的设置

项目设置了以上的编译平台属性后,一般情况下,WinForm程序或者类库运行是没什么问题了,但是如果是项目里面有WCF服务或者Web项目的,还需要注意一个设置,就是bin目录的设置。默认如果你修改了X86的平台设置属性后,WCF服务或者Web项目的输出目录也会发生变化,一般情况下,会是bin\x86\Debug\

这个时候这个项目也是能够顺利编译的,一切似乎都显得正常的,但是我们在修改部分代码,并进行调试问题的时候,可能会出现源代码和原始版本不同的情况。

如果这时候,你根据这些去百度或者Google问题,可能会给你找出很多不同的解决方案,但是可能都不太解决问题,其实这个时候的问题处理很简单,就是把输出目录重新设置为Bin就可以了,编译调试源码也一致了。

3、Winform启动项目的目标框架问题

在创建Winform启动项目的时候,默认VS创建的Winform启动项目,它的目标框架总是Client Profile(不知基于什么考虑),而一般情况,我们使用外部类库比较少的时候,或者调用的.NET自身的类库比少的情况下,问题不会出现,但是我们用这个项目开发了一段时间,逐渐加入一些模块的时候,发现总是出现错误,或者会缺少某些命名空间,这个时候,我们就应该要检查一下是否是你的启动项目的目标框架问题了,有可能是你的目标框架为Client Profile的模式,如下所示。

如果我们把它修改为正常的.NET Framework框架方式,那么编译就可能完全通过了。

4、.NET安装包的设置

为了方便客户部署使用,我们很多时候,可能把程序做成一个安装包的形式,这种模式也需要考虑32位和64位的差异,一般我们可以选择X86的平台兼容模式,这种情况下,需要我们在安装项目的属性里面设置X86的平台属性,如下所示。

这样我们生成的安装包,就能成功部署在32位和64位的系统上运行了(他们都使用了32位的程序模式运行)。程序安装后,我们可以看到它的目录也是在X86的程序目录下的。

我们制作安装包,一般情况下,我们可能需要先创建一个空白的项目,然后把相关的发布文件添加到项目中去,这里面其实也有一些讲究,一般情况下,我们先把主程序添加进去,其他部分会根据引用关系自动添加了。如果你一股脑把编译目录里面的DLL都加入进去,很多可能就会出现重复引用的情况。

因此避免以上的问题,一般先添加主程序的EXE文件,然后等它的相关DLL都已经自动引入了,最后检查缺少那些DLL和一些文档文件,然后再依次补充一些就可以了。

安装包除了上面情况需要注意外,还需要注意你的系统必备的设置

如果是.NET4的程序,我们一般勾选Microsoft .NET Framework4(X68和X64)这个必备组件(注意不是Client Profile),这样我们可以检测用户的系统是否安装了必备的环境,如果正常则可以成功运行起来了。

以上就是我在开发项目中,经常用到的一些设置属性,虽然很多看似很普通,但是每次必须确保已经成功设置好了,否则可能出现一些错误莫名其妙,会兜很多弯路。