[转载]ASP.NET 4.0 与 Entity Framework 4-第三篇-使用Entity Framework调用存储过程

mikel阅读(787)

[转载]ASP.NET 4.0 与 Entity Framework 4-第三篇-使用Entity Framework调用存储过程 – Lloyd Sheng Blog – 博客园.

摘要

本文将向你介绍如何调用存储过程对SQL Server数据库中的数据进行CRUD操作。文中使用的数据库依然是在本系列第一篇文章《采 用Model-First 开发方式创建数据库》中创建的,使用的Web页面是在第二篇文章《使 用Entity Framework 进行CRUD操作》中创建的 ,可以点击[代码]下载链下载 项目文件,要用VS2010打开。

步骤1.创建存储过程

首先需要创建存储过程,第一个存储过程的功能是查表UserAccount记录的,代码如下:

  1. CREATE PROCEDURE dbo.UserAccounts_SelectAll
  2. AS
  3. SET NOCOUNT ON
  4. SELECT Id, FirstName, LastName, AuditFields_InsertDate,
  5. AuditFields_UpdateDate
  6. FROM UserAccounts
  7. RETURN

下面一个存储过程功能是通过Id选择UserAccount表中一条记录,代码如下:

  1. CREATE PROCEDURE dbo.UserAccounts_SelectById
  2. (
  3. @Id int
  4. )
  5. AS
  6. SET NOCOUNT ON
  7. SELECT Id, FirstName, LastName, AuditFields_InsertDate,
  8. AuditFields_UpdateDate
  9. FROM UserAccounts
  10. WHERE Id = @Id
  11. RETURN

第三个存储过程的功能是向UserAccounts表中插入一条记录,代码如下:

  1. CREATE PROCEDURE dbo.UserAccounts_Insert
  2. (
  3. @FirstName nvarchar(50),
  4. @LastName nvarchar(50),
  5. @AuditFields_InsertDate datetime,
  6. @AuditFields_UpdateDate datetime
  7. )
  8. AS
  9. INSERT INTO UserAccounts (FirstName, LastName, AuditFields_InsertDate,
  10. AuditFields_UpdateDate)
  11. VALUES (@FirstName, @LastName, @AuditFields_InsertDate,
  12. @AuditFields_UpdateDate)
  13. SELECT SCOPE_IDENTITY() AS Id

第四个存储过程的功能是更新UserAccount表中的数据,代码如下:

  1. CREATE PROCEDURE dbo.UserAccounts_Update
  2. (
  3. @Id int,
  4. @FirstName nvarchar(50),
  5. @LastName nvarchar(50),
  6. @AuditFields_UpdateDate datetime
  7. )
  8. AS
  9. SET NOCOUNT ON
  10. UPDATE UserAccounts
  11. SET FirstName = @FirstName,
  12. LastName = @LastName,
  13. AuditFields_UpdateDate = @AuditFields_UpdateDate
  14. WHERE Id = @Id
  15. RETURN

最后一个存储过程的功能是删除UserAccount表中的记录,代码如下:

  1. CREATE PROCEDURE dbo.UserAccounts_Delete
  2. (
  3. @Id int
  4. )
  5. AS
  6. SET NOCOUNT ON
  7. DELETE
  8. FROM UserAccounts
  9. WHERE Id = @Id
  10. RETURN

在我们进行下一步之前,你必须在OrderSystem数据库中创建上面的那些存储过程。

步骤2:将存储过程添加到实体中

打开项目中的OrderDB.edmx文件。打开后,会出现设计器,同时模型浏览器里可以看到数据库的实体,复杂属性和关系。在 OrderDB.edmx右键选择Update Mode From DataBase,这时会出现更新向导,可以看到前面的创建的5个存储过程显示出来。

展开Stored Procedures节点,选择所有存储过程,然后点击完成。这时模型浏览器中会多出5个存储过程对象。

右击模型浏览器中的UserAccounts_SelectAll存储过程选择Add Function Import,这时就可以给OrdersDBContainer类添加一个执行UserAccounts_SelectAll存储过程的方法了。

选择存储过程执行后的返回值类型,当我们要返回一个UserAccount列表,而Framework没有提供这种类型,这时采用这个方法创建会很 方便。我们可以先创建一个复杂类型用来表示存储过程返回值的类型。由于SelectAll 和 SelectById 两个存储过程返回的字段相同,只需创建一种复杂类型。

点击Get Column Information按钮,VS将会检测存储过程返回的字段。当返回字段检测完后,点击下面的Create New Complex Type 按钮,这时会自动选择Complex 选项,复杂类型的名字也会添加到右边下列框中。默认的复杂类型的名字为存储过程的名字加上“Result”。由于将会在多个存储过程返回值中使用该复杂类 型,建议取一个比较通用的名字,这里将名字改为“UserAccounts_Select_Result”,点击OK。

这时你会看到在模型浏览器的Function Imports 文件夹中多了一个UserAccounts_SelectAll方法,同时在复杂类型节点下面多了一个 UserAccounts_Select_Result类型。

右击模型浏览器中的UserAccounts_SelectById 存储过程选择Add Function Import,然后选择UserAccounts_Select_Result 类型作为该存储过程的返回值类型,点击OK。

下面是在UserAccount中使用插入,更新和删除的存储过程,右击设计器中的UserAccount实体选择Stored Procedure Mapping,会出现详细映射窗口。

点击第一行的<Select Insert Function>,在下拉列表中选择UserAccounts_Insert ,在格子中会显示参数列表。我们必须进行实体属性到存储过程参数的映射。由于存储过程会返回新创建记录的Id,必须选择该Id赋值到哪个属性。在 Result Column Bindings节点下的<Add Result Binding>输入Id,这里返回的Id是存储过程中“SELECT SCOPE_IDENTITY() AS Id”,下面是绑定完成后的结果:

映射更 新的存储过程,将<Select Update Function>替换为UserAccounts_Update存储过程。其他的属性自动映射完成,只有 AuditFields_UpdateDate需要自动映射为AuditFields.UpdateDate属性。

最后是映射Delete存储过程,将<Select Delete Function>替换为UserAccounts_Delete存储过程,Id参数映射到和实体的Id属性。

不要忘了保存上面的操作结果,OrdersDBContainer 现在就能使用那些存储过程了,从而进行UserAccount记录的CRUD操作。

步骤3:创建Web表单

下面将在程序中创建一个表单,用来管理UserAccount数据。

1.在项目文件上右键,选择Add->New Item..

2.选择Web Form模板,将名字改为UsersSP.aspx,点“Add”。

3.在UserSP.aspx的div之间添加如下代码:

  1. <table>
  2. <tr>
  3. <td>Select A User:</td>
  4. <td><asp:DropDownList runat=server ID=“ddlUsers” AutoPostBack=“True”>
  5. </asp:DropDownList> </td>
  6. </tr>
  7. <tr>
  8. <td>First Name:</td>
  9. <td><asp:TextBox runat=“server” ID=“txtFirstName”></asp:TextBox></td>
  10. </tr>
  11. <tr>
  12. <td>Last Name:</td>
  13. <td><asp:TextBox runat=“server” ID=“txtLastName”></asp:TextBox></td>
  14. </tr>
  15. <tr>
  16. <td>Inserted:</td>
  17. <td><asp:Label runat=“server” ID=“lblInserted”></asp:Label> </td>
  18. </tr>
  19. <tr>
  20. <td>Updated:</td>
  21. <td><asp:Label runat=“server” ID=“lblUpdated”></asp:Label> </td>
  22. </tr>
  23. </table>
  24. <asp:Button runat=server ID=“btnSave” Text=“Save” />
  25. <asp:Button ID=“btnDelete” runat=“server” Text=“Delete” />

这里采用的table的对form进行了简单的布局。转到Design视图你会看到表单的样子如下:

步骤4:将数据加载到Drop Down List中

要完成的是在页面加载时,将UserAccount的Name和Id数据加载到Drop Down List中。当选择特定项时,加载更加详细的信息。

1.双击Degsin视图(F7),在后台代码中添加Page_Load 事件。

2.处理Page_Load 事件的代码如下:

  1. protected void Page_Load(object sender, EventArgs e)
  2. {
  3. if (!IsPostBack)
  4. {
  5. LoadUserDropDownList();
  6. }
  7. }

3.Page_Load方法中的LoadUserDropDownList方法代码如下:

  1. private void LoadUserDropDownList()
  2. {
  3. ddlUsers.DataSource = from u in db.UserAccounts_SelectAll()
  4. orderby u.LastName
  5. select new { Name = u.LastName + “, “ + u.FirstName,
  6. Id = u.Id };
  7. ddlUsers.DataTextField = “Name”;
  8. ddlUsers.DataValueField = “Id”;
  9. ddlUsers.DataBind();
  10. ddlUsers.Items.Insert(0, new ListItem(“Create New User”, “”));
  11. }
  12. }

注意Linq的from语句,它调用OrderDBContainer的UserAccounts_SelectAll方法,这个方法将会执行存储 过程。

DataTextField属性设置为Name,DataValueField 设置为Id,这些都是在Linq查询中创建的。设置完成后,就是绑定了。绑定时,才真正开始调用数据库查询的操作。最后给Drop Down List添加一项“Crate New User.”,这项是用来区分更新和添加操作的。

现在数据库中还没有任何数据,Drop Down List中只有”Crete New User”一项。

步骤5:添加和更新数据

下面将向你介绍如何添加和更新表中的数据。

1.转到设计视图,双击Save按钮,创建该按钮的点击事件。

2.处理点击事件的代码如下:

  1. using (OrderDBContainer db = new OrderDBContainer())
  2. {
  3. UserAccount userAccount = new UserAccount();
  4. userAccount.FirstName = txtFirstName.Text;
  5. userAccount.LastName = txtLastName.Text;
  6. userAccount.AuditFields.UpdateDate = DateTime.Now;
  7. if (ddlUsers.SelectedItem.Value == “”)
  8. {
  9. //Adding
  10. userAccount.AuditFields.InsertDate = DateTime.Now;
  11. db.UserAccounts.AddObject(userAccount);
  12. }
  13. else
  14. {
  15. //Updating
  16. userAccount.Id = Convert.ToInt32(ddlUsers.SelectedValue);
  17. userAccount.AuditFields.InsertDate = Convert.ToDateTime(lblInserted.Text);
  18. db.UserAccounts.Attach(userAccount);
  19. db.ObjectStateManager.ChangeObjectState(userAccount, System.Data.EntityState.Modified);
  20. }
  21. db.SaveChanges();
  22. lblInserted.Text = userAccount.AuditFields.InsertDate.ToString();
  23. lblUpdated.Text = userAccount.AuditFields.UpdateDate.ToString();
  24. //Reload the drop down list
  25. LoadUserDropDownList();
  26. //Select the one the user just saved.
  27. ddlUsers.Items.FindByValue(userAccount.Id.ToString()).Selected = true;
  28. }
  29. }

代码首先创建OrderDBContainer对象,再创建UserAccount对象,用输入的值填充UserAccount对象属性。更新日期 用系统当前时间,接着判断是更新操作还是添加操作了。最后就是更新Drop Down List的值并选中刚刚操作的UserAccout。这里的db.SaveChanges()最后实际上是在数据库中执行添加或更新语句。想数据库添加数 据时,打开SQL Profiler会看到Insert存储过程被调用。

  1. exec [dbo].[UserAccounts_Insert]
  2. @FirstName=N‘Lloyd’,
  3. @LastName=N‘Sheng’,
  4. @AuditFields_InsertDate=‘2010-04-26 18:14:42.4564241’,
  5. @AuditFields_UpdateDate=‘2010-04-26 18:14:42.4564241’

步骤6:查询数据

下面是实现当用户选择某一个Drop Down List项时,显示详细信息的功能。

1.双击视图设计器中的Drop Down List,这时会创建Drop Down List的SelectedIndexChanged方法。

2.编写SelectedIndexChanged方法的代码如下:

  1. if (ddlUsers.SelectedValue == “”)
  2. {
  3. txtFirstName.Text = “”;
  4. txtLastName.Text = “”;
  5. lblInserted.Text = “”;
  6. lblUpdated.Text = “”;
  7. }
  8. else
  9. {
  10. //Get the user from the DB
  11. using (OrderDBContainer db = new OrderDBContainer())
  12. {
  13. int userAccountId = Convert.ToInt32(ddlUsers.SelectedValue);
  14. var userAccounts = from u in db.UserAccounts_SelectById(userAccountId)
  15. select u;
  16. txtFirstName.Text = “”;
  17. txtLastName.Text = “”;
  18. lblInserted.Text = “”;
  19. lblUpdated.Text = “”;
  20. foreach (UserAccounts_Select_Result userAccount in userAccounts)
  21. {
  22. txtFirstName.Text = userAccount.FirstName;
  23. txtLastName.Text = userAccount.LastName;
  24. lblInserted.Text = userAccount.AuditFields_InsertDate.ToString();
  25. lblUpdated.Text = userAccount.AuditFields_UpdateDate.ToString();
  26. }
  27. }
  28. }

代码根据Drop Down List选择的Id,调用UserAccounts_SelectById方法从数据库中查询一条数据并且显示出来。

