2023年2月

在一些应用场景中,我们需要实现多层级的数据表格显示,如常规的二级主从表数据展示,甚至也有多个层级展示的需求,那么我们如何通过DevExpress的GridControl控表格件实现这种业务需求呢?本篇随笔基于这个需求,对二级、三级的主从表数据进行展示,从而揭开对多层级数据展示的神秘面纱。

1、二级主从表数据展示

主从表数据,我们知道,一个主表记录里面关联有多条明细从表记录,在数据定义的层次上我们体现如下所示。

先定义一个实体类信息作为载体。

    /// <summary>
    ///记录基础信息/// </summary>
    public classDetailInfo
{
publicDetailInfo()
{
this.ID =Guid.NewGuid().ToString();
}
/// <summary> ///ID标识/// </summary> public string ID { get; set; }/// <summary> ///名称/// </summary> public string Name { get; set; }/// <summary> ///描述信息/// </summary> public string Description { get; set; }

}

然后主从表的数据实体类就是除了包含这些信息外,再包含一个子列表(列表信息不一定是同一个实体类),如下所示。

    /// <summary>
    ///二级层次的列表/// </summary>
    public classDetail2Result : DetailInfo
{
public List<DetailInfo> Detail2List { get; set; }
}

这个是我们使用继承关系简化了信息的定义,就是这个实体类包含主表信息外,还包含一个列表集合,属于从表数据的。

有了这些数据的定义,我们构建一些测试的数据,如下所示。

            //创建测试数据
            var result = newDetail2Result()
{
Name
= "测试",
Description
= "描述内容",
Detail2List
= new List<DetailInfo>()
{
newDetailInfo()
{
Name
= "666666测试",
Description
= "666666描述内容"},newDetailInfo()
{
Name
= "222测试",
Description
= "222描述内容"},newDetailInfo()
{
Name
= "333测试",
Description
= "333描述内容"}
}
};
//构造一个记录的集合 var list = new List<Detail2Result>() { result };

这样我们就构建了一个主表从表记录的数据源,可以用于表格控件的绑定的了。

首先我们在界面上创建一个空白的窗体用于演示,并在窗体上增加一个GridControl控件用于展示主从表的数据,如下界面所示。

然后,我们可以通过代码创建我们需要的视图信息,如创建主表的GridView显示如下所示。

        /// <summary>
        ///创建第一个视图/// </summary>
        private voidCreateGridView()
{
var grv = this.gridView1;//创建从表显示的列 grv.Columns.Clear();
grv.CreateColumn(
"ID", "ID");//.Visible = false; grv.CreateColumn("Name", "名称");
grv.CreateColumn(
"Description", "描述内容");

grv.OptionsBehavior.ReadOnly
= false;
grv.OptionsBehavior.Editable
= true;
}

很简单,我们创建几个列,并指定它的Caption中文显示属性就可以了,然后我们接着还需要创建从表的GridView显示数据,这个是这篇随笔的关键。

具体的代码一次性贴出来,如下所示。

        GridView grv2 = null;/// <summary>
        ///创建第二个视图/// </summary>
        private voidCreateLevelView()
{
var grv = this.gridView1;var gridControl = this.gridControl1;//创建一个从表的GridView对象 grv2 = newGridView();
grv2.ViewCaption
= "记录明细";
grv2.Name
= "grv2";
grv2.GridControl
=gridControl;//构建GridLevelNode并添加到LevelTree集合里面 var node = newGridLevelNode();
node.LevelTemplate
=grv2;
node.RelationName
= "Detail2List";//这里对应集合的属性名称 gridControl.LevelTree.Nodes.AddRange(newGridLevelNode[]
{
node
});
//添加对应的视图集合显示 gridControl.ViewCollection.Clear();
gridControl.ViewCollection.AddRange(
newBaseView[] { grv, grv2 });//创建从表显示的列 grv2.Columns.Clear();
grv2.CreateColumn(
"ID", "ID");
grv2.CreateColumn(
"Name", "名称");
grv2.CreateColumn(
"Description", "描述内容");//设置非只读、可编辑 grv2.OptionsBehavior.ReadOnly = false;
grv2.OptionsBehavior.Editable
= true;
}

我们这里注意到 GridLevelNode 对象,它是我们主从表节点的关键信息,我们需要了解下面部分的代码

            //构建GridLevelNode并添加到LevelTree集合里面
            var node = newGridLevelNode();
node.LevelTemplate
=grv2;
node.RelationName
= "Detail2List";//这里对应集合的属性名称 gridControl.LevelTree.Nodes.AddRange(newGridLevelNode[]
{
node
});

首先是创建一个节点,然后指定它的 LevelTemplate 为我们新建的GridView,并且他的子集合对象名称为 Detail2List ,最后把这个节点的信息加入到 gridControl.LevelTree.Nodes 里面就可以了,其他的代码就和第一步差不多,指定显示的列和中文显示名称即可。

还有就是我们需要把创建的GridView 加入到指定的集合里面。

            //添加对应的视图集合显示
gridControl.ViewCollection.Clear();
gridControl.ViewCollection.AddRange(
new BaseView[] { grv, grv2 });

到这里基本上就是大功告成了,剩下的就是数据的绑定处理了。前面我们已经介绍了实体类的准备工作和创建测试数据的代码,那么我们这里沿用上面的代码进行数据的绑定就可以了。如下代码所示。

        /// <summary>
        ///绑定数据源/// </summary>
        private voidBindData()
{
//创建测试数据 var result = newDetail2Result()
{
Name
= "测试",
Description
= "描述内容",
Detail2List
= new List<DetailInfo>()
{
newDetailInfo()
{
Name
= "666666测试",
Description
= "666666描述内容"},newDetailInfo()
{
Name
= "222测试",
Description
= "222描述内容"},newDetailInfo()
{
Name
= "333测试",
Description
= "333描述内容"}
}
};
//构造一个记录的集合 var list = new List<Detail2Result>() { result };//绑定数据源 this.gridControl1.DataSource =list;
}
private void FrmTestDetails_Load(objectsender, EventArgs e)
{
BindData();
}

我们来运行下完成的程序界面,可以看到例子的效果界面如下所示。

我们可以看到数据记录是有树形节点的,展开就可以看到明细记录了,这个就是我们这里介绍的二级主从表数据的展示效果。

2、三级主从表数据展示

上面介绍了二级主从表的数据展示,其实GridControl可以用于展示三级以及更多层级的数据展示,只要你的数据设计合理,就可实现多层级的正确展示的。

本小节介绍三级的主从表数据展示,和二级数据展示类似,不过我们进一步实现了多层级的处理而已。

我们在二级层次的数据上定义了一个三级层次的数据实体类,如下所示。

    /// <summary>
    ///二级层次的列表/// </summary>
    public classDetail2Result : DetailInfo
{
public List<DetailInfo> Detail2List { get; set; }
}
/// <summary> ///三级层次的列表/// </summary> public classDetail3Result : DetailInfo
{
public List<Detail2Result> Detail3List { get; set; }
}

