这里先给出我已经实现的工具CSharpU8,这个小工具就是解决Interop生成和OCX引用问题的,不愿意看文章,可以直接去用结果https://github.com/zzlufida/CSharpU8
我们知道,U8的大部分功能使用VB6语言开发。所以整个U8功能都是基于COM组件技术实现的。对于使用VB6语言开发来说,没有什么问题。我们可以直接使用CreateObject(“类名”),动态创建我们需要的组件,然后直接调用其方法。
'创建登录对象
dim c=CrateObject("U8Login.clsLogin")
'调用登录方法
c.login()
这样的开发有点类似脚本语言,非常灵活。但有一个问题,那就是不能使用上IDE提供的类型安全和友好提示,并且如果你没有源代码的情况下,非常难知道类名对应的函数等等。
如果想使用IDE的友好提示,在VB6中,我们一般会引用需要的COM组件,然后ctrl+j的时候就能自动出来函数方法了。
为了能够使用代码提示功能,且能够使用C#开发。就必须使用interop技术,技术本身这里先不多解释,就通俗的描述一下:
简单说,为了能够使用NET调用COM组件,必须引用一些DLL文件,这些文件一般都放在C:\U8Soft\Interop下面。
这些文件一般都是以Interop单词加一个点开头的文件,或者AxInterop开头的。
例如:Interop.U8Login.dll,这个文件就是COM版本的U8登录组件。
这些文件中包含了所有COM组件提供的功能类和函数,并准确的给出了具体参数规则。和普通的NET类库是完全一样的。引用之后可以直接new这些类对象,并调用对应的函数直接使用。
那么这些Interop文件又是怎么来的呢?当然不是手写出来,他是通过命令行工具得到的,或者你在VS中引用COM组件后,VS会自动帮你做出来这些文件。具体的操作可以百度,这里不展开。
Interop文件如果进行反编译,你会发现她就是NET语言开发的,只是没有具体的实现代码,函数体都是空的,并且都是一些类的声明,其中每个接口类上面,会有一些GUID的属性。这些其实就是COM组件的编码了,或者咱们叫唯一Id,主键等等啦。window就是通过这个id,然后到注册表这个大数据库里面去select一下,看看这个Id对应的dll文件到底在哪里,然后自动根据注册表的文件地址,去初始化我们的类。
所以说Interop就是一个提供类型安全和GUID对应的类库文件。有了类型安全,就有了IDE自动提示,有了GUID就能让windows找到COM,微软提供的这个技术还是蛮不错的,但是也存在着不完美的问题。
如果你使用百度提供的方法去生成Interop文件后,你会发现一个很奇怪的事情,就是本来我想生成一个COM组件的Interop文件,结果生成出来的Interop文件好多个。
比如我想自己生成一个U8login.dll的Interop文件,结果搞出来一看,居然还有Interop.msxml2.dll和adodb.dll文件。那么这是怎么回事呢?
其实这里需要换一个角度理解问题。因为在NET里面,类库都是包装在DLL里面的,所以一个DLL就提供一套类库功能,比如log4net.dll,就提供日志功能。
以此类推,每一个COM组件,微软都认为应该有一个独立的DLL文件,所以U8login.dll封装以后,就会理所应当的变成Interop.U8Login.dll,其中也只包含了Login组件的类和函数。
可是,当COM组件引用了其他的COM组件,或者说,如果U8login中某个函数的返回值或者参数居然是另一个COM组件时怎么办呢?比如ado组件中的cn和rs都是我们最常用的参数类型。如果U8Login的某个函数需要返回cn。这时候cn对应的类功能,应该放在哪个文件里面呢?
微软在这个时候就显示出来他的可怕智慧,他就会非常智能(傻逼)地给你把需要的COM组件,逐一,自动地生成对应的Interop文件,然后让Interop.U8Login.dll去引用这些Interop.XXX.dll。注意这句话里面的引用这个动词。
比如,我们先Interop一下U8login.dll,然后得到Interop.U8Login.dll文件,随后需要Interop另个一COM组件,假设是VoucherCO_SA.dll,这是一个销售模块最核心的保存类组件,API就是直接调用他去保存单据的。当我们把他Interop一下后,会发现,居然又是一大堆Interop文件,其中可能还有Interop.U8Login.dll!这个时候就非常尴尬了,因为从引用的角度考虑,我们希望自己的Interop新文件最好去引用之前已经生成好的劳动成果,而不是每次都拖家带口的搞出来一大家子DLL。
如果我们翻看C:\U8soft\interop文件夹下的文件,你会发现,其中的Interop.U8Login.dll是有强命名的,而密匙文件snk也没有给咱们,同时目录下面还有一个叫做ADODB.dll的文件,居然也有强命名。
连带着问题一,就会发现,如果我们去生成VoucherCo_SA.dll,由于拖家带口,此时你就会得到两个Interop.U8Login.dll文件,一个是没有强命名的微软自动生出来的,一个是总部标准提供的,而Interop.VoucherCo_SA.dll引用的,是无强命名的那个文件。
这时候,就会发现整个环境已经混乱了,我们进入了Dll陷阱中,即同名文件,同类库,不同强命名。
如果你使用的是WIN7系列的操作系统,那么恭喜你了,在Interop生成这个环节,你会非常非常郁闷,因为只要遇到引用了ADODB.dll的COM组件,你根本Interop不出来文件,包括Window2008系列的系统。
核心原因是ADODB这个组件在win7时代被改过,具体这里不展开说了,简单说,在这种系统下编译VB6组件程序,不能在XP下使用,InteropADODB相关组件,会直接失败。加之拖家带口问题,你基本就没有几个COM组件能搞出来了。
这个问题在Win2003下和Win10下都不存在,微软后来吸取教训修改了Win10系统中的ADODB组件。所以如果开发的话,必须准备两个环境,一个Win10虚拟机,一个Win2008R2虚拟机,我一般使用08R2虚拟机进行程序开发,Win10就测试发版和COMInterop用。
这是我最近发现的一个特别牛逼的软件,主要用来HOOK程序,实时分析程序调用了那些api,读取了那些文件,用到了那些COM组件,读取了那些注册表。特别适合进行软件绿化,因为它能直接导出vmware thinapp,简直是神一样的存在,可惜不能在Win10下正常使用。但是绿化后的程序可以在各种系统下运行。
用于将多个NET的DLL组件合并到一起,本来是一个命令行工具,后来我找到一个GUI的小程序,可以托拉拽了,特别强大和方便。
这也是一个杀手级别的利器,主要用来将NET反编译成IL文件,然后修改IL后,再次编译成DLL。我主要用它解决引用错误问题。因为Interop文件不能很完美反编译成C#语言,所以很多工作我都是在IL级别操作的。
因为后期主要是对各种文件的操作,所以最好准备一个TC工具,可以非常方便的管理文件。对于这个软件可以说也是神级别的了,我就不多说了。自己百度吧。
准确的说,这不是一个工具了,而是一个NET的类库,由Mono提供,可以修改Net程序的DLL文件,功能比DotHelper还要强大。因为需要对Interop文件进行批量的处理,而IL操作实在是太麻烦了,所以直接用这个类库,通过编程的方式,把我想修改的内容写成代码,然后执行,全部搞定!!
以上的5个工具,除了SpyStudio暂时用不到,其他的工具,是下面将会用到的主要工具,所以希望大家提前去百度学习一下如何使用,这里不展开。毕竟也不是非常复杂,过程中我会尽量截图,让大家看的清除一些。
有了工具,我们就可以分析Interop文件了,首先拿出DotNetHelper,把Interop.U8Login.dll反编译后,看一下IL文件的开始部分信息。
文件的内容很多,但是我们主要关注开头部分,因为开头部分是引用部分,相当于我们开发C#时,引用程序集那个部分的功能,具体的格式如下:
// Metadata version: v2.0.50727
.assembly extern mscorlib
{
.publickeytoken = (B7 7A 5C 56 19 34 E0 89 ) // .z\V.4..
.ver 2:0:0:0
}
.assembly extern ADODB
{
.publickeytoken = (B0 3F 5F 7F 11 D5 0A 3A ) // .?_....:
.ver 7:0:3300:0
}
我们这里发现Interop.U8Login.dll 引用了两个组件,一个是2.0的mscorlib,也就是framewor 2.0的基本环境。一个是ADODB,版本7.0。并且我们发现ADDOB有两个特点。
- 1、ADODB的这个引用带有强命名,.publickeytoken = (B0 3F 5F 7F 11 D5 0A 3A )
- 2、ADODB这个引用前面没有Interop单词,.assembly extern ADODB,这个特征很重要,有时候我们在处理Interop.VBA.dll的时候你就会发现,有的引用居然是VBA.dll,有的是Interop.VBA.dll
我们再看一下,正确引用了Interop.U8Login.dll的其他Interop文件,比如下面就是我自己编译的Interop.USERPCO.dll,该类库引用了U8Login组件,这个类库也是非常重要的一个类,主要完成库存单据的保存功能。是API里面最核心的调用类。
// Metadata version: v2.0.50727
.assembly extern mscorlib
{
.publickeytoken = (B7 7A 5C 56 19 34 E0 89 ) // .z\V.4..
.ver 2:0:0:0
}
//这里的引用,publickey和U8login的ADODB引用是完全一样的
.assembly extern ADODB
{
.publickeytoken = (B0 3F 5F 7F 11 D5 0A 3A ) // .?_....:
.ver 7:0:3300:0
}
.assembly extern Interop.VBA
{
.ver 6:0:0:0
}
//这里的引用,没有publickey
.assembly extern Interop.USCOMMON
{
.ver 2:1:0:0
}
//注意这里的引用,有publickey
.assembly extern Interop.U8Login
{
.publickeytoken = (79 A4 E7 AD 54 EE AB CA ) // y...T...
.ver 1:14:0:0
}
.assembly extern Interop.MSXML2
{
.ver 3:0:0:0
}
- 注意U8Login的强命名还有版本号 ,.publickeytoken = (79 A4 E7 AD 54 EE AB CA ) .ver 1:14:0:0
- 注意ADODB的强命名和版本号,这两个值是(.publickeytoken = (B0 3F 5F 7F 11 D5 0A 3A ) .ver 7:0:3300:0 ),且和U8Login使用的ADODB完全一样,这样的文件,就是正确的,因为如果U8Login有返回值是ADODB中的类,那么这个Interop文件可以正常操作,不会出现类型转换失败的问题,因为他们引用的是同一个ADODB文件。
再看一个我们自己生成的Interop后最容易遇到的问题,往往我们自己生成Interop 文件以后,很有可能是这样的
//前面的我就忽略不贴出来了,直接看关键的U8Login的引用
.assembly extern Interop.U8Login
{
.ver 1:29:0:0
}
正确的应该如下
.assembly extern Interop.U8Login
{
.publickeytoken = (79 A4 E7 AD 54 EE AB CA ) // y...T...
.ver 1:14:0:0
}
这里之所以不一致,就是前文说的拖家带口问题导致的,其实你仔细看,就会发现在你生成的Interop文件对应的目录里面,居然还有一个Interop.U8Login.dll,而这个文件就是版本1.29那个文件。而NET程序是绝对不能引用两个同名文件的。所以必须将公用引用文件保持一致。
到此我们发现了正确文件的基本特征:
- 1、公用文件,比如ADODB,他们引用的名称,版本号,强命名都是完全一样的
- 2、彼此关联的文件,比如某DLL引用了Interop.U8Login.dll,必须保证某DLL中U8login的版本和强命名还有名称和原版的Interop.U8Login一致。
毛爷爷教导我们,知错不改,不是好同志!既然我们知道了错误的根本原因,那么下面就开始做一名真正的好同志。
之前说过,Interop.U8Login.dll不是能完美反编译成C#的,但是如果你愿意手工干预,通过后期的修改,这个项目可以完美的变成一个标准C#类库项目,然后通过修改引用,再次编译就好了。可惜,我不认为有人会这么干,因为Interop文件实在太多了,修改工作量巨大,错曾经尝试过几次,最后都放弃了。
正统的解决方案微软其实是给出来的,就是通过在使用Tlbimp.exe这个命令行的时候,使用/reference 参数
Tlbimp.exe C:\U8Soft\Ufcomsql\U8Login.dll /reference:"C:\U8Soft\Interop\MSXML2.dll"
Tlbimp.exe C:\U8Soft\Ufcomsql\USERPCO.dll /reference:"C:\U8Soft\Interop\Interop.U8Login.dll" /reference:"C:\U8Soft\Interop\MSXML2.dll"
上面的代码,展示了如何使用多个已有类库文件,解决引用错误。这个方法真的非常不错,我以前也使用,后来觉得还是改IL比较有成就感,所以就不怎么用这个方法。但是我还是强力推荐该方法。
之前的整个研究过程,都是基于IL进行,所以我最早使用的就是DotNetHelper工具进行IL修改去解决这个问题的。速度也比较快,反编译,打开IL文件,找到错误点,修改,重新编译,问题解决。(这里补充一下,因为ocx控件使用AxImp工具生成AxInterop文件,但是这个命令行工具没有引用参数reference,所以只能使用IL修复法。)修改的过程如下:
//前面的我就忽略不贴出来了,直接看关键的U8Login的引用
.assembly extern Interop.U8Login
{
.ver 1:29:0:0
}
正确的应该如下
.assembly extern Interop.U8Login
{
.publickeytoken = (79 A4 E7 AD 54 EE AB CA ) // y...T...
.ver 1:14:0:0
}
上文已经找到并解决了Interop错误问题,此时就可以开开心心的引用这些劳动成果,并编译出我们心爱的C#项目。可是每次我们想发版给客户的时候,都会非常郁闷,因为编译后的文件,简直多的可怕。比如下面这个截图,做了一个小的不能再小的项目了,U8登录加标准单据保存功能。结果一大堆文件,你说吓人不!
如果可以忍受这样的发版目录,那么没问题,直接拷贝给客户,运行就好了。但是强迫症的我,必须想想办法,我可不想在如此多的文件里面找可执行程序,最好目录更干净一些,C#项目引用也干净一些,不想每次都引用一大堆的Interop。毕竟开发的项目很多,如果每一个项目都引用一大堆鬼Interop,实在是崩溃。
还有一个问题,就是这些Interop在用友的各个版本里面都是一样名字,如果时间久了,你很难记住你这个项目是哪个版本的,很容易造成旧版在新系统环境中运行,结果GUID不一致,造成COM初始化失败。所以我们才经常遇到一种报错说{XXX-XXX-XXXX-一个长长的GUID}不能类型转换或者初始化失败。比如你的代码编写时还是V11.0,后来想在V13.0里面跑,结果解决方案中的Interop替换不完整。结局就莎士比亚了!
你去U8的Interop下面看看,如此之多的Interop,看着都眼晕。这我也就忍了,如果你翻看其他目录,会发现居然也有Interop文件,换言之,总部没有完全控制住Interop文件的统一性。
比如C:\U8SOFT\U8KCSN\bin\这个目录,就出现了一批新的Interop。这个目录是条码功能的web服务,这个目录里面的代码建议大家反编译以后好好研究一下,写的好不好我不敢评论,但是这是我唯一找到的全部使用C#开发的U8功能,而且还是WEB版本的。里面包装了好多U8库存单据的保存功能代码,他没有调用U8 API,而是使用Interop技术直接调用COM功能,他保存的单据支持货位,序列号,上下游单据咬合等功能,所以研究价值很高。最核心的代码都在Barcode.SV.DataAccess.DLL里面。
上面的Interop新问题,这里总结一下:
- 1、Interop在各个U8版本中同名,但是COM组件在各个U8版本中GUID会变,造成旧的Interop不能在新环境下使用,必须重新引用编译。
- 2、繁多的Interop,让你的项目在重新引用的时候,非常麻烦,而且容易出错。
- 3、当想复用别的模块中现成的DLL时,发现总部的Interop已经出现多版本了。如果去引用这些劳动成果,又会出现前文提到的同名文件不同强命名问题。
写到这里,我特别想说,总部这帮哥们天天都在干什么,能不能搞搞底层建设,开源开源业务代码。天天蹲在屋子里面,不是改BUG,就是在增加新需求。就不能给我们二开留一口吃饭的地方吗?搞一个API吧,BO对象里面的字段各种错误,让二开回归使用DOM法,一个单据一百多个字段,我上哪知道那个字段是什么。搞一个EAI,不能上下游单据咬合,就能导入个凭证BOM和订单。好容易好容易憋出来一个OpenAPI,结果还是EAI的变体,还要绕一大圈广域网来调用,性能不敢恭维。
如果U8能够趋向于平台闭源,加开源业务模块,那么总部就能从BUG解决中释放出来,让更多的伙伴去提供开源的业务模块功能,去修改现有模块中的错误。人多力量大,非要吃独食,说多了,回归正题。
针对上面的三个问题,我最理想的效果是这样:
- 1、Interop文件以某种功能或者分类,进行大合并,将现有的多个Interop合并到一个或者几个DLL中。
- 2、类名的命名规则必须出现版本号,例如COMV1300_U8Login.clsLogin,最大化规避版本错乱问题,当然这也带来了代码需要更新的问题,这个我也想过,只要批量替换COMV1300_就好了,完全可以通过命名规则搞定。
- 3、只要有了1和2,基本所有问题就能解决了,此时我可以引用COMV1300的dll和其他的原厂interop了,因为类名被我都加上了COMV1300这样的字符串,所以就没有了重名的问题。如果想在代码里调用其他类库的函数,并把我们自己的COM传进去,可以使用类型强制转换,转换后就可以将我的COM实例,送给其他的Interop类了。
简单说,就是把繁多的interop文件熔化掉,让他们变成一个类库文件,或者说一个DLL文件,这样的技术我把他叫做Interop合并。
为了实现Interop合并,我之前也试过一些方法。比如:把Interop全部反编译成C#,然后将这些源代码合并到一个类库项目里面。但是这些尝试最终都以失败告终。
原因就是反编译质量达不到要求,代码需要改动的地方太多了,就下图这些DLL如果反编译出来,代码错误量就达到了7000多处,根本改不起。后来我就Google了一下,看看有没有工具可以把编译好的DLL文件直接合并的。然后就发现了这个利器ILMerge。
这个工具可以说特别适合Interop的合并,他的工作方式就是将多个dll类库文件合并到一个dll文件中,下图是使用界面。
使用的时候,把你想合并的Interop拖进去,然后按合并按钮。接着,软件开始合并,过程中会有些错误要处理,主要就是Interop文件缺失,因为你的这Interop文件会引用其他的Interop,所以必须把所有需要合并的或者有相互关联的Interop都放在一个目录下,这样他才能在合并以后,仍然是一个正常的DLL文件。
比如我有A.dll B.dll 还有C.dll,其中B和C都引用了A,现在我想把BC合并成BC.dll,那么你就需要把三个文件放在一个目录下面,然后拖拽BC两个文件到ILMerge中,然后合并生成BC.dll。项目交付的时候你就需要交付A.dll和BC.dll两个Dll就好了。
当然你也可以,直接合并出来一个叫做ABC.dll文件,这样发版的时候就一个文件了,甚至你可以把你的exe和ABC.dll也合并到一起,最后就剩下一个exe文件。
但是要注意,最好不要把U8原厂的.NET的DLL文件合并,因为原厂的DLL会有强命名,你合并以后没法调用其他的类库了。这件事情说起来比较绕口,只需记住几个基本原则:
- 凡是Interop类型的或者AxInterop类型的文件,全部可以合并到一个DLL中。
- 凡是U8原厂的非Interop(包括AxInterop)文件,一律不进行合并。
- 自己写的类库,可以合并,但是不建议合并到Interop的那个DLL中,可以考虑直接和exe合并。
合并前,有一点需要特别注意,就是这些待合并的Interop文件,一定都是经过修复引用的,千万不要有DLL陷阱等问题,否则就会失败。
还有一点需要说明一下,这个工具只能合并出来Framework4.0的DLL,如果觉得太高,可以使用DotNetHelper转成3.5的。
在理想小节中讲过,为了防止类名同名,我希望能够将所有的Interop类名标记成带有版本号的,这样在我开发的时候和编译的时候能够准确知道是否是我想要的版本,代码和DLL是否能匹配上。
所以这里介绍如何将合并后的Interop文件中的类名重命名。由于这里没有工具可以使用,所以我纠结了很长时间是否需要改名,有一段时间甚至想要放弃这个想法,直到我看到了Mono.cecil类库。
其实修改类名的工作在IL级别上可以完成,但是经过实际的修改发现,工作量简直了!所以当需要批量有规则的修改IL的时候最好使用Mono.cecil类库进行编程。
改名前,先看一下我们合并出来的类库到底什么样子,然后修改后应该是什么样子。
上图就是修改后和修改前的对比,我主要把第一级的命名空间改变了,例如VBA.Collection这个类变成了COMV13000_VBA.Collection。
有了清晰的目标,我们就可以把大象放进冰箱里面了,正好三步。
下面说一下具体的操作环境和代码,首先需要把合并好的Interop DLL保存好,这里刚刚合并好的文件名称是U8Lib_V13000.dll,把他放在一个固定的文件夹下。我放入了C:\开发目录\MiniU8Meger\U8Lib_V13000\U8Lib_V13000.dll。
using Mono.Cecil;
static void Main(string[] args)
{
//这里增加修改的类名对照
var dicRename = new Dictionary();
dicRename["U8Login"] = "COMV1300_U8Login";
dicRename["ADODB"] = "COMV1300_ADODB";
dicRename["interop.userpco"] = "COMV1300_USERPCO";
dicRename["Interop.VoucherCO_SA"] = "COMV1300_VoucherCO_SA";
dicRename["MSXML2"] = "COMV1300_MSXML2";
dicRename["Scripting"] = "COMV1300_Scripting";
dicRename["USCOMMON"] = "COMV1300_USCOMMON";
dicRename["USERPVO"] = "COMV1300_USERPVO";
dicRename["USSAServer"] = "COMV1300_USSAServer";
dicRename["VBA"] = "COMV1300_VBA";
dicRename["MultiLangPkg"] = "COMV1300_MultiLangPkg";
dicRename["SystemInfo"] = "COMV1300_SystemInfo";
dicRename["AxUAPVoucherControl85"] = "COMV1300_AxUAPVoucherControl85";
dicRename["Skinse_VB_API"] = "COMV1300_Skinse_VB_API";
dicRename["UAPUfToolKit85"] = "COMV1300_UAPUfToolKit85";
dicRename["UAPVoucherControl85"] = "COMV1300_UAPVoucherControl85";
dicRename["UFVoucherServer85"] = "COMV1300_UFVoucherServer85";
//读取文件
var assembly = AssemblyDefinition.ReadAssembly("C:\\开发目录\\MiniU8Meger\\U8Lib_V13000\\U8Lib_V13000.dll");
;
//修改文件
ModifyAssemblyByDic(dicRename,ref assembly);
//保存文件
assembly.Write("C:\\开发目录\\MiniU8Meger\\U8Lib_V13000\\U8Lib.dll");
}
private static void ModifyAssemblyByDic(Dictionary dicRename, ref AssemblyDefinition assembly)
{
var types = assembly.MainModule.Types;
assembly.Name.Name = "U8Lib";
foreach (var type in types)
{
Console.WriteLine($"typename:{type.Name} namespace:{type.Namespace} ");
ReplaceNameByDic(type, dicRename);
try
{
var attrs =
type.CustomAttributes.Where(ca => ca.AttributeType.FullName.Contains("CoClassAttribut"));
foreach (var attr in attrs)
{
if (attr.ConstructorArguments[0].Value.GetType().Equals( typeof(TypeDefinition)))
{
var c = (TypeDefinition)attr.ConstructorArguments[0].Value;
ReplaceNameByDic(c, dicRename);
}
else if (attr.ConstructorArguments[0].Value.GetType().Equals(typeof(TypeReference)))
{
var r = (TypeReference)attr.ConstructorArguments[0].Value;
ReplaceNameByDic(r, dicRename);
}
else
{
}
}
attrs =
type.CustomAttributes.Where(ca => ca.AttributeType.FullName.Contains("ComEventInterfaceAttribut"));
foreach (var attr in attrs)
{
foreach (var aa in attr.ConstructorArguments)
{
var c = (TypeDefinition)aa.Value;
ReplaceNameByDic(c, dicRename);
}
}
attrs = type.CustomAttributes.Where(ca => ca.AttributeType.FullName.Contains("ComSourceInterfacesAttribut"));
foreach (var attr in attrs)
{
foreach (var aa in attr.ConstructorArguments)
{
var c = (TypeDefinition)aa.Value;
ReplaceNameByDic(c, dicRename);
}
}
}
catch (Exception ex)
{
// ignored
}
}
}
private static void ReplaceNameByDic(TypeReference type, IDictionary dicRename)
{
var key = type.Namespace;
if (dicRename.ContainsKey(key))
type.Namespace = dicRename[key];
}
到此成果物出来了,就是C:\开发目录\MiniU8Meger\U8Lib_V13000\U8Lib.dll,我们可以引用到C#项目里面看看效果了。
在ILmerge的时候一定不能选第二个选项,就是上面截图中的Union duplicate,否则出来的文件非常奇怪,用Cecil我没有改明白。如果哪位仁兄研究出来,可以跟我讲讲,反正我的代码不能修改Union duplicate合并出来的DLL。修改出来也不能正常使用。
平时使用的时候,直接引用U8Lib.dll,然后就可以正常写代码了,甚至你都可以在没有U8安装的环境里面编译代码,只要U8Lib.dll在,所有的COM都可以正常编译和使用,但是这里有一点需要注意,具有界面的控件,可以正常编译,但是在设计界面的时候,会出错。下图是正常情况下的,不正常的我就不演示了。
在引用U8Lib时,有个叫做是否嵌入的选项,如果选择是,VS会在编译的时候直接将U8Lib中的类嵌入到exe文件中或者你的类库中,发版时就不需要U8Lib了,但是我不建议这样使用,因为只有U8Lib中含有Axinterop或者说有控件类的COM,VS编译时,强行不嵌入。原因不明。如果你的项目全程无界面,就是个WebService可以考虑使用嵌入。
到此C#调用U8 COM组件技术的介绍,就全部结束了。该技术目前可以稳定运行在Framework4.0环境中,换言之,你可以用4.0开发U8的相关组件,生产环境中,我有个WebService项目就是4.0的,提供给第三方供应商api功能,方便对接调用,他调我的WebService,我调Interop的COM保存单据,从订单发货到出库,从采购入库到发票,全部自动生成,自动咬合,用起来还是蛮舒服的,这样不用花钱买eai了,爽哉,哈哈。
后期我会考虑,传送一部分成果物到百度网盘上,给大家看看成果物是什么样子的,但是目前代码比较乱,新旧技术混杂在一起,所以我再整理一下。
在上一篇C#下COM组件调用篇中,我当时没有把OCX的使用方法说清楚,主要原因是我那时有一个核心问题没有解决好,后来在CodeProject中看到一篇文章,一下解开了心中多年的疑惑,豁然开朗。在这里分享一下最新的研究成果。
其实OCX使用中,有两个关键点需要解决:
只要大家亲身使用过OCX控件就会有体会,以前我都是直接在工具箱里面点击添加项,然后选COM页签,最后找到我想要的控件,选中确认。此时VS会进行一连串的TlbImp和AxImp操作,然后给你自动生成一大堆的Interop和AxInterop,简直是自动的不要不要的。但是以前文章中说过的COM Interop 拖家带口问题和ADODB问题,会造成你的项目引用非常混乱,2008系统会各种报错。
加之U8的COM组件和OCX控件引用关键错终复杂,搞得我最后一听说要画控件在Form上就恶心。我也在网络上找了很多资料,不管是国内的还是国外的论坛,基本回答都是手工使用AxImp加上rcw参数,解决引用问题。然后都不说怎么在VS的工具箱里增加控件。后来我测试发现,rcw参数也解决不了ADODB错误。我也就没有在深入研究下去。有兴趣的可以看看我之前的文章,rcw参数和TlbImp的reference参数差不多。
第一个问题,Interop引用错误问题,还是建议使用IL工具进行编译修改。如果不会或者不明白看我之前的COM使用技巧第二章。换言之,我们必须先得到一个已经解决了引用关系的Interop。
以前我一直非常迷惑AxImp这个命令行工具到底是干什么的,他生成出来的AxInterop文件和Interop文件到底有什么区别。后来经过反编译,我才真正明白,其实AxInterop文件就是一个继承了AxHost类的控件类文件。
比如我们有一个按钮控件暂且叫做Button.ocx吧,正常情况下,应该是用tlbImp搞出来一个Interop.Button.Dll和AxImp搞出来一个AxInterop.Button.Dll,但AxInterop文件其实仅仅是引用了Interop.Button.Dll,并编写了一个继承AxHost的AxButton类而已(当然还有一些其他的关联类,主要是消息对象)。如果我可以手工写一个AxButton类,是不是也可以呢?这样就不用使用AxInterop文件了。甚至我把我想要的所有Ax控件类,都写到一个类库里面。经过测试,这个猜想是可以的,以至于AxImp早已经想到这件事情,他可以直接吐出来一个源码文件给你。只要经过简单的修改就可以正常使用了。
那如何让AxImp工具吐出来源码呢?其实仅仅需要使用到AxImp工具的一个参数,直接给大家看一下命令行
C:\ax>aximp /source "c:\u8soft\ufcomsql\vsflex8u.ocx"
生成的源: C:\ax\AxVSFlex8U.cs
生成的程序集: C:\ax\VSFlex8U.dll
生成的程序集: C:\ax\AxVSFlex8U.dll
简单说,我们这里利用aximp的一个叫做srource的参数,就可以搞出来一个C#版本的源码文件,引入项目,并通过修改这个文件的代码就可以让控件显示在工具箱里面了。
这里有一点,需格外强调一下,我们虽然这里使用了aximp,但是他生成的其他文件我们都不用,比如后面两个DLL文件,或者说,就唯独使用那个cs源码文件。下面看看这个刚刚生成出来的CS文件,热乎的时候是什么样子,记住此时他是不正确的,需要进行下一步修改才可以。
//------------------------------------------------------------------------------
//
// 此代码由工具生成。
// 运行时版本:4.0.30319.42000
//
// 对此文件的更改可能会导致不正确的行为,并且如果
// 重新生成代码,这些更改将会丢失。
//
//------------------------------------------------------------------------------
[assembly: System.Reflection.AssemblyVersion("1.0.0.0")]
[assembly: System.Windows.Forms.AxHost.TypeLibraryTimeStamp("09/30/2011 10:57:06")]
namespace AxVSFlex8U {
[System.Windows.Forms.AxHost.ClsidAttribute("{3705b4e2-8fcb-4a21-8cce-5d6a98c32456}")]
[System.ComponentModel.DesignTimeVisibleAttribute(true)]
[System.ComponentModel.DefaultEvent("SelChange")]
[System.ComponentModel.DefaultProperty("Text")]
public class AxVSFlexGrid : System.Windows.Forms.AxHost {
private VSFlex8U.IVSFlexGrid ocx;
private AxVSFlexGridEventMulticaster eventMulticaster;
private System.Windows.Forms.AxHost.ConnectionPointCookie cookie;
private msdatasrc.DataSource axDataSource;
public AxVSFlexGrid() :
base("3705b4e2-8fcb-4a21-8cce-5d6a98c32456") {
this.SetAboutBoxDelegate(new AboutBoxDelegate(About));
}
'后面代码一律省略,毛用没有,没必要看!!!
注意看这个文件的几个地方,第一个就是版本号,在上文的11行
[assembly: System.Reflection.AssemblyVersion("1.0.0.0")]
这行代码必须删除,放在这里就是报错用的。
namespace AxVSFlex8U {
命名空间必须修改成你的项目中的命名空间
'必须增加ToolboxItem参数
[System.ComponentModel.ToolboxItem(true)]
public class AxVSFlexGrid : System.Windows.Forms.AxHost {
这个非常非常重要,就因为有了这个才真正让VS识别出来是控件类。
最后添加 MSACAL.dll 和 Stdole.dll 两个引用,一般情况下VS会提示你的。
最后我们看一下完美解决后的文件
'删除垃圾代码
[assembly: System.Windows.Forms.AxHost.TypeLibraryTimeStamp("09/30/2011 10:57:06")]
namespace WindowsFormsApp1 '命名空间修改
{
[System.Windows.Forms.AxHost.ClsidAttribute("{3705b4e2-8fcb-4a21-8cce-5d6a98c32456}")]
[System.ComponentModel.DesignTimeVisibleAttribute(true)]
[System.ComponentModel.DefaultEvent("SelChange")]
[System.ComponentModel.DefaultProperty("Text")]
[System.ComponentModel.ToolboxItem(true)] '属性增加
public class AxVSFlexGrid : System.Windows.Forms.AxHost {
然后试着编译一下程序,用X86编译你的项目,64的编译后不能拖拽控件,去设计一下窗体,看看工具箱里面是不是出现了一个叫做AxVSFlexGrid的控件了,然后拖拽控件进入Form,版权提示后,就有一个美丽的控件嵌入Form中了。是不是很神奇!
在构建项目的时候,只要引用Interop就可以了,控件类完全靠AxImp生成的源代码来支持。
如果你的环境没有安装VB6开发工具,则你可能会发现一种报错,中文意思是你没有控件许可,一般是在你中工具箱里拖拽控件去Form时,VS弹出来的异常。其实这是一个控件许可证的问题,OCX的控件往往都需要一种叫做设计许可证的东西,防止其他人随便使用,我后来在网上翻看了一下,发现这个问题其实并不复杂,主要有两个关键点:
到此OCX问题完美解决了。如果你想让劳动成果最大化复用,并为后期的项目使用,完全可以把这个项目编译成类库,然后供给其他项目引用。我侧后发现是好用的,关键点在于需要到工具箱里面增加引用,如图
然后就可以正常拖拽控件进入Form了,使用过程非常简单,记住你的项目必须X86。
也就是说如果你愿意,可以把所有常用的控件都封装到一个DLL中,然后引用后直接使用,这操作简直666,比VB6引用控件方便太多了。