步骤7:删除数据

最后就是删除数据的功能了。 1.转到视图设计器,双击“Delete”按钮。 2.添加如下代码:

  1. if using (OrderDBContainer db = new OrderDBContainer())
  2. {
  3. if (ddlUsers.SelectedItem.Value != “”)
  4. {
  5. UserAccount userAccount = new UserAccount();
  6. userAccount.Id = Convert.ToInt32(ddlUsers.SelectedValue);
  7. db.UserAccounts.Attach(userAccount);
  8. db.ObjectStateManager.ChangeObjectState(userAccount,
  9. System.Data.EntityState.Deleted);
  10. db.SaveChanges();
  11. LoadUserDropDownList();
  12. txtFirstName.Text = “”;
  13. txtLastName.Text = “”;
  14. lblInserted.Text = “”;
  15. lblUpdated.Text = “”;
  16. }
  17. }

代码首先创建了一个UserAccount对象,将它的Id设置为选中项的Id.然后将UserAccount附加到UserAccount集合 中,设置它的状态为删除。调用SaveChanges操作,将该条数据删除,刷新Drop Down List的数据源,搞定!

小结

第二篇文章中告诉你如何用Entity Framework进行CRUD操作,本篇文章张告诉你如何用存储过程和Entity Framework进行CRUD操作。

在下一篇文章中我将想你介绍如何在三层结构中使用Entity Framework,希望你能喜欢。

项目代码:http://files.cnblogs.com/lloydsheng/OrderSystem3.zip

本文永久链接:http://lloydsheng.com/2010/04/aspnet40-entityframework4-execute-stored-procedures-using-entity-framework4.html

———–全文结束,感谢您的阅读!————-


作者:LloydSheng
出处:http://www.cnblogs.com/lloydsheng/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
该文章也同时发布在我的独立博客中-LoydShengBlog
如果你也对互联网相关新技术感兴趣,想及时了解更多新鲜有意思的东西,可以关注我的微薄[网易微博| 新浪微博|Twitter]

[转载]自然框架:CMS之数据库设计

mikel阅读(1245)

[转载]【自然框架】CMS之数据库设计 – 金色海洋工作室 – 博客园.

在园子里也混了三年多,随笔200多,一开始只是想把自己的经验写一下,后来呢弄出来了一个“自然框架”,主要精力就放在了介绍自然框架的思路 上面了。随笔多了就发现一个问题:有点乱。虽然博客有分组,但是只支持一级分组,不支持n级的。博客里也没有“栏目”这一类的设置。所以对于随笔的管理有 有点力不从心了。有些兄弟看到我的博客,看到我说自然框架,然后就会很迷茫,自然框架到底是什么?能做什么?如果想看看的话,从什么地方开始看,按照什么 顺序来看?

博客的这种形式就不大好解决这种需求了,当然也许是我对博客还不了解,没有用好吧。所以我想做一个网站,这个网站专门介绍自然框架。一开始只想 做一个静态的,内容也不多嘛,做几个页面,介绍一下,把博客里的随笔整理一下做个目录便于阅读。但是试了一下才发现,静态页面好麻烦呀,也许是我太懒了 吧,总是想简单一些。于是就想做一个简单的CMS,然后用这个CMS来做自然框架的介绍网站

您可能会说了,海洋又在重复制造轮子了,网上有一大堆现成的,有很多成熟的不去用,自己写什么呀?

首先呢,我是程序员(嘿嘿),我先想到的是我自己能不能做出来?别人能做我为什么不行?我不是顾客,我也不是有钱人,到处去弄现成的。其次呢, 做一个CMS也是一个练手的机会,同时也是自然框架的一个Demo,比较大的、完整的Demo。借此来说明自然框架的使用方式,和在网页里的作用。最后就 是想借此说一下我的设计数据库的思路。我觉得我的设计数据库的思路还是有点特色的。

好了,开始进入正题。

首先是了解需求。一个网站会有什么?首页、新闻(图文形式的信息)、产品介绍、文件下载、图片浏览、在线视频等。这些都算是“内容”的几种形式 吧,当然还可以有其他的形式。

这个需求比较简单,也比较简陋,暂时就以这个需求来进行设计吧。如果是按照面向对象的方式要如何设计呢?这个我不太清楚,也许是要画一个UML 吧,也许要建模。尝试一下,画了一个UML不知道对不对,拿出来请大家批批。

【CMS的类图】

图很简单也没什么具体的属性,因为需求是变化的,现在也没有太具体的需求,所以属性就先设置几个主要的。另外俺英文不好,怕查出来的英文单词不 正确产生歧义,所以直接用汉字了。可能您看着很别扭,但是至少不会产生什么歧义,理解起来也会比较容易吧,呵呵。

“内容”作为父类,其他的作为子类。内容是一种“抽象”,把各种形式的内容的共同部分提炼出来,比如标题、内容、添加人、添加日期、点击量等。 子类负责各自特有的属性。

我觉得这种提炼的方式比较好,在设计数据库表结构的时候可以借鉴一下。于是就有了这样的数据库设计。

【CMS ER图】

“内容”作为主体和中心,其他的都是为了这个中心(内容)来服务的。左面是对内容的限制,栏目相当于大分类,分类就是小分类(可以是n级的), 类型就是内容的形式,比如图文、下载、视频、图片等。右面是扩展。扩展和类型是一一对应的。

这就形成了一个“骨架”,骨架是以“内容”为中心,ArticleID 作为关联字段,可以增加扩展表,但是都要以ArticleID作为关联字段。至于有多少扩展表,那就可 以根据实际需求来变化,表里的字段也是可以根据需求来增减。

设置这种“骨架”的好处:虽然扩展表、字段会有变化,但是“骨架”结构是不变的。这样一是可以让结构清晰,抓住中心、重点;二是当需求变化的时 候,对结构的影响降到最低;三是,如果对于这种“骨架”习惯、掌握了之后,在看到其他项目的设计就会很容易进入和读懂。关于第三点,以后大家就会理解的。

基本思路就是这样,抛砖引玉了。

ps:CMS的字段说明

表编号 字段编号 字段名 中文名 类型 大小 默认值 允许空 说明
5000 0 CMS_Channel 网站栏目
5000 10 ChannelID 主键 int 4 1 0 主键,自增
5000 20 channelName 栏目名称 nvarchar 30 _ 0 栏目名称
5000 30 Sort 排序 int 4 10 0 小号在前
5000 40 URL 栏目的网址 nvarchar 50 _ 0 新闻内容
5005 0 CMS_ArticleClass 内容的n级分组
5005 10 ClassID 主键 int 4 1 0 主键,自增
5005 20 ChannelID 所属栏目 int 4 0 所属栏目
5005 30 Class 文章分类 nvarchar 30 0 支持n级分类,也可以不分类
5005 40 ParentID 父ID int 4 0 以备n级分类
5010 0 CMS_Article 网站里的内容
5010 10 ArticleID 主键 int 4 1 0 主键,自增
5010 30 ChannelID 所属栏目 int 4 1 0 所属栏目
5010 40 ClassID 分类 int 4 0 0 支持n级分类,也可以不分类
5010 50 TypeID 类型 int 4 0 0 1:新闻;2:下载;3:其他待定
5010 20 Title 标题 nvarchar 30 0 内容的标题
5010 60 Summary 简介 nvarchar 30 0 内容的概要介绍
5010 70 Content 内容 ntext 16 0 图文内容
5010 80 Hits 人气 int 4 0 点击量
5010 90 SearchText 搜索 ntext 16 0 搜索内容
5010 100 AddedDate 添加日期 smalldatetime 4 GetDate() 0 记录添加日期
5010 110 AddedPersonID 添加人 int 4 1 0 添加人
5010 120 UpdatedDate 最后修改日期 smalldatetime 4 GetDate() 0 记录最后修改日期
5010 130 UpdatedPersonID 最后修改人 int 4 1 0 记录哪个人最后修改的
5020 0 CMS_DownLoad 下载
5020 10 DownLoadID 主键 int 4 1 0 主键,自增
5020 20 ArticleID 内容ID int 4 1 0 关联 “内容”表
5020 30 Title 标题 nvarchar 30 0 下载的说明,比如“本地下载”、“博客园下载”
5020 40 DownURL 下载地址 nvarchar 30 0 下载地址,可以是文件地址也可以是网页地址
5020 50 DownCount 下载次数 int 4 1 0 下载/访问次数
5020 60 AddedDate 上传时间 smalldatetime 4 GetDate() 0 上传文件的时间
5020 70 Version 版本 nvarchar 30

[转载]Delphi组件开发教程指南(2)简单扩充TEdit

mikel阅读(1401)

[转载]Delphi组件开发教程指南(2)简单扩充TEdit – 得闲笔记 – 博客园.

上一篇,介绍了组件开发的一般概述,以及组件类层次结构的各个类的大致信息!现在就开始通过实例进行来简单的讲述一下一般组件的开发过程!我这里先采用最 基本的方式来创建一个组件!也就是使用Delphi自己的组件开发向导来建立一个新的组件。在这里,我以扩充TEdit为例进行讲解。

那么首先,要介绍的就是如何使用Delphi的组件开发向导了!这个向导在Delphi IDE的Component菜单下的NewComponent打开就是向导了,可以参照如下:

从图上可以看到,我通过向导,我先选择了从组件Tedit往下继承,于是Delphi自动生成了新组件类TEdit1,然后我选择了将本组件注册到 Standard这个控件面板上去,然后保存了单元文件到C:\Test这个目录下!然后点击下一步完成,delphi自动帮我们生成了一个 Edit1.pas的单元文件。这样一个新的基本控件就完成了!此时,就可以在我们的工程中使用TEdit1.Create(nil)这样的语句来创建刚 刚建立的新Edit组件了,只是现在的这个控件与TEdit没有任何功能上的区别,唯一的却别就是仅仅变了变组件的类名称而已!是一个没有任何意义的组 件!下面,我要开始讲解的便是扩充一下这个Edit,使得这个控件具备一些Delphi的TEdit所不具备的功能!

我想一想,扩充几个啥功能呢!貌似Delphi自己的Tedit没有Alignment这个属性哈,那么第一步,就来先为我们刚刚创建的那个新组件增加一 个Alignment属性,可以让用户来设置Edit中文字的对齐方式吧!怎么能设置Edit编辑框的文字的对齐方式呢!这个网上有很多资料的,我们查查 MSDN,可以发现Edit有几种样式ES_LEFT, ES_RIGHT, ES_CENTER分别用来设置文字的左对齐,右对齐和居中对齐!一般情况下,默认的是左对齐,如果要修改Edit的对齐方式为其他的对齐方式,那么,我 们就需要设置编辑框的样式为新的样式,Windows有一个API函数,专门用于设置其控件的样式的函数为SetWindowLong,如果要在运行时直 接对Delphi的Edit设置对齐方式,就可以用该函数来实现!比如

SetWindowLong(Edit1.Handle,GWL_STYLE,GetWindowLong(Edit1.Handle,GWL_STYLE) or ES_Right);

这一句就能让Edit1的对齐方式变成右对齐,于是,我们可以想象,我们可以在组件内部开放一个属性,用户一设置,就能调用这个代码来执行!所以, 我先在我们刚刚生成的那个单元类TEdit1的Published域中添加上一个新的属性

published
{ Published declarations }
property Alignment: TAlignment;

然后按住Ctrl+Shift+C,Delphi就会自动帮你把属性设置补全!变为

property Alignment: TAlignment read FAlignment write SetAlignment;

此时,有必要来介绍一下Delphi的属性Property,delphi的属性由property关键字定义,后面跟上一个属性名称,以及属性的数据类 型声明,然后跟上属性的读写来源,读写来源采用read和write关键字标记,那么当我们调用属性的数据的时候就会调用属性的读方法,如果向属性中写入 值的时候,就会调用属性对应的写方法或者写的对应变量值!

好,我们看一下Alignment这个属性的声明,read域指定的是FAlignment,这就表示当外部调用Alignment的时候,实际上就是直 接调用的FAlignment的值了,而当我们外部向Alignment属性写入值的时候,那么此时,就是调用的Write域所对应的 SetAlignment方法了!我们现在最主要最关键的就是要实现SetAlignment这个方法。当用户设置了这个属性的时候,就会直接触发改变编 辑框的对齐方式!前面已经给出了代码SetWindowLong可以实现修改Edit的编辑框样式,那么我们这里也就用那个方法来实现一下这个改变对齐方 式的属性

这个代码很简单,我就直接给出来算了

代码

procedure TEdit1.SetAlignment(const Value: TAlignment); var style: DWORD; begin if FAlignment <> Value then begin FAlignment := Value; Style := GetWindowLong(Handle,GWL_STYLE); style := style and (not ES_LEFT) and (not ES_CENTER) and (not ES_RIGHT); case FAlignment of taLeftJustify: SetWindowLong(Handle,GWL_STYLE,style or ES_LEFT); taRightJustify: SetWindowLong(Handle,GWL_STYLE,style or ES_RIGHT); taCenter: SetWindowLong(Handle,GWL_STYLE,style or ES_CENTER); end; Invalidate; end; end;