三级层次的测试数据初始化如下所示:

            //创建测试数据
            var result = newDetail3Result()
{
Name
= "测试11",
Description
= "描述内容11",//二级列表 Detail3List = new List<Detail2Result>()
{
newDetail2Result()
{
Name
= "测试22",
Description
= "描述内容22",//三级列表 Detail2List = new List<DetailInfo>()
{
newDetailInfo()
{
Name
= "31测试",
Description
= "31描述内容"},newDetailInfo()
{
Name
= "32测试",
Description
= "32描述内容"},newDetailInfo()
{
Name
= "33测试",
Description
= "33描述内容"}
}
}
}
};
//构造一个记录的集合 var list = new List<Detail3Result>() { result };

和二级层次的处理步骤类似,我们先创建主表的信息展示,如下所示。

        /// <summary>
        ///创建第一个视图/// </summary>
        private voidCreateGridView()
{
var grv = this.gridView1;var gridControl = this.gridControl1;//创建从表显示的列 grv.Columns.Clear();
grv.CreateColumn(
"ID", "ID");//.Visible = false; grv.CreateColumn("Name", "名称");
grv.CreateColumn(
"Description", "描述内容");

grv.OptionsBehavior.ReadOnly
= false;
grv.OptionsBehavior.Editable
= true;
}

然后着手创建二级、三级的列表信息展示,

        GridView grv2 = null;