我在里面写的代码比前面给的代码多了一个
style := style and (not ES_LEFT) and (not ES_CENTER) and (not ES_RIGHT);这样的东西,可以试着思考一下为什么要这样写!

然后在最后还添加了一个Invalidate;可以试着注释掉这个函数,然后试着改变属性,看看会有什么样的效果!为什么会这样?

最后,我再给一个不是用SetWindowLong这个API来实现改变这个属性的效果的方法代码,大家可以先参考参考,至于原理若何,后面再做介 绍!

那便是改写Edit控件的CreateParams方法,先在Protected域中声明

protected

procedure CreateParams(var Params: TCreateParams); override;

之后实现如下

procedure TEdit1.CreateParams(var Params: TCreateParams); const Alignments: array[TAlignment] of DWORD = (ES_LEFT, ES_RIGHT, ES_CENTER); begin inherited CreateParams(Params); with Params do begin ExStyle := Exstyle and not WS_EX_Transparent; Style := Style and not WS_BORDER or Alignments[FAlignment]; end; end;

然后SetAlignment修改为

procedure TEdit1.SetAlignment(const Value: TAlignment); begin if FAlignment <> Value then begin FAlignment := Value; RecreateWnd; end; end;

注意两者SetAlignment的实现方式!然后思考一下!

先看看效果

代码下载!

另外,这里,我并没有将这个控件注册到Delphi的IDE中去!如何注册进去,在以后的指南中,我会说明!

作者:不得闲
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必 须保留此段声明,且在文章页面明显位置给出原
文连接,否则保留追究法律责任的权利。

[转载]Delphi组件开发教程指南(1)组件开发概述

mikel阅读(1051)

[转载]Delphi组件开发教程指南(1)组件开发概述 – 得闲笔记 – 博客园.

在开讲之前,首先,我假设你了解什么是面向对象,什么是类。如果不了解的话,建议先去了解一下相关知识!

在Delphi中,组件,我们可以将它理解为一个个的封装好了的功能对象库,使用这个库,就只用简单的几句代码,甚至仅仅简单的设置几个属性就能实现某些 功能,我们可以将组件想象成日常生活中使用的某些工具,比如说收音机,我们不必了解其内部构造,只用在播放器上点一个收听按钮就能收听广播。其实这不仅仅 是Delphi组件,Delphi的中的各种对象库都是如此,比如TStingList,只用简单几句代码就能实现一些功能,组件与对象库本质相同,唯一 不同的就是组件通过Delphi的IDE接口注册到了IDE中,可以进行可视化的设计,而其他的对象库,需要我们手动创建,然后调用!

上面简单阐述了一下Delphi的组件的某些特性以及与其他对象库的一些区别。下面,咱在说说Delphi的组件的构造层次,说到这个类的层次,有一个非 常有名的Delphi对象层次图的(但是我不晓得在什么地方了,有兴趣的可以自己搜搜看,不看也不影响下面的讲解说明)。上面说了,Delphi的组件其 实也是一个对象库,也就是一个类,在Delphi中所有对象的基类都是TObject,组件的最最最基类也是TObject,这里有一个来源于网络的不完 整的类结构图

这个结构中组件的类层次用黑色粗体标记出来了,也就是

TObject———->TPersistent————>TComponent————->TControl

TControl又继续分支

TPersistent这个类,这个类是一个可持续化的对象库,只要从这个类继承下来的,都自动具备了串行化的功能,可以非常方便的读入与写入到 流,这个特性就方便了组件中的属性信息能非常方便的记录到窗体资源文件。

TComponent这个就是所有Delphi组件的基类,所有想要注册到IDE直接进行可视化设计的对象库,都要从这个类继承。 TComponent提供了必不可少的信息以使组件能够在Delphi的IDE上运作。然后TComponent下衍生出了TControl的类,这个类 就是所有的在运行期间可视控件的基类,比如Label,Panel等,只要从TControl继承下来,就能实现运行期间可视化。如果直接从 TComponent向下继承的话,那么在运行期间就不可见了,比如说TTimer等。

TControl类,从图上可见,从他开始又有了分支了,分了TGraphicControl,TWinControl。先说 TWinControl,TWincontrol就是Windows控件库的基类,这个类封装了大部分Windows的消息响应以及创建参数。再来看 TGraphicControl,这个是个特殊的控件基类,也就是Label,Image等控件库的基类,其实,他本身不算是一个Windows的控件, 而是依托于Windows控件之上的一块区域,所有的消息信息都由他所依托的Windows控件(也就是他的Parent)来响应,然后分派出来,之后他 本身才响应这些消息(比如说,TImage控件创建的一个图片显示控件Image1,它的Parent是Form,那么他的 MouseDown,MouseUp,MouseMove等消息的响应,实际上是当这个图片控件创建了之后,它的Parent就指定了一块区域是属于用来 显示那个图片的,当鼠标在Form上移动,移动到那个区域的时候,就给TImage派发一个MouseMove消息,于是这个消息就响应了,鼠标按下与鼠 标弹起消息也都是如此),从TGraphicControl继承过来的组件,它内部都有Canvas画布属性,其实,这个Canvas在大多数时刻实际上 是用的它的Parent的Canvas,也就是说他的Canvas与GraphicControl.Parent.Canvas是同一个设备场景,那么为 什么是大多数时刻,而不是任何时刻,且听我慢慢说来!其实也是在任意时刻都是Parent的canvas,不过那个任意时刻是有条件的,也就是在那个组件 的实现内部,开放给用户使用的Canvas就未必是Parent的Canvas了,这个最典型的列子就是TImage这个控件了,TImage开放给我们 使用的Canvas实际上是一个TBitmap的Canvas,而其内部的Canvas并未真正开放出来,而仅仅是作为将图片绘制到Parent的设备场 景上使用。所以此时,我郑重指出,TGraphicControl实际上是一个虚拟的界面控件,本控件不具备句柄,切莫在未给它指定Parent的情况 下,试图去使用引起控件重绘等消息的方法,比如Invalidate等。

从TwinControl继承下来的控件,都是具备有控件句柄的,也就是在Windows内部具备有唯一标记,能动态索引找到的。

TScrollingWinControl就是具备有滚动条的,Form就是来源于此。

TcustomControl这个就是通常给咱使用来开发继承新的控件地。

在创建组件之前,我们需要明确,这个组件是可视的组件(运行期间可见)还是非可视组件,如果是可视的组件,那么我们就要从TWincontrol或者 TGraphicControl继承一个新的对象类来实现,如果是非可视的组件,那么我们就从TComponent继承来实现,至于为什么,上面已经说 了。由于在开发组件的时候,各个人的水平层次不一样,然而创建一个新的组件,都需要创建一个新的对象类,所以此时各人根据各人的需求和能力做不同的构造, 能力需求点不一样,创建方式也可能不同,一般新手,建议采用向导,找到菜单中的Component那个菜单,然后选择第一个菜单 NewComponent,就能打开这个新建控件的向导,这个向导中列出来了Delphi的组件库中的所有组件,根据自己的需要选择一个你需要扩充的控件 或者选择TComponent或者TCustomControl创建您的新组件。创建好了之后就能进行组件的扩充与编写了!

休息一下,敬请期待下一篇,简单扩充TEdit

由于个人水平有限,文中,难免有纰漏错误之处,欢迎各位大虾拍砖指正!

组件教 程指南目录

Tcomponent类提供最少而且是必要的性质和事件以使组件能 在IDE上工作

作者:不得闲
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必 须保留此段声明,且在文章页面明显位置给出原
文连接,否则保留追究法律责任的权利。

[原创]我们的电子商务,他们的电子商务

mikel阅读(1061)

所谓我们的电子商务指的是目前大多数人脑袋中根深蒂固的B2B、B2C、C2C、C2B,根深蒂固的原因是淘宝、阿里巴巴、京东、当等各位的营销成果,于是乎大江南北如雨后春笋般造就出大大小小的电子商务不计其数,都在自家的一亩三分地上营销、推广的不亦乐乎,可扪心自问各位盈利了多少?不用说大多是酸甜苦辣都有,网站投入了不少资金、人力、物力,可是成交量没多少,还不够日常人员开销的,于是很多人开始问为什么我们的电子商务网站没订单、客户不买账呢?!

所谓他们的电子商务指的是普通人眼里的电子商务,对于前面说的各位电子商务代表网站的营销给人们留下的印象,哪怕是老头老太太都知道电子商务是网上买东西、卖东西了,但是具体怎么买的、怎么卖的,普通人不知道,也就是我们这些电子商务网站的潜在用户群不知道这电子商务到底怎么回事儿,怎么就通过电脑不看货,就买到东西了呢?这钱是怎么花的?这是普通老百姓也就是B2C中的C的想法;那么这现实中的各位小老板儿们怎么看待电子商务呢,从卖水果的、卖五金的、卖服装的大部分还只知道电子商务不过就是个在网上建个网站,有人看了,就会打电话联系他们讨价还价,具体交易还是得当面谈,不可能在网上全搞定,这电子商务也就是个宣传,跟平常促销,找个复印部印传单发发一个意思。

通过以上的对比,我们也就不难明白,为什么我们的电子商务不赚钱了,那是因为我们总是用那么多的名词和模式一厢情愿的去闷头做的不是他们理解的电子商务!他们根本就没明白什么是电子商务,你那费劲弄了那么一堆东西出来,人家不会用,你不赔钱你还等什么,通过对比我认为大家是该冷静下来抛开“我们的电子商务”去认真听听“他们”的想法的时候了,不了解客户的想法,满目的跟从只能是徒劳无功,当你头脑中真正了解了“他们的电子商务”,并有了一套适合“他们的电子商务”的可行方案,那么我想才真正实现了真正意义的电子商务。

[转载]我的插件框架·前传

mikel阅读(1212)

[转载]我的插件框架·前传 – Zongsoft Corporation – 博客园.

初识

话说很久很久以前,当我还是只菜菜鸟的时候,就说听过关于插件的传说。因为一直都是做富客户端模式的企业应用软件,所以对插件这种神奇的搭积木式的 挂接模式,无限向往之。再后来,听说了有关Eclipse的种种神奇传说,与日中天的名气令其在坊间一度被尊为神器,就连俺这么闭塞的.NET粉丝都能把 它的名字正确拼写出来,由此可见一番,终于在某个风高月夜伸手不见五指的大白天,俺把罪恶的手伸向了她那被万人膜拜的…

首先找了一些Eclipse的插件开发资料,硬着头皮看了些Java的代码后,始终不得要领,大部分的插件开发资料都属于依葫芦画瓢的讲解方式,而 对Eclipse插件架构的整体描述几无涉及,官方的资料中对整体架构的介绍也是粗略含糊,就我这Java门外汉来说,想要通过代码对其有高屋建瓴的理解 还真是不靠谱,苦熬之下也便不了了之。然哥早已不在江湖,却依然流传着Eclipse的传说……

时光荏苒,就在某个不经意的日子,突然晴天一声雷,一个名叫SharpDevelop的家伙半道杀出,在.NET社团掀起一阵骚动,趋之者无数,也 再次撩动起我不甘寂寞的心,于是乎,历史再度上演…

可怜SharpDevelop官方几乎没有提供有什么价值的文档,网上的资料除了一点介绍性的文字外就剩旁边的广告链接了。也罢,直接看源码吧,也 没什么比C#代码更能讲清楚问题的了。SharpDevelop早期0.x版本的代码,坦白说,写得还真是那个啥,跟了跟它的代码,基本流程是清楚了,但 是对一些关键问题的疑惑还是没有解决(唉,看来俺的智商还是没有自诩的那么高啊,鄙视一下),问题主要有下:

1、插件节点(PluginNode)与插件(Plugin)以及代码子(Codon)这三角关系究竟隐藏着啥名堂?

2、Doozer(这个不知道咋翻译了)到底主司何职?它到底对Codon负责了什么?如果说他是Codon的构建器,那为啥 Codon中还要有BuildItem方法,是不是功能重叠?

3、Codon中的BuildItem方法中还包含有ArrayList类型的subItems,难道是父Codon负责子Codon的创建?父创 建子,势必会造成父对子的依赖,如果这样的话,第三方开发人员如果想要基于已有插件进行功能扩展的话,已有插件是不可能了解子Codon的创建行为的,那 么这个subItems参数岂不是鸡肋或摆设?再则,如果父Codon只是调用子Codon的BuildItem方法,由子Codon自行构建,那么父如 何确保以正确的方式将子Codon挂接进插件树中?

4、插件如何获得环境信息?并如何确保扩展插件不会对依赖插件过度耦合?

5、消息通知机制如何处理?如何将插件自身提供的一些公共信息提供给其他插件共享?插件状态发生改变,如何以一种统一并无耦合的方式通知给感兴趣的 其他插件?插件运行时平台如何将事件消息传递给订阅者,在传递过程中,如何以灵活的方式注入进去进行过滤处理?

6、是不是每个Codon都要有对应自己的Doozer?在很多时候,可能需要创建不同的对象,但是这些对象有很大的共性,只是创建过程和后续初始 化有差异,如何使用一个Doozer来做到这点?如果一个插件想提供一个服务给其他插件使用,其他插件是不是只能通过接口引用的方式进行解偶?这样的方 式,耦合度是不是还可以继续降低?