GridView grv3
= null;/// <summary> ///创建第二个视图/// </summary> private voidCreateLevelView()
{
var grv = this.gridView1;var gridControl = this.gridControl1;//创建一个二级从表的GridView对象 grv2 = newGridView();
grv2.ViewCaption
= "记录明细";
grv2.Name
= "grv2";
grv2.GridControl
=gridControl;//创建一个三级从表的GridView对象 grv3 = newGridView();
grv3.ViewCaption
= "记录明细2";
grv3.Name
= "grv3";
grv3.GridControl
= gridControl;

这样我们相当于创建多两个(总共三个GridView对象)用于展示数据列表。

接着最为关键的是主从关系的节点,我们可以简单的理解他的Node节点和我们树形列表的Node处理方式类似即可。

            //构建GridLevelNode
            var topNode = newGridLevelNode();
topNode.LevelTemplate
= grv2; //这里是对应的视图 topNode.RelationName = "Detail3List"; //这里对应集合的属性名称//构建GridLevelNode var secondNode = newGridLevelNode();
secondNode.LevelTemplate
= grv3; //这里是对应的视图 secondNode.RelationName = "Detail2List";//这里对应集合的属性名称//需要添加节点的层级关系,类似Tree节点处理 topNode.Nodes.Add(secondNode);//最后添加节点到集合里面 gridControl.LevelTree.Nodes.Add(topNode);

通过定义两个GridLevelNode,然后指定他们的Node关系( topNode.Nodes.Add(secondNode) ),这样我们就可以很清晰的关联起来它们的节点关系了。

最后是把我们创建的几个视图加入到集合里面,并设定一些关系即可。

            //添加对应的视图集合显示
gridControl.ViewCollection.Clear();
gridControl.ViewCollection.AddRange(
newBaseView[] { grv, grv2, grv3 });//创建从表显示的列 grv2.Columns.Clear();
grv2.CreateColumn(
"ID", "ID");
grv2.CreateColumn(
"Name", "名称");
grv2.CreateColumn(
"Description", "描述内容");//创建从表显示的列 grv3.Columns.Clear();
grv3.CreateColumn(
"ID", "ID");
grv3.CreateColumn(
"Name", "名称");
grv3.CreateColumn(
"Description", "描述内容");//设置非只读、可编辑 grv2.OptionsBehavior.ReadOnly = false;
grv2.OptionsBehavior.Editable
= true;//设置非只读、可编辑 grv3.OptionsBehavior.ReadOnly = false;
grv3.OptionsBehavior.Editable
= true;

整个部分的代码如下所示。

        GridView grv2 = null;
GridView grv3
= null;/// <summary> ///创建第二个视图/// </summary> private voidCreateLevelView()
{
var grv = this.gridView1;var gridControl = this.gridControl1;//创建一个二级从表的GridView对象 grv2 = newGridView();
grv2.ViewCaption
= "记录明细";
grv2.Name
= "grv2";
grv2.GridControl
=gridControl;//创建一个三级从表的GridView对象 grv3 = newGridView();
grv3.ViewCaption
= "记录明细2";
grv3.Name
= "grv3";
grv3.GridControl
=gridControl;//构建GridLevelNode var topNode = newGridLevelNode();
topNode.LevelTemplate
= grv2; //这里是对应的视图 topNode.RelationName = "Detail3List"; //这里对应集合的属性名称//构建GridLevelNode var secondNode = newGridLevelNode();
secondNode.LevelTemplate
= grv3; //这里是对应的视图 secondNode.RelationName = "Detail2List";//这里对应集合的属性名称//需要添加节点的层级关系,类似Tree节点处理 topNode.Nodes.Add(secondNode);//最后添加节点到集合里面 gridControl.LevelTree.Nodes.Add(topNode);//添加对应的视图集合显示 gridControl.ViewCollection.Clear();
gridControl.ViewCollection.AddRange(
newBaseView[] { grv, grv2, grv3 });//创建从表显示的列 grv2.Columns.Clear();
grv2.CreateColumn(
"ID", "ID");
grv2.CreateColumn(
"Name", "名称");
grv2.CreateColumn(
"Description", "描述内容");//创建从表显示的列 grv3.Columns.Clear();
grv3.CreateColumn(
"ID", "ID");
grv3.CreateColumn(
"Name", "名称");
grv3.CreateColumn(
"Description", "描述内容");//设置非只读、可编辑 grv2.OptionsBehavior.ReadOnly = false;
grv2.OptionsBehavior.Editable
= true;//设置非只读、可编辑 grv3.OptionsBehavior.ReadOnly = false;
grv3.OptionsBehavior.Editable
= true;
}

也就是我们在窗体初始化的时候,创建它们的视图关系即可,如下代码所示。

    /// <summary>
    ///测试三级主从明细列表/// </summary>
    public partial classFrmTestDetails2 : BaseForm
{
publicFrmTestDetails2()
{
InitializeComponent();

CreateGridView();
CreateLevelView();
}

最后就是数据源的绑定操作了,这个利用前面介绍过的准备数据即可。

        private void FrmTestDetails2_Load(objectsender, EventArgs e)
{
BindData();
}
/// <summary> ///绑定数据源/// </summary> private voidBindData()
{
//创建测试数据 var result = newDetail3Result()
{
Name
= "测试11",
Description
= "描述内容11",//二级列表 Detail3List = new List<Detail2Result>()
{
newDetail2Result()
{
Name
= "测试22",
Description
= "描述内容22",//三级列表 Detail2List = new List<DetailInfo>()
{
newDetailInfo()
{
Name
= "31测试",
Description
= "31描述内容"},newDetailInfo()
{
Name
= "32测试",
Description
= "32描述内容"},newDetailInfo()
{
Name
= "33测试",
Description
= "33描述内容"}
}
}
}
};
//构造一个记录的集合 var list = new List<Detail3Result>() { result };//绑定数据源 this.gridControl1.DataSource =list;
}

以上就是三级层次的关系处理,如果我们理解了,其他更多层级的数据展示也是依照这个规则,增加节点和视图即可,原理一样。

案例的效果如下所示。

3、利用分页控件实现数据的展示

上面的两个案例是基于DevExpress的内置表格控件GridControl进行处理的,我们在Winform框架的开发过程中,往往为了效率和分页方便,一般都是使用分页控件来展示数据的,那么利用分页控件实现多层级的数据展示是如何的呢?

其实基本步骤也是差不多的,只是主表视图使用分页控件即可,如下所示。

    /// <summary>
    ///数据指定的主从表展示/// </summary>    
    public partial classFrmDictTypeMasterDetail : BaseDock
{
publicFrmDictTypeMasterDetail()
{
InitializeComponent();

InitDictItem();
this.winGridViewPager1.OnPageChanged += newEventHandler(winGridViewPager1_OnPageChanged);this.winGridViewPager1.OnStartExport += newEventHandler(winGridViewPager1_OnStartExport); this.winGridViewPager1.OnDeleteSelected += newEventHandler(winGridViewPager1_OnDeleteSelected);this.winGridViewPager1.OnRefresh += newEventHandler(winGridViewPager1_OnRefresh);this.winGridViewPager1.AppendedMenu = this.contextMenuStrip1;this.winGridViewPager1.ShowLineNumber = true;this.winGridViewPager1.BestFitColumnWith = false;//是否设置为自动调整宽度,false为不设置 this.winGridViewPager1.gridView1.DataSourceChanged += newEventHandler(gridView1_DataSourceChanged);this.winGridViewPager1.gridView1.CustomColumnDisplayText += newDevExpress.XtraGrid.Views.Base.CustomColumnDisplayTextEventHandler(gridView1_CustomColumnDisplayText);this.winGridViewPager1.gridView1.RowCellStyle += newDevExpress.XtraGrid.Views.Grid.RowCellStyleEventHandler(gridView1_RowCellStyle);CreateLevelView();
RegisterEvent();
}

GridView grv2
= null;private voidCreateLevelView()
{
var grv = this.winGridViewPager1.GridView1;var gridControl = this.winGridViewPager1.gridControl1;//创建一个从表的GridView对象 grv2 = newGridView();
grv2.ViewCaption
= "记录明细";
grv2.Name
= "grv2";
grv2.GridControl
=gridControl;//构建GridLevelNode并添加到LevelTree集合里面 var node = newGridLevelNode();
node.LevelTemplate
=grv2;
node.RelationName
= "Children";
gridControl.LevelTree.Nodes.AddRange(
newGridLevelNode[]
{
node
});

gridControl.ViewCollection.Clear();
gridControl.ViewCollection.AddRange(
newBaseView[] { grv, grv2 });//创建从表显示的列 grv2.Columns.Clear();
grv2.CreateColumn(
"ID", "ID").Visible =false; //标识行的关键字,可用于删除处理 grv2.CreateColumn("DictType_ID", "DictType_ID").Visible = false;//创建一个字段,隐藏的,存储记录 grv2.CreateColumn("Name", "项目名称");
grv2.CreateColumn(
"Value", "项目值");
grv2.CreateColumn(
"Seq", "排序");
grv2.CreateColumn(
"Remark", "备注");

grv2.OptionsBehavior.ReadOnly
= false;
grv2.OptionsBehavior.Editable
= true;
grv2.DataSourceChanged
+=grv2_DataSourceChanged;
}

以上就是基于GridControl实现数据的主从关系的处理,可以实现多层级的展示,希望这些案例能够对你展示数据有所帮助。

RabbitMQ是一个由erlang开发的AMQP(Advanved Message Queue)的开源实现,是实现消息队列应用的一个中间件,消息队列中间件是分布式系统中重要的组件,主要解决应用耦合,异步消息,流量削锋等问题。实现高性能,高可用,可伸缩和最终一致性架构。是大型分布式系统不可缺少的中间件。EasyNetQ则是基于官方.NET组件RabbitMQ.Client 的又一层封装,使用起来更加方便。本篇随笔主要大概介绍下RabbitMQ的基础知识和环境的准备,以及使用EasyNetQ的相关开发调用。

1、RabbitMQ基础知识

AMQP
,即Advanced Message Queuing Protocol,高级消息队列协议,是应用层协议的一个开放标准,为面向消息的
中间件
设计。消息中间件主要用于组件之间的解耦,消息的发送者无需知道消息使用者的存在,反之亦然。
AMQP的主要特征是面向消息、队列、路由(包括点对点和发布/订阅)、可靠性、安全。
RabbitMQ
是一个开源的AMQP实现,服务器端用Erlang语言编写,支持多种客户端,如:Python、Ruby、.NET、Java、JMS、C、PHP、ActionScript、XMPP、STOMP等,支持AJAX。用于在分布式系统中存储转发消息,在易用性、扩展性、高可用性等方面表现不俗。

RabbitMQ的特点

强大的
应用
程序消息传递;
使用方便;
运行在所有主要
操作系统上;支持大量开发人员平台;
开源

商业支持。消息队列的模式有两种模式:P2P(Point to Point),P2P模式包含三个角色:消息队列(Queue),发送者(Sender),接收者(Receiver)。每个消息都被发送到一个特定的队列,接收者从队列中获取消息。队列保留着消息,直到他们被消费或超时。Publish/Subscribe(Pub/Sub),包含三个角色主题(Topic),发布者(Publisher),订阅者(Subscriber) 。多个发布者将消息发送到Topic,系统将这些消息传递给多个订阅者。

EasyNetQ
的目标是提供一个使.NET中的RabbitMQ尽可能简单的库。在EasyNetQ中消息应由.NET类型表示,消息应通过其.NET类型进行路由。EasyNetQ按消息类型进行路由。发布消息时,EasyNetQ会检查其类型,并根据类型名称,命名空间和装配体给出一个路由密钥。在消费方面,用户订阅类型。订阅类型后,该类型的消息将路由到订户。默认情况下,EasyNetQ使用Newtonsoft.Json库将.NET类型序列化为JSON。
这具有消息是人类可读的优点,因此您可以使用RabbitMQ管理应用程序等工具来调试消息问题。

EasyNetQ是在RabbitMQ.Client库之上提供服务的组件集合。这些操作可以像序列化,错误处理,线程编组,连接管理等。它们由mini-IoC容器组成。您可以轻松地用自己的实现替换任何组件。因此,如果您希望XML序列化而不是内置的JSON,只需编写一个ISerializer的实现并将其注册到容器。
以下是官方提供的一个结构图,这个结构图可以很好的解析该组件的结构:

2、RabbitMQ的环境准备

本处主要介绍在Windows系统中安装RabbitMQ。

1. 下载安装erlang

下载地址
http://www.erlang.org/downloads
(根据操作系统选择32还64位)

2. 下载安装rabbitmq-server

下载地址
http://www.rabbitmq.com/install-windows.html

下载后获得两个安装文件,按照顺序安装即可

安装erlang环境后,一般会添加了一个ERLANG_HOME的系统变量,指向erlang的安装目录路径,如下所示(
一般都添加了,确认下

安装RabbitMQ后,在程序里面可以看到

我们使用它的命令行来启动RabbitMQ的服务

查看安装是否成功命令 :
rabbitmqctl status

安装成功,在浏览器中输入
http://127.0.0.1:15672/
,可以看到如下界面,使用默认的账号密码均为guest登陆进行管理

guest 账号是管理员账号,可以添加Exchanges,Queues,Admin。但我们一般不使用guest账号,可以选择用命令来添加账号和权限,也可以使用管理界面进行添加相应的内容。

例如我添加相应的用户账号

一般我们还需要添加虚拟机,默认的虚拟机为/,我这里添加了一个虚拟机myvhost。

然后绑定账号到虚拟机上即可。

3、EasyNetQ组件的使用

EasyNetQ组件的使用方式比较简单,跟很多组件都类似,例如:建立连接,进行操作做等等,对于EasyNetQ组件也是如此。

在.NET中使用EasyNetQ,要求至少基于 .NET4.5的框架基础上进行开发,可以直接在VS项目上使用NuGet的程序包进行添加EasyNetQ的引用。

一般添加引用后,至少包含了下面图示的几个引用DLL。

1)创建连接:

使用EasyNetQ连接RabbitMQ,是在应用程序启动时创建一个IBus对象,并且,在应用程序关闭时释放该对象。

RabbitMQ连接是基于IBus接口的,当IBus中的方法被调用,连接才会开启。创建一个IBus对象的方法如下:

var bus = RabbitHutch.CreateBus(“host=myServer;virtualHost=myVirtualHost;username=admin;password=123456”);

与RabbitMQ服务器的延迟连接由IBus接口表示,创建连接的方式连接字符串由格式为key = value的键/值对组成,每一个用分号(;)分隔。

  • host,host=localhost 或者host =192.168.1.102或者host=my.rabbitmq.com,如果用到集群配置的话,那么可以用逗号将服务地址隔开,例如host=a.com,b.com,c.com
  • virtualHost,虚拟主机,默认为'/'
  • username,用户登录名
  • password,用户登录密码
  • requestedHeartbeat,心跳设置,默认是10秒
  • prefetchcount,默认是50
  • pubisherConfirms,默认为false
  • persistentMessages,消息持久化,默认为true
  • product,产品名
  • platform,平台
  • timeout,默认为10秒

一般我们在代码里面测试的话,简化连接代码如下所示。

 //初始化bus对象
 bus = RabbitHutch.CreateBus("host=localhost");

2

关闭连接:

bus.Dispose();

要关闭连接,只需简单地处理总线,这将关闭EasyNetQ使用的连接,渠道,消费者和所有其他资源。

如果我们在Winform窗体里面初始化一个IBus对象,那么在窗体关闭的时候,关闭这个接口即可。

        private void FrmPublisher_FormClosed(objectsender, FormClosedEventArgs e)
{
//关闭IBus接口 if(bus != null)
{
bus.Dispose();
}
}

3

发布消息:

EasyNetQ支持最简单的消息模式是发布和订阅。发布消息后,任意消费者可以订阅该消息,也可以多个消费者订阅。并且不需要额外配置。首先,如上文中需要先创建一个IBus对象,然后,在创建一个可序列化的.NET对象。调用Publish方法即可。

var message = new MyMessage { Text = "Hello Rabbit"};
bus.Publish(message);

4

订阅消息:

EasyNetQ提供了消息订阅,当调用Subscribe方法时候,EasyNetQ会创建一个用于接收消息的队列,不过与消息发布不同的是,消息订阅增加了一个参数,subscribe_id.代码如下:

bus.Subscribe<MyMessage>("my_subscription_id", msg => Console.WriteLine(msg.Text));

第一个参数是订阅id,另外一个是delegate参数,用于处理接收到的消息。这里要注意的是,subscribe_id参数很重要,假如开发者用同一个subscribeid订阅了同一种消息类型两次或者多次,RabbitMQ会以轮训的方式给每个订阅的队列发送消息。接收到之后,其他队列就接收不到该消息。如果用不同的subscribeid订阅同一种消息类型,那么生成的每一个队列都会收到该消息。

需要注意的是,在收到消息处理消息时候,不要占用太多的时间,会影响消息的处理效率,所以,遇到占用长时间的处理方法,最好用异步处理。

为了测试发布和订阅消息,我们可以建立几个不同的项目来进行测试,如发布放在一个Winform项目,订阅放在一个Winform项目,另外一个项目放置共享的消息对象定义,如下所示。

定义消息对象类如下所示。

    /// <summary>
    ///定义的MQ消息类型/// </summary>
    public classTextMessage
{
public string Text { get; set; }
}

然后在发布消息的Winform项目上创建一个处理的窗体,并添加如下代码。

namespaceMyRabbitMQ.Publisher
{
/// <summary> ///测试RabbitMQ消息队列的发布/// </summary> public partial classFrmPublisher : DevExpress.XtraEditors.XtraForm
{
//构建一个IBus公用接口对象 private IBus bus = null;publicFrmPublisher()
{
InitializeComponent();
//初始化bus对象 bus = RabbitHutch.CreateBus("host=localhost");//对指定消息类型进行回应 bus.Respond<MyRequest, MyResponse>(request => new MyResponse { Text = "Responding to:"+request.Text});//收到消息后输出到控制台上显示 bus.Receive("my.queue", x =>x
.Add
<MyMessage>(message =>Console.WriteLine(message.ToJson()))
.Add
<MyOtherMessage>(message =>Console.WriteLine(message.ToJson())));
}

发布消息的处理代码,如下代码所示。

        private void btnSend_Click(objectsender, EventArgs e)
{
if (bus != null)
{
bus.Publish(
newTextMessage
{
Text
= this.txtContent.Text
});
}
}

然后在创建一个类似窗体,用来订阅消息的处理窗体,如下所示代码和窗体。

namespaceMyRabbitMQ.Subcriber
{
/// <summary> ///测试RabbitMQ消息队列的订阅/// </summary> public partial classFrmSubcriber : DevExpress.XtraEditors.XtraForm
{
//构建一个IBus公用接口对象 private IBus bus = null;publicFrmSubcriber()
{
InitializeComponent();
//初始化bus对象 bus = RabbitHutch.CreateBus("host=localhost");if(bus != null)
{
//订阅一个消息,并对接收到的消息进行处理,展示在控件上 bus.Subscribe<TextMessage>("test", (msg) =>{
StringBuilder sb
= newStringBuilder();
sb.AppendLine(msg.Text
+ "," +DateTime.Now.ToString());
sb.AppendLine(
this.txtContent.Text);this.txtContent.Invoke(new MethodInvoker(delegate()
{
this.txtContent.Text =sb.ToString();
}));
});
}
//使用消息发送接口发送消息 bus.Send("my.queue", new MyMessage { Text = "Hello Widgets!"});
bus.Send(
"my.queue", new MyOtherMessage { Text = "Hello wuhuacong!"});
}

发送请求获取响应的代码如下所示。

        private void btnRequest_Click(objectsender, EventArgs e)
{
//定义请求消息的对象 var request = newMyRequest()
{
Text
= string.Format("请求消息,{0}", DateTime.Now)
};
//异步获取请求消息的结果并进行处理,展示应答消息在窗体中的 var task = bus.RequestAsync<MyRequest, MyResponse>(request);
task.ContinueWith(response
=>{
StringBuilder sb
= newStringBuilder();
sb.AppendLine(response.Result.Text);
sb.AppendLine(
this.txtContent.Text);this.txtContent.Invoke(new MethodInvoker(delegate()
{
this.txtContent.Text =sb.ToString();
}));
});
}

两个项目联合进行测试如下界面所示。

发布者多次发送消息的情况下,订阅者中,会进行消息的轮训处理,也就是进行均匀分配。

5)消息发送(Send)和接收(Receive)

与Publish/Subscribe略有不同的是,Send/Receive 可以自己定义队列名称。

//发送端代码
bus.Send("my.queue", new MyMessage{ Text = "Hello Widgets!"});//接收端代码
bus.Receive<MyMessage>("my.queue", message => Console.WriteLine("MyMessage: {0}", message.Text));

并且,也可以在同一个队列上发送不同的消息类型,Receive方法可以这么写:

bus.Receive("my.queue", x =>x
.Add
<MyMessage>(message => deliveredMyMessage =message)
.Add
<MyOtherMessage>(message => deliveredMyOtherMessage = message));

如果消息到达队列,但是没有发现相应消息类型的处理时,EasyNetQ会发送一条消息到error队列,并且,带上一个异常信息:No handler found for message type <message type>。与Subscribe类型,如果在同一个队列,同一个消息类型,多次调用Receive方法时,消息会通过轮询的形式发送给每个Receive端。

6)远程过程调用:

var request = new TestRequestMessage {Text = "Hello from the client! "};
bus.Request<TestRequestMessage, TestResponseMessage>(request, response => 
    Console.WriteLine("Got response: '{0}'", response.Text));

7

RPC服务器:

bus.Respond<TestRequestMessage, TestResponseMessage>(request => 
    new TestResponseMessage{ Text = request.Text + " all done!" });

8

记录器:

var logger = new MyLogger() ;
var bus = RabbitHutch.CreateBus(“my connection string”, x => x.Register<IEasyNetQLogger>(_ => logger));

9

路由:

Publish方法,可以加一个topic参数。

bus.Publish(message, "X.A");

消息订阅方可以通过路由来过滤相应的消息。

* 匹配一个字符

#匹配0个或者多个字符

所以 X.A.2 会匹配到 "#", "X.#", "*.A.*" 但不会匹配 "X.B.*" 或者 "A". 当消息订阅需要用到topic时候,需要调用Subscribe的重载方法

bus.Subscribe("my_id", handlerOfXDotStar, x => x.WithTopic("X.*"));
bus.Subscribe(
"my_id", handlerOfStarDotB, x => x.WithTopic("*.B"));

上述这种方式,会将消息轮询发送给两个订阅者,如果只需要一个订阅者的话,可以这么调用:

bus.Subscribe("my_id", handler, x => x.WithTopic("X.*").WithTopic("*.B"));

RabbitMQ具有非常好的功能,
基于主题的路由
,允许订阅者基于多个标准过滤消息。*(星号)匹配一个字。#(哈希)匹配为零个或多个单词。

RabbitMQ的应用场景,一般在快速处理订单,以及异步的多任务处理中可以得到很好的体现,下面是几个应用场景。

邮件和短消息的处理

订单的解耦处理

RabbitMQ的服务器架构

3、RabbitMQ查询状态出现错误的处理

安装成功之后使用rabbitmqctl status命令之后出现如下错误。

Status of node rabbit@WUHUACONG ...Error: unable to perform an operationon node 'rabbit@WUHUACONG'. Please see diagnostics information and suggestions below.Most common reasonsforthis are:

* Target node is unreachable
(e.g. due to hostname resolution, TCP connection or firewall issues)* CLI tool fails to authenticate with the server(e.g. due to CLI tool's Erlang cookie not matching that of the server)* Target node isnotrunning

In addition to the diagnostics info below:

* See the CLI
, clustering and networking guides on http://rabbitmq.com/documentation.html to learn more
* Consult server logs
onnode rabbit@WUHUACONG

DIAGNOSTICS
===========attempted to contact: [rabbit@WUHUACONG]

rabbit@WUHUACONG:
* connected to epmd
(port 4369) onWUHUACONG
* epmd reports node 'rabbit' uses port
25672 forinter-node and CLI tool traffic
* TCP connection succeeded but Erlang distribution failed

* Authentication failed
(rejected by the remote node),please check the Erlang cookie


Current node details:
* node name: rabbitmqcli100@WUHUACONG
* effective user's home directory: C:
\Users\Administrator
* Erlang cookie hash: RgaUM2coc
+rxIhJrfLS7Jw==

这个问题出现比较常见,主要原因是两个目录的.erlang.cookie文件内容不一样。

要确保.erlang.cookie文件的一致性,不知道什么原因导致了C:\Users\{UserName}\.erlang.cookie和默认情况下C:\WINDOWS\System32\config\systemprofile\.erlang.cookie不一致了,将Windows目录下的拷贝到用户目录下就可以了。

反正无论如何,两个地址的Cookie内容一致就可以了,然后重启下RabbitMQ服务器即可正常运行,并可以正常获取它的状态信息。

在国际化环境下,越来越多的程序需要做多语言版本,以适应各种业务需求的变化。在Winform应用程序中实现多语言也有常规的处理方式处理,不过需要针对每个语言版本,重新修改Winform界面的显示,对一些常规的辅助类,也需要引入一个统一的资源管理类来处理多语言的问题,相对比较繁琐。本篇随笔针对多语言的需求,希望尽量避免繁琐的操作,既能符合本地语种开发人员的开发习惯,又能快速实现Winform程序的多语言场景处理。

1、多语言开发的困惑和思路

在常规的多语言版本程序中,开发总是伴随着很多不愉快的事情,大概列举一些仅供参考:

1)对窗体的多语言处理时,维护多个语言版本的界面非常繁琐;

2)多语言处理的时候,以资源参照的时候,默认键值为一些英文字符串或者单词,不太符合如中文语境的开发,调整代码则需要很多工作量;

3)对于已开发好的程序,全面引入多语言的处理代码,需要大量修改;

4)对于大量中文的多语言处理,工作量望而却步;

5)对于常规Resx文件的处理觉得繁琐

6)缺乏一个统一处理多语言需求的方案

在多语言的处理上,我一直希望找出一种高效的处理方式,由于我的Winform开发框架中很多模块是现成的,希望能够使用继承处理的方式,实现最简化的处理;

同时大量中文的英文(针对英文版本)翻译也是一个头痛的事情,突然想到百度的翻译API接口可以利用,那么我们可以利用翻译接口实现开始的翻译,然后对资源进行一定的调整则可以提高效率和准确率。

对于编辑和承载多语言的信息,我一直觉得JSON格式挺好的,可以利用它序列化为字典集合,通过字典获取对应键值的多语言版本字符串也是很高效的一种方式,那么就决定用JSON来存储多语言信息了,易读好用。

对于多余的处理逻辑,尽量封装为独立的模块,可以在多个模块中进行调用处理。

2、多语言的处理实现

在思考多语言的合理处理方案过程中,参考了另一位博友的文章《
分享两种实现Winform程序的多语言支持的解决方案
》,思路有点符合我的期望,因此吸收了一些处理思想进行处理,目的就是提高开发效率。

1)多语言的信息存储和加载

首先,我们来看看多语言处理的目录和格式问题,目录大概是根据多语言的简称进行放置,如下所示。