以上是当初的一部分问题,可能描述的还是笼统了,虽然陆续阅读源码,某些问题有了答案,但是对整个架构的理解还是不能很好的串联起来,故而就算能基 于SharpDevelop进行插件开发了,也总有暗箱操作的不安。另外,SharpDevelop中到处充斥着各种条件选择(Condition),尤 其是定义某些复杂Codon的时候,各种各样的Condition缠绕四处很是混乱不干净,当时虽说不出具体有啥特别不好,但却总觉有种坏味道隐隐围绕, 是不是应该有更好方式来解决Codon按条件进行挂载的问题呢?

带着这些问题,断断续续的纠结了一阵,我的惰性最终在时间的飞逝中发挥出它应有贡献,而江湖的风骚过后大家该吃吃该喝喝,歌舞升平依旧,社会一片和 谐,总之形势大好。

起因

2008年上,经朋友介绍去到一家Mini型软件公司负责一个管理软件的产品开发,当初该老板介绍的前景不错,待遇也可以,俺屁颠屁颠的带上吃饭的 家伙就杀过去了。由于对这个行业不了解以及过度自信,当初也跟着老板喊了不少口号、放了不少卫星(汗一个)。还不错,手下有两个程序员 可以使唤(其中一个是从ACCP带过去的学生,小伙非常优秀,2009年底已经出来自己创业了,他们的Ideal非常棒,祝福他们), 就这样,我这个技术架构师兼项目经理、主力程序员就这样荣耀登场了。

正如您的高见,我本敲代码者,很多时候不自觉就追求设计的完美去了,等绕了半天之后,才发现原来自己还是个项目经理,还有开发周期在候着呢,更杯具 的是,还得去收回当初信誓旦旦跟老板一起放出的那些卫星。通过这个事情,终于切身的体会到一个问题,如果没有制度上的制约,监督者与执行者又是一家人的 话,混乱与腐败就是必然的了(呵呵,和谐社会请勿过度联想)。OK,非技术问题在此就不深入展开了,如果是PLMM,则欢迎来电来函来家,彻夜深入探讨, 其他者请绕行。

在这个产品中,有个需求:不同的用户登录后,操作界面必须根据其角色进行自动调配,白话说就是用户登录后,如果其是收银员,那么那就只能操作收银窗 口及其只能看到或者使用相关的一些功能窗口的入口菜单或者导航树节点。另外,该产品分为不同的发布版本,有精简版、标准版、超级宇宙无敌奥特曼版等等,那 么在这些各种版本的发布中包含的功能窗口和模块是不一样的,也就是说,同一种东西要切成不同部分去兜售。看到这里,您或许已经告诉我该怎么做了:在登录成 功后的事件中写代码去隐藏相关菜单、导航树节点、主窗口的图形按钮等入口元素,将不同的模块放在不同的Library项目中,发布的相关版本引用相关程序 集即可。得幸,当初在下也是这么想的,但是、但是,依咱哥们的秉性,这种傻大笨粗的搞法实在太对不起架构师这个亮闪闪的头衔和Boss当初那饱含热泪殷切 期望的眼神了,再怎么地,也得想个稍微对得起咱名声的搞法,嗯,嗯,那个项目周期先缓缓,容俺想想,很快就好的了…

彗星撞地球,果然灵光一闪,插件、全面插件化的架构绝对是彻底、一劳永逸解决这个问题的根本性的宇宙无敌之方案!!!而且,它还可以带来很多别的好 处…譬如1、2、3、4… 我总得先说服自己吧.. 嘿嘿,既然这么多好处,那就赶快搞吧。嗯,嗯,那个项目周期,可以再探讨的嘛,这个东西的价值是非常可观的,而且还可以为今后公司所有的产品服务,虽然现 在还不知道其他产品在哪,但公司在迅猛发展总会有的噻…

初试

既然已经决定要搞,那么怎么个搞法就得先定下来,兵先阀谋还是知道的。做法有二:1、基于SharpDevelop的插件框架来做;2、自己设计开 发。

如果采用策略一,首先得把SharpDevelop的核心插件库剥离出来窃为己用,但是必须确保对SharpDevelop有深入的掌控,对其设计 思想和架构模式做到了然于胸,以免发生后期无法实现的想法或出现解决不了的Bugs。诚然,要做到如上要求,短期内怕是不太可行,而且有过前些年看其源码 的惨痛经历,如今已是心有戚戚焉。

还是采用策略二吧,但首先面临的问题就是,从头搭建一个框架无论是时间成本还是技术风险都是难以预估的,但凡有点良知的兄弟都能看出来,俺要真这么 干,就有点利令智昏不知死活了,基本可以拉出去枪毙五分钟。嘿嘿,所幸俺还是能在激动关头保持最后一点理智的,再三权衡,最终决定先做个能满足目前需求的 这样一个界面层自动适应功能的类库出来,其他伟大的想法等日后择机再搞吧。

要满足将功能入口点动态挂载到主窗口中去,必须得定义一个配置文件,这个文件起码需要描述挂入的类型(如菜单、导航树、主窗口的桌面快捷图标)、目 标窗口的类型、挂载点的显示文本、图标等,不同的角色需要有不同的配置方案,那么可能还要包含一个对挂载配置进行选择的机制。嗯,差不多先就这些了,呵 呵,至此俺的想法还是这么纯朴,真为自己骄傲,没有引入其他脱离实际的高调想法进来,一切看起来都在控制之中,不错不错,暗爽一下先。

好了,就从这个配置文件开始吧,有了它,思路就会一步步清晰起来的,顺着这个思路往下理,后面的实现部分就快了。看起来是这个样子:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<section name="themeManager" type="Zongsoft.Applications.Configuration.ThemeManagerSection, Zongsoft.Applications" />
</configSections>
<themeManager>
<theme name="" resourceName="" resourceAssembly="">
<menu name="BaseMenu" style="Menu" generatorType="Zogsoft.Applications.Themes.ThemeMenuElementGenerator, Zongsoft.Applications">
<menuItem name="" text="" icon="" toolTip="" shortcut="" commandType="Zongsoft.Applications.WinForms.ShowFormCommand, Zongsoft.Applications">
<properties>
<add name="Command.TargetType" type="System.String" value="Zongsoft.AMS.Basic.FTimeSpan, Zongsoft.AMS" />
<add name="Command.ShowDialog" type="System.Boolean" value="False" />
<add name="Command.Singleton" type="System.Boolean" value="True" />
<add name="Constructor.ModuleName" type="System.String" value="" />
<add name="Constructor.DefaultDateTimePart" type="Zongsoft.AMS.Common.DateTimePart, Zongsoft.AMS.Common" value="Time" />
<add name="CustomProperty" type="System.Object" value="" />
</properties>
</menuItem>
</menu>
<menu name="Navigator" style="Navigator" generatorType="Zogsoft.Applications.Themes.ThemeMenuGenerator, Zongsoft.Applications">
<menuItem name="">...</navigate>
</menu>
</theme>
</themeManager>
</configuration>

上述配置文件可包含多个主题,主题下面是具体菜单和菜单项,不同的菜单由style属性指定其对应的是菜单还是导航树抑或别的什么,在点击菜单或者 导航树的时候,通过菜单项的扩展属性集里面的Command.TargetType项指定的类型来创建对应的对象,可能不同的目标类型需要使用特定的构造 函数签名进行初始化,那么通过名称以“Constructor”打头的扩展属性项来指定构造函数的参数定义,当然,如果觉得这些还不够用,可以自定义一个 Generator类来解析这些配置内容,将该自定义的Generator类型名称放在菜单元素的generatorType属性中指定即可。需要定义的 类,大致如下:

public abstract class GeneratorBase
{
//初始化生成器,必须指定其对应 的
protected GeneratorBase(Theme theme)
{
this.Theme = theme;
/* ... more ... */
}
public Theme Theme
{
get;
private set;
}
//根据菜单元素生成对饮的菜单或 者树型节点之类的目标对象
public abstract object Generate(MenuElement element);
}
public class Theme
{
//该主题对象对应 的<theme>配置元素
private ThemeElement _element;
internal Theme(ThemeManager manager, ThemeElement element)
{
this.Manager = manager;
_element = element;
/* ... more ... */
}
public ThemeManager Manager
{
get;
private set;
}
//表示通过 GeneratorBase的生成方法后激发,可以在外部进行其他附加处理
public event EventHandler<ItemGeneratedEventArgs> Generated;
//调用GeneratorBase的Generate方法生成目标对象,并将该目标对象与caller参数指定的容器进行粘合
public object Build(object caller, string menuName);
//对Build方法的复合处理
public object[] BuildAll(object caller);
}
public class ThemeManager
{
//根据配置节创建Theme类, 并初始化资源服务
public void Initialize(ThemeManagerSection section);
//根据指定的资源项名称获取对应 的资源对象
public object GetResourceObject(string name);
//同上方法的泛型版本
public T GetResourceObject<T>(string name);
//获取主题对象集合
public ThemeCollection Themes
{
get;
}
}</theme>

就这么三个核心类,基本可以应付目前这种需求了,代码也比较简单,就不再叽歪了。

插曲

.NET社团的老大,就是那个有点微微软的家伙,总喜欢在我们为自己取得一些积累的时候,突然抛一个新玩意出来刺激下我们衰弱的神经,顺带消灭我们 那些颇为自得的成绩,而社团内的众多粉丝又都是一帮热衷自残的家伙,于是乎,大家铺天盖日的一番跟风猛跑,杯具的是,俺也是这帮家伙中的积极分子。

就在.NET 3.5发布之际,俺盯上了那个传说中叫作 System.Addin 的美女,不关三七二十八呼啦冲上去就是一阵蹂躏。唉,传说与现实往往是相对的,又或许别人眼中的美女跟自己根本就是阻抗严重不匹配吧,总之,经过俺一番彻 底细致的摆弄之后,发现这个东东压根不是俺臆想中的样子,这使得我早已疲惫的心再次惨遭无情痛击。

在 MAF(System.Addin)中,插件与插件宿主之间需要通过严谨的Contract来进行阻抗匹配,宿主与插件内部都有各自的视图用以隔离实现与 通讯协定,而各自的视图再通过对应的适配器与之协定进行转换。这个高度繁琐的模型就是为了确保插件与宿主之间阻抗的正确匹配,还可以在不影响宿主或外接程 序的情况下更改适配器和协定。想法是好的,设计是糟糕的,在没有亲手写代码去实验的情况下,要想把这么绕的理念整明白,还是有些难度的,为了搞清楚这到底 是怎么回事,俺只好先身体力行的按照MSDN上的指引,一步步把Demo重做了一遍,虽然Demo期间我无数次的问候过一些人,但还是咬牙坚持了下来,一 个小Demo总共需要7个项目,就算除掉Host之外也还有6个,如果你做完Demo后还能神志清醒的淡定高呼一声“Microsoft”的话,那么恭喜 你,本年度最佳盖子奖非您莫属。

一不小心,咱也奔三了,梦想中的插件框架一直萦绕心头已成为挥之不去的梦魇。这期间又做过一些项目,对俺上次做的那个简单的主题类库也进行了一番升 级改造,但是,底层设计思想决定了高层结构,胚子太丑再怎么拾辍始终还是装不出美女来,不进行基因变异,是没有出路的。

前戏

老天爷突然睡醒,一摆手丢了个机会下来,不偏不倚正好砸在白天没啥鸟事,晚上鸟没啥事的本少爷头上 ——嘿嘿,有个哥们找我做个富客户端的企业应用项目。赶紧把俺那套混饭的家伙拾掇拾掇开工干吧,一切按部就班,也没啥叛逆的想法就盼着早点把东西做完交活 收钱,然天有不测风云,人有发癫打狂,就在一切顺风顺水的时候,突然地,看着那个主题类库就莫名开始抽经,越看越郁闷,越郁闷就越想废掉它。这次,这个项 目时间非常宽松,基本任由着我的性子来,怎么办?要不要搞?到底是要还是要?!呵呵,以前那么多闲淡时间因为没有做项目,就没觉着这个东西还是那么重要, 这次切肤之不爽,再不彻底把她整服帖了,以后还是不好混啊,而且MAF这个不靠谱的家伙也让我彻底死心了。

承蒙天意,皇恩浩荡,俺在历经一阵痛苦的抽搐之后,终于痛下决心!某位非著名人士有云:人生的重大事件往往都是短短几分钟内决定的。

把玩Unity

根据以往搭建那个主题类库的经验,我首先面临一个对象动态创建和依赖注入的问题,因为各种组件类必须动态创建,这个问题好办,一般使用反射 (Reflection)就可以轻松拿下,但是当类之间存在彼此依赖的话,那么这个问题就变得不太好办了,而且在面临多种注入方式(构造子注入、 属性注入、方法注入),甚至在构造出对象后,还需要自动调用一些非注入性的初始化方法或者响应处理一些事件的话,这个问题就变得相当棘手了。所 幸我对Unity这位仁兄略有耳闻,知道他能够帮我处理这些非插件框架核心但却是基本功能的问题,不过对于如何使用Unity与插件框架进行整合,我还没 有清晰的思路,更让我抑郁的是,至今还没听说过SharpDevelop或者Eclipse中集成相关IoC容器的做法,我不确定我这个想法的方向是否正 确,而且以前从SharpDevelop的源码中也没看到过类似这么复杂的创建或依赖注入的代码。怀疑终归是怀疑,我还是需要一点论证,至少我需要仔细了 解下Unity的内部构造,起码也要看看能不能从另外一个角度帮我理清下思路。