这个目录就是会输出到debug或者Release的运行目录中,我们就是根据相对于运行目录进行资源读取即可,所有模块共用同一的多语言文件,我们可以把各个模块基础通用的多语言文件放在Basic.json文件中,也可以根据模块独立起名,主程序如TestMultiLanguage的多语言文件我则放在TestMultiLanguage.json文件中。实际上目录名称是为了区分而已,程序加载的时候,会把目录下面所有的JSON文件进行加载,读取里面的键值作为资源的字典参照。

多语言的JSON文件是标准的Json格式,只是我们只用键值的字典参考即可,不需要使用复杂的JSON对象格式,如下是basic.json文件的部分内容。

这些资源文件采用
中文-英文
的参照方式,我们以我们常规的母语开发,即使我们不做多语言,也不影响代码的正常处理,我们只需要把窗体上和代码里面的中文提取出来,然后进行多语言处理(如变为英文)即可。

由于我们使用键值字典对象的JSON内容,那么我们就可以把这些内容序列号为字典集合,如下代码我们可以通过 JSON.NET 组件把它们序列化为字典集合,这些字典集合就是我们用来做多语言的关键。

            var content =File.ReadAllText(file, Encoding.UTF8);if (!string.IsNullOrEmpty(content))
{
var dict = JsonConvert.DeserializeObject<Dictionary<string, string>>(content);foreach (string key indict.Keys)
{
//遍历集合如果语言资源键值不存在,则创建,否则更新 if (!resources.ContainsKey(key))
{
resources.Add(key, dict[key]);
}
else{
resources[key]
=dict[key];
}
}
}

加载多语言处理的时候,我们遍历相对目录下的lang/***里面的文件即可实现多语言信息的加载,如下代码所示。

        /// <summary>
        ///根据语言初始化信息。///加载对应语言的JSON信息,把翻译信息存储在全属性resources里面。/// </summary>
        /// <param name="language">默认的语言类型,如zh-Hans,en-US等</param>
        private void LoadLanguage(string language = "")
{
if (string.IsNullOrEmpty(language))
{
language
=System.Threading.Thread.CurrentThread.CurrentUICulture.Name;
}
this.resources = new Dictionary<string, string>();string dir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, string.Format("lang/{0}", language));if(Directory.Exists(dir))
{
var jsonFiles = Directory.GetFiles(dir, "*.json", SearchOption.AllDirectories);foreach (string file injsonFiles)
{
LoadFile(file);
}
}
}

我们把多语言的加载和翻译处理,放在一个独立的项目上,如我定义为框架的一个模块:WHC.Framework.Language

这样我们在各个模块中使用多语言处理过程的时候,包含这个模块就可以了。

2)多语言信息的翻译

做多语言的版本程序,翻译工作也是一个繁琐的工作,如果你是非常精通各种语言(如中文、英文、日文等等),那当然不在话下,不过我们做开发的多少也是会一些的,如英语吧,即时不能非常准确,那么也可以做到差不多,但是做这个还是累,还容易敲打错别字,那么用第三方提供的翻译API来预处理后调整,结果就简化很多了,可以极大提高效率的。

这里以我们经常使用的百度翻译来实现(用Google翻译也可以,增加接口实现即可)

百度翻译接口的使用,你先注册一个开发账户,获得相应的秘钥信息就可以使用免费的翻译接口了(
http://api.fanyi.baidu.com/api/trans/product/index
)。

有了这些准备后,就可以利用C#代码进行翻译处理了。

百度翻译的接口处理代码如下所示。

        /// <summary>
        ///百度接口翻译/// </summary>
        /// <param name="inputString">输入字符串</param>
        /// <param name="from">源内容语言</param>
        /// <param name="to">目标语言</param>
        /// <returns></returns>
        private static string BaiduTranslate(string inputString, string from = "zh", string to = "en")
{
string content = "";string appId = "你的APPID";string securityId = "你的秘钥";int salt = 0;

StringBuilder signString
= newStringBuilder();string md5Result = string.Empty;//1.拼接字符,为了生成sign signString.Append(appId);
signString.Append(inputString);
signString.Append(salt);
signString.Append(securityId);
//2.通过md5获取sign byte[] sourceMd5Byte =Encoding.UTF8.GetBytes(signString.ToString());
MD5 md5
= newMD5CryptoServiceProvider();byte[] destMd5Byte =md5.ComputeHash(sourceMd5Byte);
md5Result
= BitConverter.ToString(destMd5Byte).Replace("-", "");
md5Result
=md5Result.ToLower();try{//3.获取web翻译的json结果 WebClient client = newWebClient();string url = string.Format("http://api.fanyi.baidu.com/api/trans/vip/translate?q={0}&from=zh&to=en&appid={1}&salt={2}&sign={3}", inputString, appId, salt, md5Result);byte[] buffer =client.DownloadData(url);string result =Encoding.UTF8.GetString(buffer);var trans = JsonConvert.DeserializeObject<TranslationJson>(result);if (trans != null)
{
content
= trans.trans_result[0].dst;
content
=StringUtil.ToProperCase(content);
}
}
catch(Exception ex)
{
Debug.WriteLine(ex);
}
returncontent;
}

其中把JSON转换为类对象需要两个类,对翻译结果进行转换,如下代码所示。

    internal classTranslationJson
{
public string from { get; set; }public string to { get; set; }public List<TranslationResult> trans_result { get; set; }
}
internal classTranslationResult
{
public string src { get; set; }public string dst { get; set; }
}

这样我们在多语言处理的时候,可以对默认输入为空的键值进行翻译即可(如英文翻译)。

    //遍历集合进行翻译
    var value =dict[key];if (string.IsNullOrWhiteSpace(value))
{
//如果值为空,那么调用翻译接口处理 var newValue = TranslationHelper.Translate(key, from, to);if (!string.IsNullOrWhiteSpace(newValue))
{
dict[key]
=newValue;
}
}

然后重新更新我们的资源文件就可以了

    //不排序
    var newContent =JsonConvert.SerializeObject(dict, Formatting.Indented);

File.WriteAllText(file, newContent, Encoding.UTF8);

如果需要对键值进行排序,那么使用SortDictionary进行包装下即可

    //进行排序
    SortedDictionary<string, string> sortedDict = new SortedDictionary<string, string>(dict);var newContent = JsonConvert.SerializeObject(sortedDict, Formatting.Indented);

在多语言处理的时候,我们一般不必要一次填写完毕中英文对照的资源,我们可以先把字典键值的键写出来,值保留为空,如下文件所示。

运行程序的时候,让翻译的接口先行翻译,然后我们再对翻译的资源进行调整,适应我们程序的语境即可,翻译后的内容后如下所示。

好了,弹药都准备好了,就看我们如何使用, 下一步介绍如何使用这些资源。

3、多语言在界面中的应用

前面介绍都是为程序界面准备好对应的多语言资源内容,我们在程序启动的时候,可以通过常规的方式,设置界面的CurrentUICulture区域信息,如下代码所示。

            //界面多语言//System.Threading.Thread.CurrentThread.CurrentUICulture = new System.Globalization.CultureInfo("zh-Hans");//中文界面
            System.Threading.Thread.CurrentThread.CurrentUICulture = new System.Globalization.CultureInfo("en-US");//英文界面

然后我们在Winform程序中开发设计我们的界面内容,例如设计一个普通的界面如下所示。

这个窗体我们添加了几个按钮,并设置它的中文显示内容,它的基类默认还是保持它的DevExpress基类 XtraForm,如下所示。

    /// <summary>
    ///测试多语言的窗体界面/// </summary>
    public partial class Form1 : XtraForm

那么我们如果要自动实现多语言的处理,那么还需要在窗体的Load或者Shown事件里面实现处理,如下代码所示。

        private void Form1_Shown(objectsender, EventArgs e)
{
//窗体加载并显示后,对窗体实现多语言处理 if (!this.DesignMode)
{
LanguageHelper.InitLanguage(
this);
}
}

如果我们为每个窗体都需要添加这些代码,也是繁琐的事情,那么我们可以把这个处理逻辑,放到我们常规自定义的窗体基类里面(如BaseForm),那么我们就不需要任何额外的代码了。

所需的就是集成窗体基类即可,这也是我们一般开发都做的事情,通过继承使得我们的代码又省去了。

    /// <summary>
    ///测试多语言的窗体界面/// </summary>
    public partial class Form1 : BaseForm

那么我们真正关注的就是我们前面介绍的逻辑代码实现了

LanguageHelper.InitLanguage(this);

这个辅助类,主要就是在窗体初始化后,遍历界面的所有类型控件,对控件进行相应的多语言处理。

    /// <summary>
    ///对界面控件进行多语言的处理辅助类/// </summary>
    public classLanguageHelper
{
/// <summary> ///初始化语言/// </summary> public static voidInitLanguage(Control control)
{
//如果没有资源,那么不必遍历控件,提高速度 if (!JsonLanguage.Default.HasResource)return;//使用递归的方式对控件及其子控件进行处理 SetControlLanguage(control);foreach (Control ctrl incontrol.Controls)
{
InitLanguage(ctrl);
}
//工具栏或者菜单动态构建窗体或者控件的时候,重新对子控件进行处理 control.ControlAdded += (sender, e) =>{
InitLanguage(e.Control);
};
}

通过递归的方式,我们可以对常规的如GridControl,工具栏、NavBar导航栏、菜单、按钮等资源进行统一的多语言处理,而这里面对于我们开发应用程序界面,都不需要额外的担心,极大的提高了效率。

下面是几个常规的界面,我们来体验下英文版本的界面效果。

这些英文界面我们只需要把界面的中文提取出来放到JSON文件中,自动翻译再调整即可,然后界面继承保持BaseForm或者BaseDock这些窗体基类不变,只是调整了这些基类的加载,增加一行代码就可以顺利实现了多语言的效果了。

这样我们就把核心的工作放在提取界面中的中文资源并进行整理即可,这是核心的工作但翻译也基本不用自己从头做,窗体代码几乎不需要做其他修改就实现了我们所需要的多语言效果了,这样做极大提高了开发效率,对于我们已经开发好的模块,更是四两拨千斤了。

我在上篇随笔《
在WinForm应用程序中快速实现多语言的处理
》里面介绍了Winform开发中多语言的处理解决方案,整个多语言解决方案是以实际需求为驱动,以减少代码改动,高效处理为目的,通过基类继承的方式减少代码修改,通过引入翻译API方式减少翻译处理时间,本随笔继续深化这个多语言处理方案的介绍,是指整合在开发框架中进行无缝的使用。

1、多语言的处理步骤

1)提取多语言处理JSON文件

我们通过把程序界面或者代码里面的中文提取出来,放到JSON文件中,对不同模块可以分为不同的JSON文件,如下所示。

然后使用辅助类的接口调用实现英文内容的翻译,如下代码所示:

            //界面汉化System.Threading.Thread.CurrentThread.CurrentUICulture= new System.Globalization.CultureInfo("en-US");//英文界面//如果语言包有空白的翻译内容,可以调用下面语句(百度翻译)获得翻译内容,减少手工翻译的繁琐//翻译后将内容进行审核或调整一下即可使用//正式发布程序需要屏蔽这个调用JsonLanguage.Default.Translate();

可以把内容翻译并调整好。

这些JSON资源文件,按照程序运行的相对目录
Lang/en-US/ABC.json
目录放置即可,可以根据不同的模块或者需求分开不同的文件,程序加载多语言信息的时候,会全部进行加载。

2)继承窗体基类和使用统一的信息提示辅助类

准备好多语言文件后,那么多语言的处理还需要一些辅助的处理才能实现,由于我们已经把多语言处理的逻辑放在了窗体的基类,如BaseForm窗体基类里面,如果原来窗体就是继承了这个基类,那么多语言处理逻辑会在窗体加载后自动进行处理的了。

通过继承关系的处理,我们可以不用修改子窗体任何代码就可以自动具有多语言的处理过程了,子窗体在加载完毕后,自动遍历内部控件实现多语言的处理逻辑。

如果我们一些窗体,不方便集成这些基类,那么也可以使用增加代码的方式进行处理即可,在窗体的Load或者Shown事件里面实现处理,如下代码所示。

        private void Form1_Shown(objectsender, EventArgs e)
{
//窗体加载并显示后,对窗体实现多语言处理 if (!this.DesignMode)
{
LanguageHelper.InitLanguage(
this);
}
}

还有一种特殊的界面,就是一些信息的提示,如消息框的提示,这种默认的处理方式,如下面是一般的消息提示代码。

XtraMessageBox.Show("欢迎使用Winform开发框架", "提示信息", MessageBoxButtons.OK, MessageBoxIcon.Information);

我们在框架里面也是通过辅助类封装的方式进行提示的,因此多语言也只需要修改这个辅助类就可以了,不用修改我们窗体里面已完成的代码。

MessageDxUtil.ShowTips("欢迎使用Winform开发框架");

由于我们在框架里面统一使用消息提示处理,那么多语言的处理,在其内部实现即可,如下所示。

        /// <summary>
        ///显示一般的提示信息/// </summary>
        /// <param name="message">提示信息</param>
        /// <param name="args">字符串里面的参数内容</param>
        /// <returns></returns>
        public static DialogResult ShowTips(string message, params object[] args)
{
//对消息的内容进行多语言处理 message = JsonLanguage.Default.GetString(message);if (args != null)
{
message
= string.Format(message, args);
}
returnDevExpress.XtraEditors.XtraMessageBox.Show(message, Caption_Tips, MessageBoxButtons.OK, MessageBoxIcon.Information);
}

如果是带有一些参数占位符的多语言处理,我们一样可以实现它。

MessageDxUtil.ShowTips("用户【{0}】身份验证正确", loginName);

通过窗体基类BaseForm内部逻辑的修改,以及一些辅助类如MessageDxUtil的修改,我们悄悄的就实现了多语言的处理逻辑,原来的代码还是不用修改就支持了多语言的处理,大大节省了工作量。

当然如果是一些特殊的情况,我们还是可以通过统一的多语言处理逻辑来获得它的内容,如下代码所示。

JsonLanguage.Default.GetString(message);

3、模块化的多语言支持

除了上面介绍的一些总体逻辑,我们在Winform开发过程中,往往也是以模块化的开发方式来提高开发效率,如利用框架的公用类库、基础界面模块、分页控件模块、字典模块、权限管理系统等等,这些模块都是整个模块化过程中的一部分,当然多语言的支持也是很必要,由于我们已经通过基类和多余与模块独立的方式来简化它,但是里面的多语言资源我们也应该以模块方式提供,翻译好并集成在系统中使用即可。

上面的Basic.json是框架整个基础的界面(如分页、字典等)英文参照资源,Security.json为权限系统的界面资源,Commons.json为一些其他模块的多语言资源等等 。

以及权限系统的多语言参照资源

例如分页控件的展示界面,我们的英文版效果如下所示(以用户管理界面为例)

编辑界面效果如下所示

权限管理系统整体的界面效果如下所示,包括工具栏、菜单、选项卡,以及里面的所有控件,测试均表现OK。

当然,里面的数据源肯定还是中文的,除非我们系统的数据全部以英文语境进行录入。

在之前版本的代码生成工具Database2Sharp中,由于代码生成都是考虑Winform和Web通用的目的,因此Winform界面或者Web界面都是单独生成的,在工具中生成相应的界面后,复制到项目里面去使用即可。由于我们Winform的界面是模块化的方式独立开发,因此对某些表来说,我们可以独立开发模块,那么它的Winform窗体界面也应该是在独立的项目上,开发完毕后,我们在主体框架里面通过菜单动态配置即可加入到系统中使用的。

1、框架插件化回顾

在Winform开发框架的程序中,实现基于插件化的应用,如下所示。

通过在权限系统中的预先的配置,独立模块的窗体可以进行动态加载解析,就能实现插件模块的热插拔功能了。实际插件化框架的菜单配置界面效果如下所示。

下图是菜单管理里面的 “Winform窗体类型” 信息的具体内容。

通过动态的配置,可以实现模块的独立开发,动态整合的插件化特性,非常适合我们企业的分工开发,从而提高开发效率。

2、快速生成Winform框架的界面项目

前面我们介绍了,通过模块化的独立来发,动态整合,可以提高开发效率。

其实就是我们对大型项目的横向切割,把大型项目分为各个独立的模块,给不同的开发人员或者团队进行开发,可以极大提高效率,也减少对某个人的过度依赖,也同时可以极大提高开发效率。

在实际使用中,一些客户就提出其实模块化的Winform界面项目可以使用代码生成工具一键生成,和其他模块一样,选定数据库后生成项目一并生成,这样可以更加人性化,减少创建空白界面项目,然后引入所需DLL和文件引用等操作0所花费的时间。

如一般我们创建一个基于DevExpress的空白项目过程是这样的。

先准备创建一个基于DevExpress的模板项目

接着就是选择一个空白的DevExpress项目,这样可以在创建项目的时候,包含一些基础的DevExpress应用,减少我们后续引用DLL的工作。

然后就可以看到生成空白的窗体项目如下所示。

这样离我们开发独立的模块界面项目,还是有很多工作要做,如还需要引入额外的DevExpress所需的DLL,引入框架支持的相关DLL,另外还有一些业务逻辑层和数据访问层等类文件需要包含进去(这么做是减少文件太多的依赖,一个UIDx程序集或者ClientDx程序集就可以了)。

如果我们能够在生成其他项目工程的时候,一并生成该模块的UI项目就可以减少这部分的时间花费,要知道我们开发人员的时间也是很宝贵的,只应该花在有意义,有深度的地方。

如对于普通的Winform界面项目,我们生成后的项目结构视图如下所示。

而对于混合框架方案的项目,我们生成的界面项目结构视图如下所示。

这样就是我们最终要做的视图,一键就可以快速生成了,包含了相关的DLL应用,也包含了一些所需的文件应用,省却了很多繁琐的处理过程。

下面我们可以回顾下两种不同框架的架构分层图示。

普通Winform框架的分层架构图。

我们在Winform开发框架、混合式开发框架、Web开发框架等各种开发框架,底层都是支持多种数据库的,如MS SQLServer、Oracle、MySQL、SQLite、PostgreSQL等等,如下图所示。

混合框架的多种方式支持

混合框架的分层架构图