先上博客园找了一些介绍Unity的文章做到心中有数,一番风卷残云般的快速阅读,大致理念弄清楚了。再从Unity的官方网站下载最新源码,对照 一些朋友写的剖析Unity的文章(看来跟我一样喜欢扒源码的窥窃男不在少数哈),一边看源码一边对资料,对着对着发现路子不对,因为 这些兄弟写的文章基本是依照老版本写的,本着从高从新的伟大理念,也懒得去找老版源码了,就这么凑合看吧,基本把ObjectBuilder的东西看了一 遍,大致意思是明白了,主要是Strategies指使Policies去干活,由Lifetime负责管控对象的生命周期和提供容器,一些细节如定位查 找等交由Locator去打理,ObjectBuilder只是个对外的前台小妹。UnityContainer内部把对象创建和管理的工作都交给 ObjectBuilder处理,自己专注其他辅助功能,譬如配置管理、注入依赖的关联操作并由此引申出来的一大坨杂七杂八的事项。看到这里,基本也没兴 趣深挖下去了,因为跟我想要的插件思想没有关系,看来这位仁兄不能带给我额外的思路,而且,如果一个插件框架需要包含一个这么复杂的构建容器是不是有些本 末倒置之嫌,这不符合我历来讲究的精巧优雅的架构原则,嗯,可以把它排除了。

细探Addin

锁定SharpDevelop,不把她拿下绝不睡觉!

老套路,还是从她的官方网站(http://www.icsharpcode.net/opensource/sd/) 下载最新源码,乖乖,现在都3.2版本了,已经长成大姑娘了,嘿嘿~ 文档还是一如既往的精炼,官方论坛的一些零星讨论不能解决我的大问题,只好把那本已经压在床脚多年的《C#软件项目开发全程剖析——全面透视 SharpDevelop软件的开发内幕》(http://www.china-pub.com/13944) 一书翻出来,在杀死我一些脑细胞之后,除了令我比以前更抑郁外,就是使我更加怀疑自己到底有没有学过中文,这些年说得是不是火星语。

熟练的扒掉她的外套,仔细阅读源码,这次汲取上次的失败,不再从StartUp项目入手而是打开她的AddIn-Scout工具,高瞻远瞩的从整体 结构布局入手,一边用该工具查看插件结构,一边对照ICSharpCode.SharpDevelop.addin源文件,一点一点的摸索她的规律,再结 合这些年自己断断续续的一些所思所得去推敲她为什么要有一个这样的结构,这些结构究竟是为了解决什么问题,她的核心思想是什么。在深夜里俺不怀好意的死盯 她的内部结构,时而跟踪看看类的定义,时而看看对应的Addin文件定义项,时而停下来思考下为什么的问题,终于在自虐了数个不眠之夜、耗光多包‘红色利 群’和槟榔后,被俺想通了几个关键问题,譬如那个非著名的暧昧三角关系以及这些内部构件之间的协同作战问题,同时也想明白了为什么插件架构不需要使用像 Unity这样的专业IoC框架来处理构件的创建和依赖注入的问题。

主题

前面调戏了这么久,再不切入主题就该挨埋怨了。感谢各位东西方仙界朋友的保佑,费了九牛二虎之力把那些关键问题想通之后,再不果断下手就不只是挨埋 怨那么简单了,让人怀疑能力有问题那才真麻烦呢。

开搞

在本文的「初识」部分谈到过我对SharpDevelop的一些不爽之处,除此之外还有个令我一直如鲠在喉的不快,那就是对Codon和 Doozer这些东西的命名甚感纠结,虽然SharpDevelop开发者在书中也谈到过这个命名问题,但我还是认为这些名字太过生僻。一个好的命名会让 人望文生义,这也是我得到偏执者名号的缘由之一。所以,我慎重决定重新厘清这些庞杂系统的概念一并更新命名。首先附上主要的概念性名字解释(含英文名):

  • 插件(Plugin):该英文单词有Plugin和PlugIn的两种写法,前种写法表专有名词,后种为词组,为了照顾编码时书写的整洁 度以及.NET通用的命名规则,而采用Plugin写法。插件是插件框架中的逻辑组织单位,可以对应到一个或多个程序集(Assembly),每个插件必 须对应一个XML格式的插件定义文件(*.plugin),一个插件由众多构件(Builtin)组成,插件定义文件详细描述该插件内各个构件的组合关 系。
  • 构件(Builtin):英文Built-in原意为“内置件、零件”,在此将该词组进行合并引申为构件、 部件之意,这个意思非常符合其在插件框架中的角色定位,该元素完全等同于SharpDevelop中的Codon。构件为插件(Plugin)的组成部 分,其与插件的关系就好比人体器官(如心脏、头、四肢)与人体,其不能脱离插件独立存在,也是基本功能的具体载体,众构件(Builtins)必须通过插 件定义文件将其发布到插件框架中。
  • 插件树(PluginTree):该英文为词组,遵循.NET开发规范对类 名、方法名、属性名等一律使用Pascal命名规范,下同。首先必须高调澄清一个事实,插件树并不是对插件进行数据结构定义的概念,虽然我在这个地方也一 度陷入SharpDevelop中的泥潭难以自拔。插件树是对构件(Builtin)对象在内存中的一种树型数据结构的定义,之所以要称之为 PluginTree而不是“BuiltinTree”,那是因为它不只是负责管理某一个插件内的所有构件定义,而是全局唯一的,要负责管理整个运行时所 有插件内的构件。
  • 插件树节点(PluginTreeNode):顾名思义这个元素描述插件树中的某个节点的数据 结构,主要包含两个构成:1、该节点在插件树中所处的路径(Path);2、该节点对应的构件(Builtin)。一个节点只对应一个构件对象,通过它我 们可以在插件树中任意导航。
  • 构建器(Builder):注意区分该中文名词与Builtin的中文名中的“件” 和“建”字。该概念大致类似于SharpDevelop中的Doozer,它的唯一功能就是根据插件框架提供给它的构件对象生成真实具体干实事的目标对 象。正是因为这个东西的存在,插件框架才能彻底摆脱对庞杂的IoC框架的依赖,因为它非常清楚它要构建的对象是什么。但是,我当初的一个主要疑惑就是当它 要构建对象的构造函数参数需要引用插件平台的一些公共服务,譬如当前的登录用户名、或者数据访问服务接口对象,或者该参数还需要引用另外一个插件中的构件 对象实例以及这个构件实例中的某个属性值,那么这个构建器如何以一种简单优雅的方式去获得这些依赖对象?再次感谢万能的主,因为插件架构的这种树型数据结 构,导致了她天然就有解决这个棘手问题的良方,而且是以一种异常优美简洁的方式来实现的,呵呵,稳住稳住,注意控制好节奏,想想美妙的序曲才刚开始,后面 还有高潮部分在等着咱呢。
  • 类型(Type):该元素是本人在设计该插件框架时思忖许久决定引入的一个新概念,跟我设计数据访问框架一样,我的一个原则就是尽量利用.NET中 现有或者业界主流设计思想中的概念,非万不得已才引入额外的设计元素,将易用性和广泛适用性原则始终摆在框架设计的最高级别。该元素是为了解决构建器创建 构件对应的目标对象时,需要使用到对一些非插件框架服务以及其他插件所能提供的常量型数据的问题。该元素只作为构件的子元素出现在插件定 义文件中,是对构件目标类型的一种补充描述。

    好吧,就算可以引入该元素,但我还是要找到能100%说服自己有绝对充分的理由和必要。为什么SharpDevelop中没有包含类似的概念?经过 我的一番努力查找源码,发现SharpDevelop这种应用主要是处理开发工具这种需求,在这种应用中基本没有在企业管理软件中一个窗体在构建过程中需 要使用一些固定常量的情况,OK,这个理由不够说服力,因为我无法从根本上枚举出所有的需求情景,那么理由二:对于这种简单常量的初始化参数如果也使用构 建器中描述的那种依赖注入方式,明显有高射炮打蚊子之嫌,而且对于插件的实现者而言,明显会增加不必要的开发量,更严重的是,它会破坏我一直提倡的简洁优 雅的设计和使用理念。呵呵,这个理由足够充分了罢,而且增加这个概念之后并不会给系统带来额外的复杂性和臃肿感,无论对于普通插件开发人员还是框架实现者 本人都不会造成额外的工作量,相反它只是简化插件开发的工作并使之更易于理解和配置。

  • 类型别名(TypeAlias):在构 件定义过程中,经常需要指定一些没有挂入插件框架的类型(如:System.Windows.Forms.Form, System.Windows.Forms),如果不幸这些类型又需要频繁使用的话,那么在每个地方都重写一遍,无疑是劳民伤财的,与朝廷提倡的建设低碳 节能型社会是背道而驰滴,是不与时俱进滴,是没有充分理解带三个表滴,是反XXX…. (汗) 所以,我就让这个概念华丽登场了,应该不会有不知时务者反对吧,呵呵,河蟹好河蟹好。
  • 解析器(Parser):这个对应到 SharpDevelop中的解析器类,但是处理手法上与SharpDevelop不同,我把这部分的功能单独出来别于普通构件,让其专司解析之职。它只 有一个重要职责:负责将插件定义文件中的某些构件的设置属性的文本解析成它真正需要的东西。听来有点绕口,举例:假如某菜单项构件在插件定义文件中的定义 如:
    <menuItem name="Print" type="MenuItem"
    text="{res:PrintLabel}"
    icon="{res:Icons.16x16.Print}"
    shortcut="Control|P"
    command="/Workspace/Commands/PrintCommand" />

    看 到text和icon特性(XML元素的Attribute)中的那些{res:xxxxxxx}字符么咯?Good,恭喜你答对 了,Parser就是负责解析这些字符串的家伙,它会返回一些别的什么玩意给构件(Builtin)对象,其中的res:部分就是指定用哪个解析器来解析 冒号(:)后面的内容。

以上较为详细的介绍了本插件框架中涉及的几个重要核心概念,需要特别指出的是,所有这些元素都是生而平等的,他们在插件环境中都占有同等地位的位 置,虽然概念和职责各不相同,但都是为人民服务,身为它们的God,俺是绝对不允许这些家伙中有歧视存在,Never!每个插件结构都可以通过插件树查找 到这些元素,并随时为您服务。

不妙,有情况

在此,还是忍不住要批评一下SharpDevelop这位国际友人,虽然她如此无私的将她的Body任由我多番蹂躏,也曾陪伴我走过很多个落寞的夜 晚,曾让我时而抑郁、时而激昂,时而欢欣、时而捶胸顿足,但不管怎样她都始终一如既往的陪伴着我欢乐悲喜的一路成长。

不幸在一些主要类中看到这样的代码:

static AddInTree()
{
doozers.Add("Class", new ClassDoozer());
doozers.Add("FileFilter", new FileFilterDoozer());
doozers.Add("String", new StringDoozer());
doozers.Add("Icon", new IconDoozer());
doozers.Add("MenuItem", new MenuItemDoozer());
doozers.Add("ToolbarItem", new ToolbarItemDoozer());
doozers.Add("Include", new IncludeDoozer());
conditionEvaluators.Add("Compare", new CompareConditionEvaluator());
conditionEvaluators.Add("Ownerstate", new OwnerStateConditionEvaluator());
ApplicationStateInfoService.RegisterStateGetter("Installed 3rd party AddIns", GetInstalledThirdPartyAddInsListAsString);
}

这披露了一个性质非常严重的事件,这个 AddInTree 类是SharpDevelop插件框架的核心类,在其静态构造函数中硬编码对某些特殊Doozer和ConditionEvaluator进行初始化动 作,这本身就有悖于所有插件结构均一视同仁的插件化理念,正是因为这些少数的特殊化分子导致了后面的插件应用有样学样的一通胡搞。这个问题,我必须得严肃 反应一下,虽然我不否认这些个Doozer在整个框架中的重要性,但是就算其功能再重要也不能脱离插件平台的法则约束,所谓法则就是我上面提到的“所 有插件结构在平台中都具有生而平等的不容侵犯的插件权”,就算某个元素告诉我他掌管着诸多构件的生杀大权,那依然得遵守‘天赋插权’的 法则,总统都还得轮着坐呢。难怪我先前在Addin-Scout工具中怎么都不能在插件树中找到这些Doozer的位置,感情都跑到这里潜规则来了。

再来围观一下 WorkbenchSingleton 类中的 InitializeWorkbench() 方法

public static void InitializeWorkbench(IWorkbench workbench, IWorkbenchLayout layout)
{
WorkbenchSingleton.workbench = workbench;
DisplayBindingService.InitializeService();
LayoutConfiguration.LoadLayoutConfiguration();
FileService.InitializeService();
StatusBarService.Initialize();
DomHostCallback.Register(); // must be called after StatusBarService.Initialize()
ParserService.InitializeParserService();
Bookmarks.BookmarkManager.Initialize();
Project.CustomToolsService.Initialize();
Project.BuildModifiedProjectsOnlyService.Initialize();
/* ... more ... */
}

还有 CoreStartup 类中的 StartCoreServices() 方法

public void StartCoreServices()
{
if (configDirectory == null)
configDirectory = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
applicationName);
PropertyService.InitializeService(configDirectory,
dataDirectory ?? Path.Combine(FileUtility.ApplicationRootPath, "data"),
propertiesName);
PropertyService.Load();
ResourceService.InitializeService(FileUtility.Combine(PropertyService.DataDirectory, "resources"));
StringParser.Properties["AppName"] = applicationName;
}

这些特殊分子的榜样力量由此可见一番,其他方面的影响就不一而足了。整个事件给我的感受就有如:当酣畅淋漓正准备放开拳脚大干一番的时候,突然发现 某种介质上出现了个洞洞,大煞风景之事,轻者造成一次机体伤害,严重的话,嘿嘿,为此而因噎废食就搞大了。

高潮

主题切入这么久了,按理也应该到大家期盼已久的高潮部分了,但是很遗憾,不是每次都能有的,虽然我也很想在这篇文章中,把我设计的插件定义文件作为 高潮的产物奉献给大家。无奈,身心俱惫,已经连续苦熬了两个通宵,就俺这个年纪而言,已实属不易。

当自己真正开始插件框架的开发后,里面非常多的细节问题开始逐一浮现,然本文无法一一介绍到,但是,挑战总与成就感结伴同行,上帝与魔鬼将伴随我此 后一段相当长的日子,我想这次真的会坚持下去,起码我已能隐约感受到黎明时的温暖。

尾声

与日方长,不争一朝一夕,有意者请恕半老不小生不道德的先撤了,后会无期…… bye-bye


作者:钟峰(Popeye Zhong)目前是 北大青鸟(深圳中青)培训中心 的ACCP讲师,负责讲授.NET和SQL数据库开发课程。他曾经使用 C 语言做过图形程序设计,在相当长的一段时期内从事 COM/COM+ 组件的开发和设计工作,并且短暂的做过 Lotus/Notes 和 Dialogic 语音卡程序的开发,从2003年初开始使用.NET这个充满趣味和挑战的开发平台,还领导过.NET平台下的 Windows Mobile 几个项目的开发,对WinForm和WebForm均比较熟悉。感兴趣的除了企业应用架构设计、组件开发、安全、图像处理外还对汽车和枪械模型、边境牧羊 犬有浓厚的兴趣。如果希望与他联系,可访问 http://www.cnblogs.com/sw515 或者Email Zongsoft # gmail.com (将#换成@)

[转载]面向对象思想的头脑风暴(二)—— 详解继承与组合的优缺点

mikel阅读(964)

[转载]面向对象思想的头脑风暴(二)—— 详解继承与组合的优缺点 – 你听海是不是在笑 – 博客园.

组合与继承都是提高代码可重用性的手段。在设计对象模型时,可以按照语义来识别类之间的组合关系和继承关系。在有些情况下,采用组合关系或者继承关 系能完成同样的任务,组合和继承存在着对应关系:组合中的整体类和继承中的子类对应,组合中的局部类和继承中的父类对应,如下图:

组合:继承:

一、基础知识

我们先用代码帮大家来理解一下组合和继承:

1、对于已经存在Parent类时需要扩展其方法时

结构图:

继承代码:

代码

class Parent
{
public void Method1()
{
}
public void Method2() { }
public void Method3() { }
}
class Child1:Parent
{
public void MethodA() { }
}
class Child2:Parent
{
public void MethodA() { }
}

组合代码:

代码

class ComponentA
{
Parent p
= new Parent();
public void Method2()
{
p.Method2();
}
public void MethodA()
{
}
}
class ComponentB
{
Parent p
= new Parent();
public void Method2() {
p.Method2();
}
public void MethodB()
{
}
}

2、如果发现两个类具有很多代码相同的类需要抽象时,如下图A,B两个类,这两个类中method1,和method3两个方法代码相同

继承:

实现代码:

代码

class A:C
{
public void MethodB() { }
public override void Method2()
{

}
}

class B:C
{
public override void Method2()
{

}

public void MethodB() { }
}
class C
{
public void Method1() { }
public virtual void Method2() { }
public void Method3() { }
}

组合:

实现代码:

代码

class A
{
public void MethodA() { }
C c
= new C();
public void Method1()
{
c.Method1();
}
public void Method2() { }

}

class B
{
public void MethodB() { }
public void Method2() { }
}
class C
{
public void Method1() { }
public void Method2() { }
public void Method3() { }
}

二、继承与组合的优缺点

合 关 系 继 承 关 系
优点:不破坏封装,整体类与局部类之间松耦合,彼此相对独立 缺点:破坏封装,子类与父类之间紧密耦合,子类依赖于父类的实现,子类缺乏独立性
优点:具有较好的可扩展性 缺点:支持扩展,但是往往以增加系统结构的复杂度为代价
优点:支持动态组合。在运行时,整体对象可以选择不同类型的局部对象 缺点:不支持动态继承。在运行时,子类无法选择不同的父类
优点:整体类可以对局部类进行包装,封装局部类的接口,提供新的接口 缺点:子类不能改变父类的接口
缺点:整体类不能自动获得和局部类同样的接口 优点:子类能自动继承父类的接口
缺点:创建整体类的对象时,需要创建所有局部类的对象 优点:创建子类的对象时,无须创建父类的对象

1、为什么继承破坏封装性:

鸭子中不想要“飞”的方法,但因为 继承无法封装这个无用的“飞”方法

2、为什么继承紧耦合:

当作为父类的BaseTable感觉Insert这个名字不合适时,如果希望将其修改成Create方法,那使用了子类对象Insert方法将会编译出错,可能你会觉得 这改起来还算容易,因为有重构工具一下子就好了并且编译错误改起来很容易。但如果BaseTable和子类在不同的程序集中,维护的人 员不同,BaseTable程序集升级,那本来能用的代码忽然不能用了,这还是很难让人接受的

3、为什么继承扩展起来 比较复杂

当图书和数码的算税方式 和数码产品一样时,而消费类产品的算税方式是另一样时,如果采用继承方案可能会演变成如下方式:

这样如果产品继续增加, 算税方式继续增加,那继承的层次会非常复杂,而且很难控制,而使用组合就能很好的解决这个问题,参见:面向对象思想的头脑风暴(一)

4、继承不能支持动态继 承

这个其实很好理解,因为 继承是编译期就决定下来的,无法在运行时改变,如3例中,如果用户需要根据当地的情况选择计税方式,使用继承就解决不了,而使用组合结合反射就能很好的解 决。

5、为什么继承 子类不 能改变父类接口

如2中的图

子类中觉得Insert方法不合适,希望使用Create方法,因为继承的原因无法改变

三、如何使用继承

1、精心设计专门用于被继承的类, 继承树的抽象层应该比较稳定,一般不要多于三层。

2、对于不是专门用于被继承的类, 禁止其被继承。

3、优先考虑用组合关系来提高代码 的可重用性。

4、 子类是一种特殊的类型,而不只是父类的一个角色

5、 子类扩展,而不是覆盖或者使父类的功能失效

四、组合的缺点:

1、整体类不能自动获得和局部类同样 的接口

如果父类的方法子类中几乎都要暴露出 去,这时可能会觉得使用组合很不方便,使用继承似乎更简单方便。但从另一个角度讲,实际上也许子类中并不需要暴露这些方法,客户端组合应用就可以了。所以 上边推荐不要继承那些不是为了继承而设计的类,一般为了继承而设计的类都是抽象类。

2、创建整体类的对象时,需要创建所 有局部类的对象

这个可能没什么更好的办法,但在实际 应用中并没有多出多少代码。

五、相关原则

1、里氏代换原则(LSP)(以下转 自http://www.cnblogs.com/zhenyulu/articles/36061.html

Liskov Substitution Principle(里氏代换原则):子类型(subtype)必须能够替换它们的基类型。

白马、黑马

反过来的代换不成立
《墨子·小取》说:”娣,美人也,爱娣,非爱美人也……”娣便是妹妹,哥哥喜爱妹妹,是因为两人是兄妹关系,而不是因为 妹妹是个美人。因此,喜爱妹妹不等同于喜爱美人。用面向对象语言描述,美人是基类,妹妹是美人的子类。哥哥作为一个有”喜爱()”方法,接受妹妹作为参 数。那么,这个”喜爱()”方法一般不能接受美人的实例。

下边那个长方形正方形的例子我就不转了,大家可以到上边那个博客地址中了解。

2、合成/聚合复用原则(CARP)(以下转自http://www.cnblogs.com/zhenyulu/articles/36068.html
合 成/聚合复用原则(Composite/Aggregate Reuse Principle或CARP)经常又叫做合成复用原则(Composite Reuse Principle或CRP),就是在一个新的对象里面使用一些已有的对象,使之成为新对象的一部分;新对象通过向这些对象的委派达到复用已有功能的目 的。

简而言之,要尽量使用合成/聚合,尽量不要使用继承。

o Design to interfaces.
o Favor composition over inheritance.
o Find what varies and encapsulate it.
(摘自:Design Patterns Explained)

区分”Has-A”与”Is-A”

“Is-A”是严格的分类学意义上定义,意思是一个类是另一个类的”一种”。而”Has-A”则不同,它表示某一个角色具有某一项责任。

导致错误的使用继承而不是合成/聚合的一个常见的原因是错误的把”Has-A”当作”Is-A”。

例如:

实际上,雇员、经理、学生描述的是一种角色,比如一个人是”经理”必然是”雇员”,另外一个人可能是”学生雇员”,在上面的设计中,一个人无法同时 拥有多个角色,是”雇员”就不能再是”学生”了,这显然是不合理的。

错误源于把”角色”的等级结构与”人”的等级结构混淆起来,误把”Has-A”当作”Is-A”。解决办法:

总结:

根据我们前面讲的内容我们可以发现继 承的缺点远远多于优点,尽管继承在学习OOP的过程中得到了大量的强调,但并不意味着应该尽可能地到处使用它。相反,使用它时要特别慎重。只有在清楚知道 继承在所有方法中最有效的前提下,才可考虑它。 继承最大的优点就是扩展简单,但大多数缺点都很致命,但是因为这个扩展简单的优点太明显了,很多人并不深 入思考,所以造成了太多问题,希望这篇文章能引发你一些思考。

参考:

http://blog.csdn.net/Cpp_Java_Man/archive/2006/05/02/705279.aspx

http://blog.csdn.net/zjliu1984/archive/2009/06/26/4299657.aspx

http://www.cnblogs.com/zhenyulu/category/6930.html

http://www.cnblogs.com/bluedy1229/archive/2008/11/19/1286692.html

[转载]你现在的CSS水平处于什么等级?

mikel阅读(779)

[转载]你现在的CSS水平处于什么等级? – Jensen – 博客园.

CSS(层叠样式表),可能看过网站制作教程网基础教程的人都知道大概是什么回事.本文来测一下,你学习CSS现在处于怎么一个阶段或者说处在一个什么等 级.

本文总共将CSS学习者从低到高分为六个等级.快来看看你是在哪个等级.

第0级:CSS?什么CSS,是不是CS啊.那游戏我玩过呀.是个多人游戏呀

CSS? Isn’t that a multiplayer game?

有些人因为在找CS:S(Counter Strike: Source)这款游戏的资讯而进到#CSS网站。不必担心这些人,他们不可能制作太多网页,所以对网络也不会造成多大伤害。

第1级:CSS我知道啊.也用.我偶尔用它来移除连结的底线

CSS Yeah, I use it to remove underlines on links sometimes

和第0级的人不同,这些人学过HTML,也制作过几个简单的网站。他们只有在无法使用HTML制作出某些效果时,才会使用CSS,例如移除连结 的底线或者设定行高。这些人的网站通常规模不大也没什么访客,所以他们也不会对网络造成太大伤害。

第2级:不,我不喜欢div元素;表格好用多了!

No, I don’t like divs; tables are much easier to work with

他们听过使用div元素来设计网页这件事,也花了些时间学习CSS。不过他们很快就放弃了,觉得CSS太难而且支持度不佳,还是宁可用表格排 版。

注意!他们是危险人物!他们在这个领域已经待了一段时间,许多都是网站部门的主管。和他们接触是很重要的,提倡网络标准的人应该多多开导他们,这对 网络的意义非同小可。

第3级:是啊,听说它挺好的,不过我没用它因为……

Yes I’ve heard it’s good, but I can’t use it because of…

这些人虽然知道CSS的优点,不过总是基于某些原因而无法使用它,例如他们可能有第2级的上司,或者他们必须考虑到Netscape 4的使用者。

旧版的浏览器虽然不支持CSS,不过使用者还是可以看见完整的内容。而基于亲和力(accessibility)和易用性 (usability)的好 处,CSS可以为网站带来更多访客。把这些优点告诉第3级的开发人员,即使他们不是决策者,或许多少能对第2级的上司发挥点影响力吧。

第4级:CSS?喔!没错,我用div元素来排版

CSS? Oh! Yes, I use divs for all my layouts

在页面中使用过多的div元素反而是这群人的问题,他们会用#toprightredline或是#r5_c7(表示第5列第7栏)这样的方式 设定div元 素的id。即使可以通过XHTML 1.1的验证,这种排版方式却无法发挥CSS的优势。荧幕朗读软体(screen readers,视障者使用的浏览器)很难解译这些网页,旧版浏览器也会有同样的问题,网页的内容无法完整显示。而不良的class与id命名方式,也对 于版面的修改造成极大不便。

虽然第4级的人制作的网站仍然很糟,不过由于他们很容易接受新的观念,因此对网络造成的伤害较小。许多所见即所得(WYSIWYG)的编辑器制作出 充满div元素的原始码,可能也是误导这群人的原因。幸好这些编辑器已经逐渐改良了,希望这会有助于第4级的人继续往上提升。

第5级:我用CSS来设计,这比表格好多了,因为……

I use CSS for design, it’s better than tables because of…

第5级的人知道CSS的优点并乐于使用,虽然有时会遇到些问题,但并不严重。他们也能运用长期的CSS工作经验,与人辩论为什么要将网页的结构 与设计分开。我猜这篇文章大部分的读者是这一级的CSS开发人员,我想我自己也是。不过这还不是最好的……

第6级:哪个版本的CSS?是的,我知道啊。你有读过我的书关于……

What version of CSS? Yes, I do. Did you read my book about…

第6级的人致力于改良CSS,并且写了许多很棒的文章介绍它的新用法。其中有些人将W3C关于CSS的说明文件全部读完了,并且知道哪些功能在 哪个版本的浏 览器才有支持。他们是CSS初学者的典范,并且运用他们的影响力使网络更加进步。其中有许多人组成了Web Standards Project,如果你在他们的网站上面发现任何错误,那一定是有理由的,向他们请教之后你必能获得满意的解答。

不管你处于哪个等级.只要认识学习.但一定要认清楚.CSS网页布局与表现的分离是一定有好处的.

[转载]OCI编程历程

mikel阅读(1027)

[转载]OCI编程历程 – 一个壮族小伙的技术博客 – 博客园.

前几天和哥们聊天,谈到连接数据库及一些数据库调用接口的开发问题。那哥们直接来了一句:“那东西没什么搞头,就调用些函数,然后做些错误处理”……… 我很是郁闷,回想起来实习时第一个能拿得出手的程序就是对OCI10库,当时为了测试效果一个人在机房里呆了一个多月,每天不停的拔网线。后来还把这个封 装库写在简历中项目经历的第二项。虽然现在看起来封装得有点幼稚,但还不至于一文不值。把我憋个够!想想还是把它写下来吧。
PS:本篇主要对OCI编程进行一些入门介绍,还涉及一些非阻塞和即时客户端配置的内容,所以高手请绕行。本人水平有限可能存在很多错误。

一、问题

为什么要使用OCI?可以使用ODBC对oracle数据库进行连接啊。当然这在widows系统上一点问题都没有,但如果程序需要跨多个不同的平台使用 ODBC就有一定的问题。不是还有unixODBC和iODBC吗?……在unix平台上使用MS推出的东西进行开发还是让我这种转牛角尖的菜鸟程序员难 以接受。

实习时所在的部门一直使用OCI来访问oracle数据库,所有的版本是oci7.3。对!没看错一直使用的就是这个96年的oci版本。确实太老了!这 么多年没有换版本主要是没有人再去维护它,还有勉强可用;另一方面,就像我那哥们讲的那样,大家都觉得这东西没什么可以做的了。

一切事物都逃不过产生、发展和消亡的命运。而在事物消亡的过程中总能找到几个标志性的事件来见证这个过程。部门里使用的oci7.3的接口库也逃不出这个 规律:有一次在给某大领导演示公司产品前十几分钟,所有的演示用工作站都开好,静等领导的到来。这时几个同事在搬动机器的时候不小心把系统中唯一的数据库 服务器的网线碰掉了(单点运行就是不靠谱),导致所有数据库连接的工作站都阻塞了,演示用机像死机似地没有一点反应。这下子把大家忙坏了,赶紧把数据库服 务器的网线插上,重启工作站的对应进程。还好在领导来到的时候,所有演示用机都重新准备好了。这件事加速原有的oci7.3接口库退出历史的舞台。大概在 08年10月,老大给我分配了改造这个oci接口的任务,要求:1.使用新的oci版本,本来想用oci9,但后来发现oci9的一些版本不支持某些特殊 的功能(本文后面会提到),所以后面选用03年出的oci10;2.接口函数尽量兼容原有版本,这样别人更换版本的时候不需要修改太多。当然还要跨平台、 调用过程方便简洁等等等等;3.执行查询语句时,每次获取N行(可以由调用者设定)放入调用者指定的缓冲区直至取完,当不满N行时取实际的行数放入缓冲 区,并返回实际取到的行数;4.与数据库的连接断开后能返回错误,并提够重连机制。这个要求也算是在服务器短时间内不会发生变化的情况下,一种用客户端来 保证稳定性的妥协做法吧。因为能oci接口如果能检测连接断开,那么调用者就可以采取一些处理,比如在本地缓存数据库的操作,或者向使用者发出连接断开的 提示等。5.用业余时间完成。

二、OCI编程的一般过程

与OCI7.3中使用的宿主语言定义变量存储空间(很拗口,不用管它)的方式不同,OCI9以后已不再使用原有的变量结构来初始化及维护数据库的信息,而 改用句柄的形式来和Oracle数据库进行交互。将常用的句柄定义在一个结构内,方便程序维护:

1 typedef struct _OCI_HANDLE
2 {
3 OCIEnv     *phEnv; //环境句柄,要使用oracle数据库, 必须首先获得环境句柄
4 OCISvcCtx  *phService; //oracle的服务句柄,也可以说是连 接句柄。
5 OCIError   *phErr; //oracle的错误句柄,可以获取错误 信息
6 OCIStmt    *phStmt; //oracle的语句描述句柄
7 OCIServer  *phServer; //Oracle 的服务器句柄
8 OCISession *phSession; //Oracle会话句柄
9 }OCIHANDLE, *LPOCIHANDLE;

OCI9编程的一般步骤有:初始化环境句柄、生成其他各类句柄、建立数据库连接进行登录、执行SQL语句,对返回的结果进行处理、终止用户会话,断开连 接,释放各种句柄。

image 上图给出OCI初始化的一个过程,OCI能初始化成功的前提当然是系统中已经安装或设置了Oracle的client端(在本文第四节有介绍)。其中步骤 (3)到(7)都分别调用OCIHandleAlloc()函数进行分配,顺序可以不同,它们都只依赖环境句柄;步骤(1)和(2)可以使用 OCIEnvCreate()函数替换掉,这两种的初始化OCI环境的方法在不同的使用条件下是不同的,一般建议使用OCIEnvCreate()代替 OCIInitialize()和OCIEnvInit(),因为OCIInitialize()和OCIEnvInit()主要是为了 backwards-compatible。而如果是编写DLL更是应该使用OCIEnvCreate()函数,user‘guide是这样说的:

If you are writing a DLL or a shared library using OCI library then this call should definitely be used instead of OCIInitialize() and OCIEnvInit() call.

OCI各句柄初始化完毕后,接下来就是连接数据库,如下图:

image

数据库连接好后可以执行SQL语句:一条SQL语句在OCI应用程序中的执行步骤一般如下:(1)准备SQL语句。(2)在SQL语句中绑定需要输入到 SQL语句中的变量。(3)执行SQL语句。(4)获取SQL中的输出描述。(5)定义输出变量。(6)获取数据。具体过程及过程中调用的函数如下图所 示。对于SQL中的定义语句(如CREATE,DROP)和控制语句(如GRANT,REVOKE),由于没有数据的输入输出,只需要图2中第一步和第三 步即可。操作语句(如INSERT,DELETE,UPDATE)则需要执行前三步。而查询语句(如SELECT)不仅可能有数据输入,而且也有数据的输 出,因此需要执行六个步骤。

image OCI编程的一般过程还是很清晰的,流程图都是一条线………

三、单次查询返回多行结果的实现

设计的时候老大要求要像原有接口库那样一次查询返回多行,然后再在本地进行处理,以减少对数据库的访问。这本来是一个很正常的要求,但后面看了好些开源的 OCI封装,发现它们的demo里都没有给出如何fetch多行……… 比如写的比较好的ocilib,demo中就没有给出(至少是以前的版本没给出,现在就不知道了),在写完这个OCI接口库大概半年后,再看ocilib 的代码OCIDefineByPos()函数时,发现倒数第三第四个参数都是指针,说明可以fetch多行。进而发现ocilib可以通过 OCI_SetFetchSize()函数来设置查询返回的行数。但为什么不在demo里给一个示例呢?就连该版本的文档里也没有这个函数的说明。很奇 怪!没办法还是自己动手丰衣足食,使劲啃user’guide。

需要fetch多行,首先要考虑执行select语句后,接收到数据放在什么地方?当然是放在缓冲区里了,在OCI里通过不同的变量函数绑定来告诉 oracle client把从数据库取到的数据存放在什么地方。这里使用OCIDefineByPos()函数。下面以每次取100行为例给出具体步骤:

1. 分配足够大的缓冲区m_pData = new unsigned char[m_DataLen * 100]。m_DataLen表示数据库表中每一行的长度(各个列长度之和),这样需要使用unsigned作Buffer,因为如果使用有符号char 则取带时间的表会有问题。

2. 根据各列的长度来定义各列在缓冲区中的位置:

01 OCIDefineByPos(m_hOCI.phStmt, //语句句柄
02 &(m_vecColInfo[i].phdefine),//定义句柄
03 m_hOCI.phErr,
04 i+1, //列序号
05 (ub1 *)(&(m_pData[pos])), //各列的位置pos等于当前列之前各列长度和乘以100
06 m_vecColInfo[i].collen, // 对应列的长度 SQLT_STR,
07 (m_vecColInfo[i].indp), // 指示器,因为每次最多要取100行,所以indp应设为维数为100的数组。
08 (m_vecColInfo[i].rlenp), // 返回数据的真实长度,这里也应把rlenp设为维数100的数组
09 0,
10 OCI_DEFAULT));

m_vecColInfo为保存各列信息的vector。值得注意的是OCIDefineByPos()的第八和第九个参数:第八个参数是指示器参数,在 OCIStmtFectch后只是所取的对应数据是否完整(0表示完整),由于要取100行,则在m_vecColInfo中每一个列元素对应的结构中都 应定义indp[100]的数组。第九个参数用于返回所取数据的实际长度,因此也需要在一个列元素的结构体中定义rlenp[100]的数组。还有一个需 要注意的是第四个参数里的pos,pos用于指定该列保存在Buffer中的起始位置。如下表是数据库中某表,执行select查询该表前100行后,数 据在缓冲区m_pData中保存数据的形式如图4:

CarKey MakeKey ModelKey ColorKey Year
1 1 1 2 2003
2 2 1 3 2005
3 2 1 2 2005
……. ……. …….. …….. ………
100 2 1 1 2006

image 上图可以看到OCI在fetch多行时,先将第一列的100行数据放入m_pData中,然后以列为单位每次取100行放入m_pData。因此pos变 量的赋值应写为:pos +=  100 * (m_vecColInfo[i-1].collen); 其中collen代表该列的长度。

3. 获取数据:

1 OCIStmtFetch(m_hOCI.phStmt,
2 m_hOCI.phErr,
3 100,//每次取100行的数据
4 OCI_FETCH_NEXT,
5 OCI_DEFAULT);

第三个参数设置为100后,执行OCIStmtFetch完毕后数据就填充到缓冲区中。这里需要注意的是最后一个fetch,因为最后一次fetch时数 据库表中往往已经不足100行,所以每次执行OCIStmtFetch()函数完毕要需要检查其返回值,当返回值为OCI_NO_DATA时使用:

1 OCIAttrGet(m_hOCI.phStmt,
2 OCI_HTYPE_STMT,
3 (dvoid *) &row_fetched,
4 (ub4 *) NULL,
5 (ub4) OCI_ATTR_ROWS_FETCHED,
6 m_hOCI.phErr);

row_fetched将返回剩下的行数,倒数第二个参数为OCI_ATTR_ROWS_FETCHED,在oci.h中是这样定义的:

#define OCI_ATTR_ROWS_FETCHED  197 /* rows fetched in last call */

在oci10中这里没有任何问题,但在一些较早的oci9版本中找不到OCI_ATTR_ROWS_FETCHED的定义……… 也就是说无法fetch多行?!迫不得已只能用oci10。可能ocilib在demo中没有fetch多行的示例也是出于这个考虑吧。

四、Oracle即时客户端(instantclient)的配置

过去使用OCI需要安装oracle的客户端,Oracle的普通客户端一般都很庞大,Windows平台下的客户端就有700M。Oracle公司在 10g版本后推出了大小只有30M的InstantClient(即时客户端)作为oracle的访问客户端。不需要安装就可以访问Oracle的服务 器。

Windows平台下instantclient的配置和使用:

下面以C:\Oracle为例介绍具体的配置过程。
1.将instantclient的basic包及sqlplus包中所有文件解压至C:\Oracle。
2.配置系统的环境变量:

• 将 C:\Oracle 添加到 PATH 中(位于其他 Oracle 目录之前)。例如,在 Windows 2000 上,依次单击“开始”->“设置”->“控制面板”->“系统”->“高级”->“环境变量”,编辑系统变量列表中的 PATH。WindowXP上,右击“我的电脑”->“高级”->“环境变量”。
• 添加用户环境变量 TNS_ADMIN 设置为C:\Oracle。
• 设置必要的 Oracle 全球化语言环境变量, 添加用户环境变量NLS_LANG 中文对应的字符集是 SIMPLIFIED CHINESE_CHINA.ZHS16GBK

3. 一共设置以下三个环境变量(以解压缩目录C:\Oracle为例)环境变量名 变量值

path C:\Oracle
TNS_ADMIN C:\Oracle
ORACLE_HOME C:\Oracle (可选)
NLS_LANG SIMPLIFIED CHINESE_CHINA.ZHS16GBK

4. tnsnames.ora和sqlnet.ora文件,这两个文件可以在所要访问的Oracle数据库服务器的$ORACLE_HOME/network /admin目录下找到,把tnsnames.ora中的服务器主机名改为ip地址即可。需更改时注意备份原来的文件。
5. 配置完毕后进入C:\Oracle运行sqlplus.exe登陆对应的数据库测试是否设置正确。在windows下使用instantclient时, 需要将instantclient的sdk包中的include和lib加到工程中。

Unix平台下instantclient的配置和使用:

本例中使用solaris_x86_10.2.0.2为客户端
1. 将instantclient_solaris_x86_10.2.0.2中的basic、sqlplus和sdk解压至同一目录,用chmod将该目录 下的所有文件设为可读写,比如:chmod –R 777 ./*

2. 配置环境变量:
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/ychellboy/instantclient_10_2/
ORACLE_HOME=/home/ychellboy/instantclient_10_2/
export TNS_ADMIN=/home/ychellboy/instantclient_10_2/
可将上述命令写入一个XXX.sh文件。

3. tnsnames.ora和sqlnet.ora文件,这两个文件可以在所要访问的Oracle数据库服务器的$ORACLE_HOME/network /admin目录下找到,把tnsnames.ora中的服务器主机名改为ip地址即可。
4. 配置完毕后进入ORACLE_HOME指定的目录运行sqlplus.exe登陆对应的数据库测试是否设置正确。
在solaris下使用instantclient时需将链接库定位至ORACLE_HOME下,链接libclntsh打头的库文件即可。

五、instantclient的Bug

在开发时,最早下载InstantClient for win32 Version 10.2.0.5版,但怎么配置程序都无法初始化OCI的环境,错误发生在OCIInitialize()函数,换成OCIEnvCreate() 同样有错,设断点调试时报的错误是:弹出对话框“User breakpoint called from code at 0x…….”如下图所示;

image

同时Debug窗口输出的错误是:“HEAP[testlib.exe]: Invalid Address specified to RtlFreeHeap( 00140000, 0014F390 )”,下图所示:

image 都在还没初始化OCI怎么就溢出了?开始以为是instantclient没有正确配置,但sqlplus是可以使用的。然后怀疑是程序写错了,但使用普 通的oracle9i的客户端程序又能正确运行。期间还把VC6换成VS2005,问题依然没有解决。在快要放弃的时候换了InstantClient for win32 Version 10.1.0.5版,一试就通,原来是被oracle耍了。我现在还保留着10.2.0.5版,有兴趣玩一下的站内联系我。

六、连接断开的错误处理

据说自从OCI7.3后,OCI的连接可以设置成非阻塞模式。本文第二节可以看到到调用OCI函数和数据建立会话后可以使用OCIAttrSet()函数 将会话设置为费阻塞模式,但我没在user‘guide里找到有专门讲那些函数会受到影响的章节……(可能我看得不够仔细)?不知道那些函数操作会受到连 接断开的影响,那么即使设置了非阻塞模式也不知道在哪进行错误处理啊!没办法只能自己动手挨个函数试………
经过多次断网测试后得出以下函数将受到连接断开的影响,注意在windows下拔开网线后不管设置成阻塞或非阻塞这些函数都能返回并给出错误码。

Windows下:

1) OCIServerAttach:错误码12560,错误信息“TNS协议适配器错误”
2) OCISessionBegin:错误码12571,错误信息“TNS包写入程序失败”
3) OCIStmtExecute:如果在此函数执行前断网则返回错误码12571,错误信息“TNS包写入程序失败”,如果在此函数第一次执行(返回 OCI_STILL_EXECUTING)到第二次执行之间断网则返回错误码03113,错误信息“通信信道的文件结束”
4) OCIStmtFetch:错误码12571,错误信息“TNS包写入程序失败”
知道那些函数会受到断网影响后,处理起来就比较简单,即增加对这四个函数返回失败时的错误码判断,在断网时返回相应的错误返回给接口调用者, 由调用者选择处理方法。这种通过错误码进行处理的方法并不是长久之计,因为如果Oracle改变所返回的错误码,或者是在不同widows版本下返回的错 误码不一致则错误处理失效。不过windows并不是我们的主用系统所以就得过且过了^_^

Unix下:

Unix下断网情况和windows的情况相差很大,在unix下OCIServerAttach连接建立成功后,将此会话设置为阻塞则真的阻 塞!windows下即使设置成阻塞,断网发生时照样返回错误信息。unix下设置为非阻塞则情况又不一样。下面的函数带①表示会话设置为阻塞,断网后会 出现的情况。带②则表示会话设置为非阻塞,断网后会出现的情况。①②则表示阻塞、非阻塞情况都一样。
1)OCIStmtExecute() ===》① 无返回死等,检测不出断网,网络恢复后可正确执行(因为OCI用TCP做连接,可能也会有超时的时间) ② 执行时返回OCI_STILL_EXECUTING
2)OCIReset() ===》①② 阻塞死等
3)OCITransCommit() ===》①② 阻塞
4)OCIStmtFetch() ===》① 阻塞 ② 返回OCI_STILL_EXECUTING
5)OCISessionEnd() ===》①② 返回错误ORA-03127
6)OCIServerDetach() ===》①② 无影响可返回
7)OCIServerAttach() ===》①② 阻塞3~5分钟后返回错误ORA-12170
可以看到OCIServerAttach()和OCITransCommit()函数是一定阻塞的,不管会话有没有设置成非阻塞模式。我想 OCIServerAttach()函数里应该会调用到connect(),为什么Oracle就不做一个非阻塞的connect呢?搞不懂。如果要进行 断网的保护,那么在实现该OCI接口库时,使用到这两个函数的接口函数就应该放到子线程里进行调用。在主线程里timewait子线程,一旦子线程操作超 时就把它Terminate或cancel掉,然后返回连接断开的错误给调用者。这样干似乎很危险啊,查看了一下内存,发现在Solaris平台下每次打 断OCIServerAttach()函数后会产生60K的内存泄露。

终于写完了,托了好几天。接着闭关去!
References:
[1]Oracle® Call Interface Programmer’s Guide 10g Release 1(10.1), Part No.B10779-01, December 2003

[转载]操作PDF文档功能的相关开源项目探索iTextSharp 和PDFBox

mikel阅读(1233)

[转载]操作PDF文档功能的相关开源项目探索——iTextSharp 和PDFBox – 无痕客 – 博客园.

很久没自己写写心得日志与大家分享了,一方面是自己有点忙,一方面是自己有点懒,没有及时总结。因为实践是经验的来源,总结是提升的基础,所以 无论怎样,自己都该反省一下。今天我主要是研究学习了两个PDF文档的相关类,iTextSharp 和PDFBox。我研究出发点是实现PDF文档的检索,需要提取PDF文档中的文字内容,然后通过正则匹配实现搜索。

类似 Windows Search的文件搜索系统》中介绍的文件检索方法是很不错的,但它里面对PDF中的中文检索不支 持,因为里面调用的iTextSharp不能很好地支持英文,PdfReader类的GetPageContent()方法无法正常返回中文字符,经我测 试,并非简单的编码问题。所以,急需能够从PDF中提取text功能。

我首先学习iTextSharp.dll 下载:http://sourceforge.net/projects/itextsharp/ 这里面有很多输出PDF文档的简单例子(下载iTextSharp例子),在学习中发现,不支持中文内容输出。在网上搜索相关内容发现,原来是缺少字体库。 有两种方法解决:

1.自己指定系统的字体库,创建PDF中使用的字体。参见:http://unruledboy.cnblogs.com/Skins/ChinaHeart/Controls/archive/2005/08/30/225984.html

Document document = new Document(PageSize.A4,50, 50, 50, 50);
try
{
PdfWriter writer = PdfWriter.GetInstance(document, new FileStream(“Chap11.pdf”, FileMode.Create));

//下面是创建PDF文档加密的
//writer.SetEncryption(PdfWriter.STRENGTH40BITS,”654321″, “654321”, PdfWriter.AllowCopy);
document.Open();

//指定字体库,并创建字体
BaseFont baseFont = BaseFont.CreateFont(
“C:\\WINDOWS\\FONTS\\SIMHEI.TTF”,
BaseFont.IDENTITY_H,
BaseFont.NOT_EMBEDDED);
iTextSharp.text.Font font = new iTextSharp.text.Font(baseFont, 9);

//指定输出内容的字体

document.Add(new Paragraph(” This document is Top Secret! “, font));
document.Close();
}
catch (Exception de)
{
Console.WriteLine(de.StackTrace);
}

2.从http://sourceforge.net/projects/itextsharp/ 下载扩展字体库 iTextAsianCmaps.dll 和iTextAsian.dll,支持亚洲字体。

下载界面如下:

/// <summary>
/// 创建中文字体(实现中文)
/// </summary>
/// <returns></returns>
public static iTextSharp.text.Font CreateChineseFont()
{
BaseFont.AddToResourceSearch(“iTextAsian.dll”);
BaseFont.AddToResourceSearch(“iTextAsianCmaps.dll”); //”STSong-Light”, “UniGB-UCS2-H”,
BaseFont baseFT=BaseFont.CreateFont(“STSong-Light”, “UniGB-UCS2-H”, BaseFont.EMBEDDED);

iTextSharp.text.Font font = new iTextSharp.text.Font(baseFT);
return font;
}

“UniGB-UCS2-H” “UniGB-UCS2-V”是简体中文。 “STSong-Light”是字体名称。BaseFont.EMBEDDED是将字体嵌入文档内。

其次,我接下来尝试在使用iTextSharp读对象类时,指定字体库,可是很遗憾没有相应方法。请参照:http://www.cnblogs.com/diction/articles/1120984.html (提 取文本不支持中文)而且,即使有也很不灵活,因为你不可能预知PDF文档中使用的字体,PDF文档中可能有多种字体。后来,搜索网页相关信息发现:原来iTextSharp的操作PDF文档优势是PDF文档的创建。

需求是学习和工作的动力

我的原始目标是找到PDF文档内容提取为文本的方法,我转向《How to parse PDF files》 该文章完整讲述了PDF文档提取文本的方法和整个解决过程 思路,我会单独转载该文章,希望不能访问国外网的网友也能看到。PDFBox的下载http://sourceforge.net/projects/pdfbox/files/ 下 载解压后里面内容很丰富,

所有需要的dll都包含在Bin文件夹里面

“PDFBox is a Java PDF Library. This project will allow access to all of the components in a PDF document. More PDF manipulation features will be added as the project matures. This ships with a utility to take a PDF document and output a text file. ”

PDFBox是个JAVA开源项目,里面使用IKVM.NET开源项目http://www.ikvm.net/ 支持JAVA类库在.NET中调用。

IKVM.NET is an implementation of Java for Mono and the Microsoft .NET Framework. It includes the following components:

  • A Java Virtual Machine implemented in .NET
  • A .NET implementation of the Java class libraries
  • Tools that enable Java and .NET interoperability

对IKVM.NET的学习,对以后在.NET下使用JAVA类库很有帮助,其实IKVM.Runtime.dll 就是封装了JAVA类库的运行环境。

需要添加的DLL有:FontBox-0.1.0-dev.dll、IKVM.GNU.Classpath.dll、 IKVM.Runtime.dll、PDFBox-0.7.3.dll

PDFBox使用实例代码如下:请参照:http://www.cnblogs.com/wuhenke/archive/2010/04/16/1713949.html

private static string parseUsingPDFBox(string filename)
{
PDDocument doc = PDDocument.load(filename);

PDFTextStripper stripper = new PDFTextStripper();

return stripper.getText(doc);
}

PDFBox功能很强大,有时间值得好好学习一下。

参考:

http://www.codeproject.com/kb/cpp/ExtractPDFText.aspx?df=100&forumid=47947

http://www.codeproject.com/KB/string/pdf2text.aspx

http://www.cnblogs.com/hardrock/

http://www.ikvm.net/