2004年09月20日

http://blog.csdn.net/arielxp/category/16426.aspx?Show=All

原文链接http://www.javaworld.com/javaworld/jw-08-2004/jw-0809-ejb.html

 

译者语:

翻译完这篇文章之后,感觉EJB3.0确实有了很大的改进,尤其在降低开发难度方面,个人认为EJB3.0采用注释的方式来简化开发确实是一个很好的注意。以前用过XDoclet或者BEA公司的WeblogicWorkShop(它的jws就是一个加了注释的java)的朋友对于这一技术应该不是很陌生。本文的作者是BEA公司的资深程序员他不仅向我们描述了最新的EJB3.0技术,而且提出了他个人的一些观点,非常值得我们参考,作者还在文末倡议大家一起参加到EJB3.0规范的制定中来。希望通过阅读本文能使你一窥EJB3.0的冰山一角。

对于本文的中文翻译,作者保留其一切权利,如需转载需要经过译者的同意。

 

引言

期待以久的EJB3.0规范在最近发布了它的初稿。在本文中将对新的规范进行一个概要性的介绍,包括新增的元数据支持,EJBQL的修改,实体Bean模型访问bean上下文的新方法和运行时环境等等。作者还讨论了EJB在未来要作出的调整以及EJB3.0与其他开发规范之间的关系。

开始

无论如何由于EJB的复杂性使之在J2EE架构中的表现一直不是很好。EJB大概是J2EE架构中唯一一个没有兑现其能够简单开发并提高生产力的组建。EJB3.0规范正尝试在这方面作出努力以减轻其开发的复杂性。EJB3.0减轻了开发人员进行底层开发的工作量,它取消或最小化了很多(以前这些是必须实现)回调方法的实现,并且降低了实体BeanO/R映射模型的复杂性。

在本文中,我首先会介绍EJB3.0中几个主要的改变。它对进一步深入了解EJB3.0是非常重要的。随后,我会从更高的层面来描述已经被提交到EJB3.0规范中的细节,并一个个的讲解新的规范中的改变:实体Bean,O/R映射模型,实体关系模型和EJB QL(EJB查询语言)等等。

背景

EJB3.0中两个重要的变更分别是:使用了Java5中的程序注释工具和基于HibernateO/R映射模型。

Java5中的元数据工具。

Java5(以前叫J2SE1.5Tiger)中加入了一种新的程序注释工具。通过这个工具你可以自定义注释标记,通过这些自定义标记来注释字段、方法、类等等。这些注释并不会影响程序的语义,但是可以通过工具(编译时或运行时)来解释这些标记并产生附加的内容(比如部署描述文件),或者强制某些必须的运行时行为(比如EJB组件的状态特性)。注释的解析可以通过源文件的解析(比如编译器或这IDE工具)或者使用Java5中的APIs反射机制。注释只能被定义在源代码层。由于所有被提交到EJB3.0草案中的注释标记都有一个运行时的RetentionPolicy,因此会增加类文件占用的存储空间,但这却给容器制造商和工具制造商带来了方便。

Hibernate

目前Hibernate非常受欢迎,它是开发源代码的Java O/R映射框架,目的是把开发人员从繁琐的数据持久化编程中解脱出来。它也有一个标准的HQLHibernate 查询语言)语言,你可以在新的EJB QL中看到它的影子。Hibernate在处理如数据查询、更新、连接池、事务处理、实体关系处理等方面非常简单。

概览

在已经提交的EJB3.0规范中主要涉及两个方面的改变:

1.         一套以注释为基础的EJB编程模型,再加上EJB2.1中定义的通过部署描述符和几个接口定义的应用程序行为。

2.         新的实体Bean持久化模型,EJBQL也有许多重要的改变。

还有一些有关上述的提议,比如:一个新的客户端编程模型,业务接口的使用以及实体Bean的生命周期。请注意EJB2.1编程模型(包括部署描述符和home/remote接口)仍然是有效的。新的简化模型并没有完全取代EJB2.1模型。

EJB注释

EJB规范组织一个重要的目标是减轻原始代码的数量,并且他们为此给出了一个完美而简介的办法。在EJB3.0的里,任何类型的企业级Bean只是一个加了适当注释的简单Java对象(POJO)。注释可以用于定义bean的业务接口、O/R映射信息、资源引用信息,效果与在EJB2.1中定义部署描述符和接口是一样的。在EJB3.0中部署描述符不再是必须的了;home接口也没有了,你也不必实现业务接口(容器可以为你完成这些事情)。

比如,你可以使用@Stateless注释标记类把Java类声明为一个无状态回话bean。对于有状态回话bean来说,@Remove注释可以用来标记一个特定的方法,通过这个注释来说明在调用这个方法之后bean的实例将被清除掉。

为了减少描述组件的说明信息,规范组织还采纳了由异常进行配置configuration-by-exception)的手段,意思是你可以为所有的注释提供一个明确的缺省值,这样多数常规信息就可以据此推断得出。

新的持久化模型

新的实体bean也是一个加了注释的简单Java对象(POJO)。一旦它被EntityManager访问它就成为了一个持久化对象,并且成为了持久化上下文context)的一部分。一个持久化上下文与一个事务上下文是松耦合的;严格的讲,它隐含的与一个事务会话共存。

实体关系也是通过注释来定义的,O/R映射也是,并提供几种不同的数据库规范操作,在EJB2.1中这些要通过开发人员自己的设计模式或者其它技术来完成的(比如,自增长主键策略)。

深入研究

现在是时候详细了解EJB3.0草案了。让我们开始探讨所有EJB中四种企业级bean,并看看他们在新的规范中是什么样子。

无状态回话bean

EJB3.0规范中,写一个无状态回话bean(SLSB)只需要一个简单的Java文件并在类层加上@Stateless注释就可以了。这个bean可以扩展javax.ejb.SessionBean接口,但这些不是必须的。

一个SLSB不再需要home接口,没有哪类EJB再需要它了。Bean类可以实现业务接口也可以不实现它。如果没有实现任何业务接口,业务接口会由任意public的方法产生。如果只有几个业务方法会被暴露在业务接口中,这些方法可以使用@BusinessMethod注释。缺省情况下所有产生的接口都是local(本地)接口,你也可以使用@Remote注释来声明这个接口为remote(远程)接口。

下面的几行代码就可以定义一个HelloWorldbean了。而在EJB2.1中同样的bean至少需要两个接口,一个实现类和几个空的实现方法,再加上部署描述符。

import javax.ejb.*;

/**
* A stateless session bean requesting that a remote business
* interface be generated for it.
*/
@Stateless
@Remote
public class HelloWorldBean {
   public String sayHello() {
      return “Hello World!!!”;
   }
}

有状态回话bean

除了几个SFSB的特别说明之外,有状态回话bean(SFSB)SLSB一样精简:

l         一个SFSB应该有一个方法来初始化自己(在EJB2.1中是通过ejbCreate()来实现的)。在EJB3.0的规范中建议这些初始化操作可以通过自定义方法完成,并把他们暴露在业务接口中。在使用这个bean之前由客户端来调用相应的初始化方法。目前规范组织就是否提供一个注释来标记某个方法用于初始化还存在争议。

l         Bean的提供者可以用@Remove注释来标记任何SFSB的方法,以说明这个方法被调用之后bean的实例将被移除。同样,规范组织仍然在讨论是否要有一种机制来处理这种特殊的情况,即当这个方法出现异常的情况下bean的实例是否被移除。

下面是对以上问题我个人的观点:

l         是否应该有一个注释来标明一个方法进行初始化呢?我的观点是――应该有,这样容器就可以在调用其他方法之前至少调用一个方法来进行初始化。这不仅可以避免不必要的错误(由于没有调用初始化方法)而且可以使容器更明确的判断是否可以重用SFSB实例。我暂且把这个问题放一放,规范组织只考虑为一个方法提供一个注释来声明它是一个初始化方法。

l         对于第二个问题我的观点也是肯定的。这有利于Bean的提供者合客户端程序对其进行控制。只有一个遗留的问题:那就是一旦调用这个方法失败,是否能移除这个bean 的实例?答案是不能,但是它将会在回话结束的时候被移除。

消息驱动Bean

消息驱动Bean是唯一一种必须实现一个业务接口的Bean。这个接口指出bean支持的是哪一种消息系统。对于以JMS为基础的MDB来说,这个接口是javax.jms.MessageListener。注意MDB业务接口不是一个真正意义上的业务接口,它只是一个消息接口。

实体Bean

l         实体Bean使用@Entity注释来标记,所有实体bean中的属性/字段不必使用@Transient注释来标记。实体bean的持久化字段可以通过JavaBean-style机制或者声明为public/protected字段来实现。

l         实体bean可以使用助手类来描述其状态,但是这些类的实例并没有持久化唯一性(persistent identity的特性(即,唯一标识这个bean的字段等),实际上这些助手类与他们的实体bean实例是紧密结合的;并且这些对象还是以非共享方式来访问实体对象的。

实体关联

EJB3.0同时支持Bean之间双向的合单向的关联,它们可以是一对一、一对多、多对一或者是多对多的关联。然而双向关联的两端还要分为自身端owning side)和对方端inverse side)不同的端。自身端负责向数据库通告关联的变更。对于多对多的关联自身端必须明确的声明。实际上对方端通过isInverse=true进行注释(由此自身端就不必说明了而是由另一段推断出)。看来上面的描述,规范组织还能说让EJB变的简单了吗?

O/R映射

EJB3.0中的O/R映射模型也有了重要的改变,它从原来的abstract-persistence-schema-based变成了现在的Hibernate-inspired模式。尽管目前规范组织还在就此进行讨论但是一个明确的模型将会出现在下一个版本的草案中。

举例来说,O/R映射模型将通过bean类中的注释来声明。而且此方法还会指出对应的具体表和字段。O/R映射模型提供了一套自有的SQL;而且除了提供一些基本的SQL外还支持某些高层开发的功能。比如,有一个通过@Column注释声明的字段columnDefinition,那么可以写这样的SQLcolumnDefinition=”BLOB NOT NULL”

客户端程序模型

一个EJB客户端可以通过@Inject注释以一种“注入”的方式获得一个bean的业务接口引用。你也可以使用另一个注释@javax.ejb.EJBContext.lookup()来完成上面的操作,但是规范中没有告诉我们一个普通的Java客户端怎样获得一个Bean的实例,因为这个普通的Java客户端是运行在一个客户端容器中,它无法访问@javax.ejb.EJBContex对象。现在还有另外一种机制来完成上面的工作那就是使用一个超级上下文环境对象:@javax.ejb.Context()。但是规范中没有指出该如何在客户端中使用这个对象。

EJB QL

EJB QL可以通过@NamedQuery来注释。这个注释有两个成员属性分别是namequeryString.一旦定义了这些属性,就可以通过EntityManager.createNamedQuery(name)来指向这个查询。你也可以创建一个标准的JDBC风格的查询并使用EntityManager.createQuery(ejbqlString)EntityManager.createNativeQuery(nativeSqlString)(这个方法用于执行一个本地查询)来执行查询。

EJB QL有两个地方可以定义其参数。javax.ejb.Query接口提供了定义参数、指向查询、更新数据等等方法。下面是一个EJBQL指向查询的例子:

.. ..
@NamedQuery(
name=”findAllCustomersWithName”,
queryString=”SELECT c FROM Customer c WHERE c.name LIKE :custName”
)
.. ..
@Inject public EntityManager em;
customers = em.createNamedQuery(“findAllCustomersWithName”)
.setParameter(“custName”, “Smith”)
.listResults();

下面列出了一些EJB QL的增强特性:

l         支持批量更新和删除。

l         直接支持内连接和外连接。FETCH JOIN运行你指出关联的实体,Order可以指定只查询某个字段。

l         查询语句可以返回一个以上的结果值。实际上,你可以返回一个依赖的类比如下面这样:SELECT new CustomerDetails(c.id, c.status, o.count)
      FROM Customer c JOIN c.orders o
      WHERE o.count > 100

l         支持group by having

l         支持where子句的嵌套子查询。

在提交的EJB3.0草案中,EJB QL与标准SQL非常的接近。实际上规范中甚至直接支持本地的SQL(就像我们上面提到的那样)。这一点对某些程序员来说也许有些不是很清楚,我们将在下面进行更详细的讲解。

多样性

方法许可(Method permissions)可以通过@MethodPermissions@Unchecked注释来声明;同样的,事务属性也可以通过@TransactionAttribute注释来声明。规范中仍然保留资源引用和资源环境引用。这些一样可以通过注释来声明,但是有一些细微的差别。比如,上下文(context)环境要通过注入工具控制。容器根据bean对外部环境引用自动初始化一个适当的已经声明的实例变量。比如,你可以象下面这样获得一个数据源(DataSource):

@Resource(name=”myDataSource”) //Type is inferred from variable
public DataSource customerDB;

在上面的例子中如果你不指定引用资源的名称(name)那么其中的customerDB会被认为是默认值。当所有的引用属性都可得到时,@Injec注释就可以这样写:

@Inject public DataSource customerDB;

容器负责在运行时初始化customerDB数据源实例。部署人员必须在此之前在容器中定义好这些资源属性。

更好的消息是:那些以前必须检测的异常将一去不复返。你可以声明任意的应用程序异常,而不必在再抛出或捕获其他类似CreateExceptionFinderException这样的异常。容器会抛出封装在javax.ejb.EJBException中的系统级异常或者只在必要时候抛出IllegalArgumentExceptionIllegalStateException异常。

EJB文件处理模式

在我们结束本节之前,让我的快速的浏览一下容器提供商在EJB处理模式方面可能的变更。规范中对此并没有明确的表态,但我可以想到至少两种模式。

l         一种办法是首先利用EJB文件生成类似于EJB2.1部署模式的文件(包括必要的接口和部署描述符)然后再用类似于EJB2.1的方式来部署这个EJB组件。当然,这样产生的部署描述符可能并不标准但是它可以解决同一个容器对EJB2.1EJB3.0兼容的问题。下面这幅图描述了这一过程。

 

 

l         另一种方法是一种类似于JSP托放的部署模式。你可以把一个EJB文件放到一个预先定义的目录下,然后容器会识别这个EJB并处理它,然后部署并使之可以使用。这种方法可以建立于上面那种方法之上,在支持反复部署时有很大的帮助。考虑到部署的简单性也是EJB3.0规范的目的之一,我真诚的希望在下一个草案出来时能够确定一个模式(至少能有一个非正式的)

你有什么想法?

EJB3.0规范的制定正在有序的进行,为了使EJB的开发变得更加容易,EJB规范组织作出的努力是有目共睹的。就像他们说的那样,一切对会变得简单,但做到这一点并不容易。目前已经定义了50个注释标记(还有几个将在下一个草案中发布),每一个都有自己的缺省规则和其他的操作。当然,我真的不希望EJB3.0变成EJB2.1的一个翻版“EJB 3.0 = EJB 2.1 for dummies”(希望这个等式不要成立)。最后,我还是忍不住要提一些我自己的观点:

l         首先,规范确实使反复部署变得容易了,并且有一个简单的模式来访问运行时环境。我还是觉得home接口应该放弃。

l         在早期的EJB规范中,实体bean用于映射一个持久化存储。理论上(也许只是理论上)可能需要把实体bean映射到一个遗留的EIS(enterprise information system)系统中。出于将来扩展的考虑这样作是有好处的,并且可以使更多的业务数据模型采用实体bean。也因此其伴随的复杂性使得实体bean不被看好。在本次提交的草案中,一个实体bean只是一个数据库的映射。并且是基于非抽象持久化模式和简单的数据访问模式的更加简单开发。

l         我对模型变更持保留态度,我认为在EJB中包含SQL脚本片断并不是个好注意。一些开发人员完全反对包含某些“SQL片段(SQLness)”(比如@Table @Column注释)。我的观点是这些SQLness是好的,据此我们可以清楚的知道我们到底要数据库作些什么。但是某些SQL段我看来并不是很好,比如columnDefinition=”BLOB NOT NULL”,这使得EJB代码和SQL之间的耦合太过紧密了。

l         尽管对于本地SQL的支持看似很诱人,其实在EJB代码中嵌入SQL是一个非常糟糕的主意。当然,有些办法可以避免在EJB中硬编码SQL,但是这应该在规范中说明,而不能是某些开发人员自己定义的模式。

l         假设@Table注释只用于类。在运行时通过@Table注释的name属性定义的表名称将必须对应一个实际的数据库表。规范对此应该给予清楚的说明和一致的模式。

l         规范还需要更清楚的说明客户端编程模型,尤其是普通java客户端。规范中所有的参考都假设或者隐含的使用EJB客户端。而且规范中对客户端的向后兼容方面也没有给出明确的说法。

l         Transient注释应该重新命名以避免和已有的transient关键字发生冲突。事实上,在这一点上我们更乐于稍微的背离一下configuration-by-exception原则并且定义一个@Persistent注释来明确的定义持久化字段。@Persistent注释可以仅仅是一个标记注释或者它可以有几个属性来关联O/R映射注释。

与其他规范的关联

目前可能影响到EJB3.0JSRJSR175java语言元数据工具)和JSR181Java Web服务元数据)

JSR175已经初步完成并且不会和EJB3.0有太大的冲突;但是JSR181EJB3.0有两个关联的地方:

l         Web service接口:EJB规范将采用一种机制适应JSR181以便可以把一个bean实现为一个Web service并告诉Web service如何被客户端调用。

l         JSR 181计划采用不同的机制来处理安全问题。在早期的规范中EJB建议使用一个一致的机制(MethodPermissions),但是JSR 181计划使用一个稍微不同的方式(SecurityRolesSecurityIdentity注释)。同样的RunAs注释的定义也存在这些许差别。这一问题还在解决中最终会在J2EE层的规范中维持其一致性。

J2EE 1.5中的一些开发规范可能与EJB3.0有关联。除了上面说到的几个关联之外现在没有其他的开发规范与EJB3.0有冲突。

结束语

在使EJB的开发变得简单高效之前,我们还有很长一段路要走。规范组织在降低EJB的开发难度方面起了个好头。O/R映射模型的提议还处在早期阶段,规范组织正在完善它。我希望它不要太复杂也不要与SQL过分的耦合。让我们不要只是停留在期望、希望、思考和请求中:提出你的想法并把你的建议发送给规范组织ejb3-feedback@sun.comJCP并不是很民主的组织,但是你的建议一定是有价值的。

本文的观点是作者的个人主张与作者所在的公司没有任何关系。作者非常感谢Hemant Khandelwal对发表此文的帮助。

提供下载的代码是EJB3.0草案的示例代码。由于没有工具和环境的支持这个例子是没有经过验证的。这些代码只是示例了未来的EJB3.0大概的样子。

Anil SharmaBEA System公司开发Weblogic Integration产品的资深程序员。在加入BEA之前曾就职于Oracle Pramati,并一直致力于研究J2EE技术。在业余时间它喜欢听音乐看电影;他还参加一些开源项目的开发工作。

在最近的一个项目中用户要求把原来的历史数据导入到新的系统中,我们用Java为用户开发的系统,用户提供的数据格式为Excel文件。于是, 我想到了使用apache的POI,因为以前使用过,觉得还可以,先做了个测试,感觉没什么问题,但是在导入另外一个文件时出错了,系统捕获一个javaCastException.引发这一异常的原因是由于java.util.TreeMap进行类型比较时出现的错误,由于POI时开发源代码的,于是我跟踪了代码出错的位置,发现是POI一个内部的错误,并且这个BUG已经提交到了apache的bugzilla中了。导致这个异常的原因是由于,客户提供的Excel文件是由原来的一个系统中通过程序生成的Excel文件,所以在读的时候由于文件头的错误而造成的。本来想试着自己改一下的,但是时间太紧就改变了主意,决定使用另外一个库,在sourceforge中找到了jxl。试用了一下,还是不错的,虽然在读那个比较特殊的文件时出现了警告,但是并不影响读出数据的准确性。下面我把实现的方法记下来。

import java.io.File;
import java.util.Date;
import jxl.*;
import jxl.read.biff.*;
import java.io.*;

public class testjeapi {
  public testjeapi() {
    try {
      Workbook workbook = Workbook.getWorkbook(new File(“C:/TEMP/ehc.XLS”));
      Sheet sheet = workbook.getSheet(0);
      int rows=sheet.getRows();
      for(int i=1;i<rows;i++){
         String cbxmdm=sheet.getCell(0,i).getContents();
         String cbxmmc=sheet.getCell(1,i).getContents();
      //   String jfsl=sheet.getCell(7,i).getContents().replaceAll(“,”,”");
//System.out.println(“jfsl:”+jfsl);
         NumberCell n=(NumberCell)sheet.getCell(7,i);//NumberCell用于读取数字格式的单元格。
         System.out.println(“n:”+n.getValue());
         Double d=new Double(n.getValue());
         System.out.println(“d:”+d);
        // System.out.println(“cell format:”+sheet.getCell(7,i).getType());
//         String jfsl=sheet.getCell(7,i).getContents();
//         String jfje=NumberCell.getCell(8,i).getContents();
//         Double dfsl=new Double(NumberCell.getCell(9,i).getContents());
//         String dfje=sheet.getCell(10,i).getContents();
//         System.out.println(i+”|cbxmdm:”+cbxmdm+”|”+”cbxmmc:”+cbxmmc+”|”+”jfsl:”+jfsl+”|”+”jfje:”+jfje+”|dfsl:”+dfsl+”|dfje:”+dfje);

      }
    }
    catch (BiffException ex) {
      ex.printStackTrace();
    }
    catch (IOException ex) {
      ex.printStackTrace();
    }

  }
  public static void main(String[] args) {
    testjeapi testjeapi1 = new testjeapi();
  }

}

 

本文是由JR主持写作的《J2SE进阶》一书的部分章节整理而成,《J2SE进阶》正在写作、完善阶段。《J2SE进阶》写作项目组感谢您阅读本文。

本文摘自雪无痕Blog

随着网络应用的不断推广,电子邮件越来越多的被大家使用。虽然我们往往将电子邮件与 Foxmail、Outlook 这样的电子邮件客户端联系起来,但是往往我们也需要自己编程实现发送接收邮件,例如在一个网站注册用户后网站发出的回执mail,或者在网络购物的时候,在完成订单后的几分钟之内发送确认电子邮件。对于这样的需求,我们不能通过已有的邮件客户端而需要自己编写邮件发送或者处理程序。在这里向大家讲解一下如何利用JavaMail来实现邮件的收发。

注意:本文只打算讨论JavaMail收发带附件邮件的一些技巧,所以只是给出部分代码,更多更精彩的内容,请期待《J2SE进阶》一书。

1.发送带附件的邮件

  我们平时发送的邮件主要可以分解成2大部分,一个是发信人,接信人,主题等邮件标头,另外一部分是邮件内容,它包括了邮件的附件。我们在发送普通邮件的时候content设置的类型是”text/html”,带上附件后,我们需要把content的类型设置成Multipart,这时content包括了附件和”text/html”类型的正文。下面的这个告诉大家如何把附件放置到邮件中。

private Multipart getMultipart() throws  MessagingException,UnsupportedEncodingException  { 
   MimeMultipart mp = new MimeMultipart();    
   try    
   {      
      //设置content里的内容      
      MimeBodyPart contentMbp = new MimeBodyPart();      
      //请指定字符集,否则会是乱码      
      contentMbp.setContent(_mailContent.getContent(),
                            “text/html; charset=GB2312″);      
      mp.addBodyPart(contentMbp);      
      //添加附件      
      for (int i=0;i<_mailAttachment.getAttachPath().size();i++)
      {        
         MimeBodyPart mbp = new MimeBodyPart();        
         FileDataSource fds = new FileDataSource((String)
           _mailAttachment.getAttachPath().get(i));        
         mbp.setDataHandler(new DataHandler(fds));        
         mbp.setFileName(MimeUtility.encodeWord(fds.getName(),
           “GB2312″,null));        
         mp.addBodyPart(mbp);      
      }
   }    
   catch(MessagingException ie)    
   {      
      System.out.println(“Set Content Message error…”+ie.getMessage());      
      throw ie;    
   }    
   catch(UnsupportedEncodingException ie)    
   {      
      System.out.println(“Encode the fileName error…”+ie.getMessage());      
      throw ie;    
   }    
   return mp;  
}
放置附件的注意事项如下:
  在发mail时需要注意字符集的问题。不但content里要设置,而且文件名也需要设置。如果我们去掉mbp.setFileName(MimeUtility.encodeWord(fds.getName(),”GB2312″,null));这句话,那么你选中的附件还是会带到邮件里,但是在附件里看不到。我们可以通过查看邮件大小知道。我们利用这个特点来实现发送content中写的是html语言,而且包含图片信息的邮件。

2.发送content中包含html页面的邮件

大家都知道html语言可以带上图片链接(<img src=”c:/test.jpg”></img>),那么我们在发送邮件的时候就需要对这些链接的图片做特殊处理。否则在对方接收到邮件的时候会看不到图片。我们特殊处理的方法就是把它们当成附件发送,但不显示在附件里。要做到这些就首先需要对输入的content进行解析,找到所带图片的路径。然后把content中<img src=”c:/test.jpg”></img>这段代码变成<img src=” cid:IMG”></img>。我们在发送附件的时候用mbp1.setHeader(“Content-ID”,”IMG”) 来把图片和附件对应上。如何具体解析content的操作我就不赘述了,我现在给出如何把修改好的content发送出去的例子。

  //对于发送html类型的content。里边包括图片。        
  for(int i=0;i<_mailContent.getImgHash().size();i++)        
  {          
     MimeBodyPart mbp1 = new MimeBodyPart();
     //得到图片的数据
     FileDataSource fds = new FileDataSource(
       (String)_mailContent.getImgHash().get(“IMG”+i));          
     //设置到MimeBodyPart中
     mbp1.setDataHandler(new DataHandler(fds));          
     //设置图片附件和html的对应关系
     mbp1.setHeader(“Content-ID”,“IMG”+i);          
     mp.addBodyPart(mbp1);        
  }

3.邮件的状态

  我们在阅读完邮件后可以给邮件设置删除标志,然后在关闭FOLDER的时候用true来清空已经被标志为删除的邮件。邮件的状态是在类FLAGS.FLAG中定义的。包括如下几种:
    Flags.Flag.ANSWERED 
    Flags.Flag.DELETED 
    Flags.Flag.DRAFT 
    Flags.Flag.FLAGGED 
    Flags.Flag.RECENT 
    Flags.Flag.SEEN 
    Flags.Flag.USER 
  
我们可以根据不同的需要进行设置,但是需要注意的是,不是所有的服务器都支持这些状态。我们在做操作之前可以用getPermanentFlags方法来得到Message中的状态。参考下面代码
  
  Message m = folder.getMessage(1); 
  // set the DELETED flag 
  m.setFlag(Flags.Flag.DELETED, true); 
  // Check if DELETED flag is set of this message 
  if (m.isSet(Flags.Flag.DELETED))    
  System.out.println(“DELETED message”); 
  // Examine ALL system flags for this message 
  Flags flags = m.getFlags(); 
  Flags.Flag[] sf = flags.getSystemFlags(); 
  for (int i = 0; i < sf.length; i++) 
  {    
     if (sf[i] == Flags.Flag.DELETED)            
        System.out.println(“DELETED message”);    
     else if (sf[i] == Flags.Flag.SEEN)            
        System.out.println(“SEEN message”); 
  }

4.接收带附件的邮件

  在带有附件的邮件中,消息的内容是Multipart型,这样我们就需要解析它来得到content和附件(它是发送带附件的邮件的逆向过程)。大家在使用outlook、foxmail这些电子邮件客户端的时候会发现,我们的邮件被从服务器上下载下来并且保存到本地硬盘上了,这种方式方便我们离线浏览邮件。在下面的范例中我们也把服务器上的邮件保存到本地。如果有兴趣大家可以编写一个客户端的图形界面来读取保存下来的邮件。
  在下面的例子里,我只是向大家介绍如何解析附件。
private void getAttachFile(Part messagePart,BufferedOutputStream writeAttachObj) 
  throws IOException, MessagingException  
{    
   Object content = messagePart.getContent() ;    
   try    
   {      
      //这种情况下的邮件都是用multi模式发送的,
      // 这种模式包括有附件的邮件和用html表示content的邮件      
      if (content instanceof Multipart)      
      {        
         Multipart contentTmp = (Multipart) content ;        
         //如果是MULTI模式发送的,BodyPart(0).getContent()肯定就是content        
         System.out.println(“content==” + contentTmp.getBodyPart(0).getContent()) ;        
         //getCount()可以得到content中bodyPart的个数,content就是第一个
         //bodyPart,其它的附件按照顺序类推。但是有的时候附件就是另外一个邮件,
         //而这个邮件里边可能有其他的附件。下面代码用循环对嵌套情况进行解析。
         for (int i = 0 ; i < contentTmp.getCount() ; i++)        
         {          
            if (contentTmp.getBodyPart(i).isMimeType(“multipart/*”))          
            {           
               Multipart multipart = (Multipart) 
                 contentTmp.getBodyPart(i).getContent() ;
               //这个地方增加循环是为了解决嵌套附件的情况。
               for (int k = 0 ; k < multipart.getCount() ; k++)            
               {             
                  //content也会存在于INPUTSTREAM中。              
                  saveAttacheFile(multipart.getBodyPart(k).getContentType(),                              
                                  multipart.getBodyPart(k).getDisposition(),                              
                                  multipart.getBodyPart(k).getFileName(),                              
                                  multipart.getBodyPart(k).getInputStream(),                              
                                  writeAttachObj);            
               }          
            }          
            else          
            {            
               saveAttacheFile(contentTmp.getBodyPart(i).getContentType(),                            
                               contentTmp.getBodyPart(i).getDisposition(),                            
                               contentTmp.getBodyPart(i).getFileName(),                            
                               contentTmp.getBodyPart(i).getInputStream(),                            
                               writeAttachObj);          
            }        
         }      
      }      
      //这种情况中邮件是纯文本形式,并且没有附件      
      else      
      {        
         writeAttachObj.write((“content = ”+content+“\r\n”).getBytes());        
         writeAttachObj.flush();      
      }    
   }    
   catch (Exception ie)    
   {      
      System.out.println(“exception====” + ie.getMessage()) ;    
   }  
}

原文出处不祥,有知情者请与我联系

一. Input和Output
1. stream代表的是任何有能力产出数据的数据源,或是任何有能力接收数据的接收源。在Java的IO中,所有的stream(包括Input和Out stream)都包括两种类型:
1.1 以字节为导向的stream
以字节为导向的stream,表示以字节为单位从stream中读取或往stream中写入信息。以字节为导向的stream包括下面几种类型:
1) input stream:
1) ByteArrayInputStream:把内存中的一个缓冲区作为InputStream使用
2) StringBufferInputStream:把一个String对象作为InputStream
3) FileInputStream:把一个文件作为InputStream,实现对文件的读取操作
4) PipedInputStream:实现了pipe的概念,主要在线程中使用
5) SequenceInputStream:把多个InputStream合并为一个InputStream
2) Out stream
1) ByteArrayOutputStream:把信息存入内存中的一个缓冲区中
2) FileOutputStream:把信息存入文件中
3) PipedOutputStream:实现了pipe的概念,主要在线程中使用
4) SequenceOutputStream:把多个OutStream合并为一个OutStream
1.2 以Unicode字符为导向的stream
以Unicode字符为导向的stream,表示以Unicode字符为单位从stream中读取或往stream中写入信息。以Unicode字符为导向的stream包括下面几种类型:
1) Input Stream
1) CharArrayReader:与ByteArrayInputStream对应
2) StringReader:与StringBufferInputStream对应
3) FileReader:与FileInputStream对应
4) PipedReader:与PipedInputStream对应
2) Out Stream
1) CharArrayWrite:与ByteArrayOutputStream对应
2) StringWrite:无与之对应的以字节为导向的stream
3) FileWrite:与FileOutputStream对应
4) PipedWrite:与PipedOutputStream对应
以字符为导向的stream基本上对有与之相对应的以字节为导向的stream。两个对应类实现的功能相同,字是在操作时的导向不同。如CharArrayReader:和ByteArrayInputStream的作用都是把内存中的一个缓冲区作为InputStream使用,所不同的是前者每次从内存中读取一个字节的信息,而后者每次从内存中读取一个字符。
1.3 两种不现导向的stream之间的转换
InputStreamReader和OutputStreamReader:把一个以字节为导向的stream转换成一个以字符为导向的stream。
2. stream添加属性
2.1 “为stream添加属性”的作用
运用上面介绍的Java中操作IO的API,我们就可完成我们想完成的任何操作了。但通过FilterInputStream和FilterOutStream的子类,我们可以为stream添加属性。下面以一个例子来说明这种功能的作用。
如果我们要往一个文件中写入数据,我们可以这样操作:
FileOutStream fs = new FileOutStream(“test.txt”);
然后就可以通过产生的fs对象调用write()函数来往test.txt文件中写入数据了。但是,如果我们想实现“先把要写入文件的数据先缓存到内存中,再把缓存中的数据写入文件中”的功能时,上面的API就没有一个能满足我们的需求了。但是通过FilterInputStream和FilterOutStream的子类,为FileOutStream添加我们所需要的功能。
2.2 FilterInputStream的各种类型
2.2.1 用于封装以字节为导向的InputStream
1) DataInputStream:从stream中读取基本类型(int、char等)数据。
2) BufferedInputStream:使用缓冲区
3) LineNumberInputStream:会记录input stream内的行数,然后可以调用getLineNumber()和setLineNumber(int)
4) PushbackInputStream:很少用到,一般用于编译器开发
2.2.2 用于封装以字符为导向的InputStream
1) 没有与DataInputStream对应的类。除非在要使用readLine()时改用BufferedReader,否则使用DataInputStream
2) BufferedReader:与BufferedInputStream对应
3) LineNumberReader:与LineNumberInputStream对应
4) PushBackReader:与PushbackInputStream对应
2.3 FilterOutStream的各种类型
2.2.3 用于封装以字节为导向的OutputStream
1) DataIOutStream:往stream中输出基本类型(int、char等)数据。
2) BufferedOutStream:使用缓冲区
3) PrintStream:产生格式化输出
2.2.4 用于封装以字符为导向的OutputStream
1) BufferedWrite:与对应
2) PrintWrite:与对应
3. RandomAccessFile
1) 可通过RandomAccessFile对象完成对文件的读写操作
2) 在产生一个对象时,可指明要打开的文件的性质:r,只读;w,只写;rw可读写
3) 可以直接跳到文件中指定的位置
4. I/O应用的一个例子
import java.io.*;
public class TestIO{
public static void main(String[] args)
throws IOException{
//1.以行为单位从一个文件读取数据
BufferedReader in =
new BufferedReader(
new FileReader(“F:\\nepalon\\TestIO.java”));
String s, s2 = new String();
while((s = in.readLine()) != null)
s2 += s + “\n”;
in.close();

//1b. 接收键盘的输入
BufferedReader stdin =
new BufferedReader(
new InputStreamReader(System.in));
System.out.println(“Enter a line:”);
System.out.println(stdin.readLine());

//2. 从一个String对象中读取数据
StringReader in2 = new StringReader(s2);
int c;
while((c = in2.read()) != -1)
System.out.println((char)c);
in2.close();

//3. 从内存取出格式化输入
try{
DataInputStream in3 =
new DataInputStream(
new ByteArrayInputStream(s2.getBytes()));
while(true)
System.out.println((char)in3.readByte());
}
catch(EOFException e){
System.out.println(“End of stream”);
}

//4. 输出到文件
try{
BufferedReader in4 =
new BufferedReader(
new StringReader(s2));
PrintWriter out1 =
new PrintWriter(
new BufferedWriter(
new FileWriter(“F:\\nepalon\\ TestIO.out”)));
int lineCount = 1;
while((s = in4.readLine()) != null)
out1.println(lineCount++ + “:” + s);
out1.close();
in4.close();
}
catch(EOFException ex){
System.out.println(“End of stream”);
}

//5. 数据的存储和恢复
try{
DataOutputStream out2 =
new DataOutputStream(
new BufferedOutputStream(
new FileOutputStream(“F:\\nepalon\\ Data.txt”)));
out2.writeDouble(3.1415926);
out2.writeChars(“\nThas was pi:writeChars\n”);
out2.writeBytes(“Thas was pi:writeByte\n”);
out2.close();
DataInputStream in5 =
new DataInputStream(
new BufferedInputStream(
new FileInputStream(“F:\\nepalon\\ Data.txt”)));
BufferedReader in5br =
new BufferedReader(
new InputStreamReader(in5));
System.out.println(in5.readDouble());
System.out.println(in5br.readLine());
System.out.println(in5br.readLine());
}
catch(EOFException e){
System.out.println(“End of stream”);
}

//6. 通过RandomAccessFile操作文件
RandomAccessFile rf =
new RandomAccessFile(“F:\\nepalon\\ rtest.dat”, “rw”);
for(int i=0; i<10; i++)
rf.writeDouble(i*1.414);
rf.close();

rf = new RandomAccessFile(“F:\\nepalon\\ rtest.dat”, “r”);
for(int i=0; i<10; i++)
System.out.println(“Value ” + i + “:” + rf.readDouble());
rf.close();

rf = new RandomAccessFile(“F:\\nepalon\\ rtest.dat”, “rw”);
rf.seek(5*8);
rf.writeDouble(47.0001);
rf.close();

rf = new RandomAccessFile(“F:\\nepalon\\ rtest.dat”, “r”);
for(int i=0; i<10; i++)
System.out.println(“Value ” + i + “:” + rf.readDouble());
rf.close();
}
}
关于代码的解释(以区为单位):
1区中,当读取文件时,先把文件内容读到缓存中,当调用in.readLine()时,再从缓存中以字符的方式读取数据(以下简称“缓存字节读取方式”)。
1b区中,由于想以缓存字节读取方式从标准IO(键盘)中读取数据,所以要先把标准IO(System.in)转换成字符导向的stream,再进行BufferedReader封装。
2区中,要以字符的形式从一个String对象中读取数据,所以要产生一个StringReader类型的stream。
4区中,对String对象s2读取数据时,先把对象中的数据存入缓存中,再从缓冲中进行读取;对TestIO.out文件进行操作时,先把格式化后的信息输出到缓存中,再把缓存中的信息输出到文件中。
5区中,对Data.txt文件进行输出时,是先把基本类型的数据输出屋缓存中,再把缓存中的数据输出到文件中;对文件进行读取操作时,先把文件中的数据读取到缓存中,再从缓存中以基本类型的形式进行读取。注意in5.readDouble()这一行。因为写入第一个writeDouble(),所以为了正确显示。也要以基本类型的形式进行读取。
6区是通过RandomAccessFile类对文件进行操作。

发布日期: 09/19/2004 | 更新日期: 09/19/2004

Bob Beauchemin
DevelopMentor

适用于:
Microsoft ADO.NET 2.0
Microsoft Visual Studio 2005
C# 编程语言

摘要:了解在 ADO.NET 中对于从您的数据源访问元数据的增强支持。

下载相关的 SchemasSample.exe 示例代码。

本页内容
深入了解新的公共元数据 API 深入了解新的公共元数据 API
究竟谁需要元数据 究竟谁需要元数据?
我能得到什么样的元数据 我能得到什么样的元数据?
Restrictions Restrictions
DataSourceInformation DataSourceInformation
自定义并扩展元数据 自定义并扩展元数据
用户自定义 用户自定义
小结 小结:元数据支持的最终部分


深入了解新的公共元数据 API

我的前一篇文章,我指出 Visual Studio 2005 服务器资源管理器现在使用一个包含了 .NET 数据提供程序(而不是 OLE DB 提供程序)列表的对话框来提示连接信息。当您确定一个连接字符串并添加数据连接时,每个数据连接也显示一个关于通过连接直接可见的数据库对象(如表、视图和存储过程)的信息的树形结构。但这些信息来自哪里呢?难道是 Visual Studio 仅为某些数据提供程序硬编码来生成这个信息,而假如我编写我自己的数据提供程序或者从第三方购买一个的时候,就留给我一个空节点?不,在 Visual Studio 2005 中并不是这样。由于 ADO.NET 2.0 中的新架构 API,所有这些有用的信息都将提供给您。我不知道这是否是 Visual Studio 做到它的方法,但是这里就是使用新的 API 来获得一个数据库中的表的列表的代码。

// uses a ADO.NET 2.0 named connection string in config file // uses ADO.NET 2.0 ProviderFactory and base classes // see previous article public static void GetListOfTables(string connectstring_name) { ConnectionStringSettings s = ConfigurationSettings.ConnectionStrings[connectstring_name]; DbProviderFactory f = DbProviderFactories.GetFactory(s.ProviderName); using (DbConnection conn = f.CreateConnection()) { conn.ConnectionString = s.ConnectionString; conn.Open(); DataTable t = conn.GetSchema("Tables"); t.WriteXml("tables.xml"); } } 
返回页首返回页首


究竟谁需要元数据?

元数据是每个数据访问 API 的一部分。尽管元数据的主要使用者是像 Visual Studio 这样的工具或者像 DeKlarit 这样的代码生成器,但是它们并不是惟一的使用者。应用程序包设计师可能允许最终用户通过向现有的表添加新表或新列来自定义一个应用程序。当最终用户如此改变了数据库架构时,一个通用查询和修改工具可以在维护、备份和其他应用程序函数中使用元数据来包含用户的新表,就像它们是应用程序附带的内置表一样。程序员可以使用元数据来编写他们自己的派生自 System.Data.Common.DbCommandBuilder的自定义类,并为使用 DataSet 创建插入、更新和删除命令。多数据库应用程序(即设计为在用户选择的数据库上运行的应用程序)的构建者可以尽可能多地使用元数据来维护公共代码库,在需要时优化数据访问代码。

通过一般元数据 API 来公开元数据要比让每个使用者使用特定于数据库的 API 要好。这样,工具编写人员可以维护一个可管理性更好的代码库。这样的 API 还必须是非常灵活的,因为在编写一般 API 来公开元数据时有四个障碍需要考虑。

1.

元数据集合和信息在数据库间是有差别的。例如,SQL Server 用户可能想要公开一个链接服务器的集合,但是 Oracle 用户可能对关于 Oracle 序列的信息感兴趣。

2.

不仅在不同的数据库产品中,而且即使在相同数据库的不同版本中,存储公共数据库元数据的基础系统表都是不同的。例如,SQL Server 2005 使用在一个“sys”架构下的新表(例如,sys.tables)公开它的元数据,而 SQL Server 以前的版本使用元数据表(如 sysobjects£?来存储相同的数据。

3.

不同的程序可能有不同的元数据视图。以一个例子来说,许多程序员抱怨 Oracle 数据库中表的列表太长,因为大多数元数据 API 将“system”表与用户表混在一起。他们想要一个仅由他们定义的表组成的短列表。

4.

是否根本不支持元数据,要提供多少元数据,应该完全取决于提供程序的编写者。

大部分数据库 API 提供一个标准的元数据集,它是所有提供程序必须支持的,并且允许提供程序编写者添加新的元数据表。这与使用 ANSI SQL 标准采用的方法是一致的。解决这一问题的标准的一部分是 Schema Schemata(INFORMATION_SCHEMA 和 DEFINITION_SCHEMA)的第 11 部分。ANSI SQL INFORMATION_SCHEMA 定义了由一个兼容的数据库支持的标准的元数据视图集。但即便是这个规范也需要有一个方法来解决上面这些问题。它声明:

“实施者可以自由添加额外的表到 INFORMATION_SCHEMA 或添加额外的列到预定义的INFORMATION_SCHEMA 表。”

OLE DB 作为一个与 ANSI SQL 标准概念一致的数据访问 API 示例,定义了一系列的名为“架构行集合”的元数据。它从一个大致遵循 INFORMATION_SCHEMA 的预定义集开始,并添加 OLE DB 特定的列到每个行中。ADO.NET 2.0 提供了一个甚至更强大更灵活的机制来公开元数据。

返回页首返回页首


我能得到什么样的元数据?

ADO.NET 2.0 允许提供程序编写者公开 5 种不同类型的元数据。这些主要的元数据 — “元数据集合”或“类别” — 在 System.Data.Common.DbMetaDataCollectionNames 类中被枚举出来。

?

MetaDataCollections — 可用元数据集合的列表。

?

Restrictions — 对于每个元数据集合,存在一批可以用于限制被请求的架构信息范围的限定符。

?

DataSourceInformation — 关于数据提供程序引用的数据库实例的信息。

?

DataTypes — 一组关于数据库支持的每个数据类型的信息。

?

ReservedWords — 适用于该种数据库查询语言的保留字。通常“查询语言”等同于一种 SQL 的方言。

MetaDataCollections 是 INFORMATION_SCHEMA 集合的名称,如“表”、“列”或“主键”。但是,如果使用 DbConnection.GetSchema,这些元数据类别也被认为是元数据。这意味着在代码方面来说是这样,这些集合可以像普通的元数据一样被获得。

// gets information about database Views Table t1 = conn.GetSchema("Views"); // gets information about collections exposed by this provider // this includes the five "meta-collections" described above Table t2 = conn.GetSchema(DbMetaDataCollectionNames.MetaDataCollections); // gets information about the Restrictions meta-collection Table t3 = conn.GetSchema(DbMetaDataCollectionNames.Restrictions); // No argument overload is same as asking for MetaDataCollections Table t4 = conn.GetSchema(); 

5 个元数据集合中的 2 个值得进一步解释。

返回页首返回页首


Restrictions

Restrictions 可以用来限制返回元数据的数量。如果您熟悉 OLE DB 或 ADO,那么术语“restriction”意味着在那些 API 中同样的内容。作为示例,让我们使用 MetaDataCollection“列”,它是表中的列名称的集合。这个集合可以用于获取所有表中的所有列。但是,此被请求的列集合会被数据库名称、所有者/架构或者表限制。每个元数据集合可以有不同数量的可能限制,并且每个限制会有一个默认值。在我们下面的示例中,这里是一个对列元数据的限制的 XML 表示:

清单 1. 列集合上的 Restrictions(XML 格式)

<Restrictions> <CollectionName>Columns</CollectionName> <RestrictionName>Catalog</RestrictionName> <RestrictionDefault>table_catalog</RestrictionDefault> <RestrictionNumber>1</RestrictionNumber> </Restrictions> <Restrictions> <CollectionName>Columns</CollectionName> <RestrictionName>Owner</RestrictionName> <RestrictionDefault>table_schema</RestrictionDefault> <RestrictionNumber>2</RestrictionNumber> </Restrictions> <Restrictions> <CollectionName>Columns</CollectionName> <RestrictionName>Table</RestrictionName> <RestrictionDefault>table_name</RestrictionDefault> <RestrictionNumber>3</RestrictionNumber> </Restrictions> <Restrictions> <CollectionName>Columns</CollectionName> <RestrictionName>Column</RestrictionName> <RestrictionDefault>column_name</RestrictionDefault> <RestrictionNumber>4</RestrictionNumber> </Restrictions> 

Restrictions 是使用一个重载的 DbConnection.GetSchema 指定的。这些限制被指定为一个数组。您可以将一个数组指定为和整个限制集合或者一个子集数组一样大,因为“RestrictionNumbers”通常从最少限制向最多限制发展。对您想要省略的限制值使用空值(不是数据库 NULL,而是 .NET NULL,或者在 Visual Basic .NET 中的 Nothing)。例如:

// restriction string array string[] res = new string[4]; // all columns, all tables owned by dbo res[1] = "dbo"; DataTable t1 = conn.GetSchema("Columns", res); // clear collection for (int i = 0; i < 4; i++) res[i] = null; // all columns, all tables named "authors", any owner/schema res[2] = "authors"; DataTable t2 = conn.GetSchema("Columns", res); // clear collection for (int i = 0; i < 4; i++) res[i] = null; // columns named au_lname // all tables named "authors", any owner/schema res[2] = "authors"; res[3] = "au_lname"; DataTable t3 = conn.GetSchema("Columns", res); // clear collection for (int i = 0; i < 4; i++) res[i] = null; // columns named au_lname // any tables, any owner/schema res[3] = "name"; DataTable t4 = conn.GetSchema("Columns", res); 

您无需指定整个限制数组。在上面的情况中,这里您只想看到“dbo”拥有的表中的列,您可以指定一个只带有两个而不是全部四个成员的数组。也请注意,将一个空字符串指定为一个限制与指定一个 null(在 Visual Basic .NET 中为 Nothing)值是不同的。您不必记住这些限制,您始终可以查询它们,就像任何其他集合一样。“Restrictions”集合本身不允许限制,但是因为信息被提取到 DataTable 中,所以您可以使用一个 DataView 来提供相似的功能,如下所示。

DataTable tv = conn.GetSchema(DbMetaDataCollectionNames.Restrictions); DataView v = tv.DefaultView; // show restrictions on the "Columns" collection, sorted by number v.RowFilter = "CollectionName = 'Columns'"; v.Sort = "RestrictionNumber"; for (int i = 0; i < tv.Count; i++) Console.WriteLine("{0} (default){1}", tv.Rows[i]["RestrictionName"], tv.Rows[i]["RestrictionDefault"]); 
返回页首返回页首


DataSourceInformation

DataSourceInformation 集合为查询生成器提供关于当前数据库(数据源)实例的信息。虽然这个集合可包含提供程序需要的任何内容,但在 Microsoft 提供程序 (SqlClient、OracleClient、OleDb、Odbc) 中,这个集合包含相似的信息。这里是您默认获得的信息。

表 1. 在 Microsoft 提供程序中的 DataSourceInformation
格式/意义

CompositeIdentifierSeparatorPattern

多部分名称的分隔符(如 pubs.dbo.authors 中的点)

DataSourceProductName

数据库名称

DataSourceProductVersion

数据库版本。请注意这是当前通过 DbConnection 访问的数据库实例的版本。

DataSourceProductVersionNormalized

?

GroupByBehavior

枚举,System.Data.Common.GroupByBehavior

IdentifierPattern

正则表达式字符串

IdentifierCase

枚举,System.Data.Common.IdentifierCase

OrderByColumnsInSelect

布尔值,默认情况下您应该在一个 SELECT 语句中 ORDER BY 这些列

ParameterMarkerFormat

说明参数标记是否以一个特殊的字符开始(如 T-SQL 中的 @)

ParameterMarkerPattern

用于创建参数的正则表达式字符串

ParameterNameMaxLength

参数的最大长度

ParameterNamePattern

用于创建参数的正则表达式字符串

QuotedIdentifierPattern

用于引用标识符的正则表达式字符串

QuotedIdentifierCase

枚举,System.Data.Common.IdentifierCase

StatementSeparatorPattern

正则表达式字符串

StringLiteralPattern

正则表达式字符串

SupportedJoinOperators

枚举,System.Data.Common.SupportedJoinOperators

对特定的数据库方言来说有太多的信息来产生 SQL 了,您不这样认为么?还有一条信息我想要说一下,那就是提供程序是否在参数化查询中使用命名参数或者位置参数。在我关于编写独立于提供程序的代码的上一篇文章中,我将命名和位置参数作为编写参数化命令的两种方法来讨论。

返回页首返回页首


自定义并扩展元数据

既然我们已经看到了提供的基础元数据并且可以围绕 DbConnection.GetSchema() 找到我们的方法,让我们讨论提供程序编写者使用简单的声明性格式自定义元数据的方法,以及程序员如何能够挂钩到那种格式中。这个讨论与文章开始的元数据复杂性相关:如何提供独立于数据库版本的元数据以及如何处理不同使用者可能需要相同元数据的不同视图的事实。

首先,让我们指出元数据支持完全是可选的。提供程序不必支持 DbConnection.GetSchema,这种方法会引发 NotSupportedException。此外,如果提供程序编写者选择支持 DbConnection.GetSchema,只有 MetaDataCollections 类别是必需的。提供程序可以选择不提供任何或所有其他 4 个类别的信息。

其次,每个提供程序可以公开相同元数据集合的不同信息。例如,Tables 集合的结构完全取决于提供程序编写者。举个例子,SqlClient 提供程序在 Tables 集合中公开了 4 个信息项:table_catalog、table_schema、table_name 和 table_type。OracleClient 提供程序仅公开了 3 个信息项(OWNER、TABLE_NAME 和 TYPE),因为 Oracle 数据库不包含多种目录。对每个提供程序来说限制和限制项的数量可以是不同的。再次以表的情况为例,SqlClient 提供程序支持 4 个限制,而 OracleClient 提供程序只支持 2 个。限制也不必按照任何指定的顺序发生。所以与在 OLE DB 和 ODBC API 中不同,这里没有委托管理的元数据结构、大量元数据或者元数据顺序。提供程序可以自由公开任何相关的元数据。但是,如果一个特定的应用程序(如 Visual Studio)要求在一个应用程序里使用的所有 .NET 数据提供程序中元数据是一致的,它也能够通过覆盖提供程序的标准行为来获得这个行为。我们将在后面的用户自定义小节进一步讨论这个问题。

提供程序编写者可以将元数据逻辑直接硬编码到他们的提供程序中,每个提供程序编写者为了获得相似的元数据可以使用不同的内部算法。例如,在实现 OLE DB 的 ISchemaRowset 方法时,这就是过去完成它的方法。但是,在 ADO.NET 2.0 中,在 System.Data.ProviderBase 命名空间中有一些基类是提供程序编写者可用的。4 个可用的 Microsoft 提供程序使用这些基类,因此它们都以相似的方法实现架构。我将用这个实现进行讲解,希望大多数编写提供程序的程序员喜欢 DataDirect 技术,而其他提供程序编写者也使用它。

公开元数据的基类是 DbMetaDataFactory。实现它的一个子类的提供程序使用一个 XML 文件来定义其元数据提取行为。这些文件是在 System.Data.dll 和 System.Data.OracleClient.dll 中嵌入的资源,您可以通过从命令行运行 ILDASM.exe 来查看原始的 XML 文件。

>ildasm.exe System.Data.dll /out:dummy.il 

查看从 ILDASM 生成的 XML 资源文件,如同剥开了洋葱的另一层。这个文件枚举了被支持的集合以及包含在每个元数据集合(通过架构)中的信息,并且看起来是使用 DataSet.WriteXml(XmlWriteMode.WriteSchema) 重载的 DataSet.WriteXml 方法的输出。最有趣的部分是在除了 DataSourceInformation 和 MetaDataCollections 元素中的 PopulationMechanism/PopulationString 子元素以外的所有元数据集合中的 MinimumVersion/MaximumVersion 元素。

使用 MinimumVersion/MaximumVersion 允许提供程序编写者指定对于不同版本的数据库要执行哪些元数据查询。通过使用单个 MetaDataCollection 的多个元素,您可以使 GetSchema 对不同版本的数据库表现不同。举一个明显的例子,您可以使用与以前版本的 SQL Server 不同的 SQL Server 2005 各版本。这里是一个使用来自 SQL Server 元数据资源 (System.Data.SqlClient.SqlMetaData) 的 MinimumVersion 的例子:

清单 2. 数据类型集合中的数据类型 XML 的元数据项

<DataTypes> <TypeName>xml</TypeName> <ProviderDbType>25</ProviderDbType> <ColumnSize>2147483647</ColumnSize> <DataType>System.String</DataType> <IsAutoIncrementable>false</IsAutoIncrementable> <IsCaseSensitive>false</IsCaseSensitive> <IsFixedLength>false</IsFixedLength> <IsFixedPrecisionScale>false</IsFixedPrecisionScale> <IsLong>true</IsLong> <IsNullable>true</IsNullable> <IsSearchable>true</IsSearchable> <IsSearchableWithLike>false</IsSearchableWithLike> <MinimumVersion>09.00.000.0</MinimumVersion> <IsLiteralSupported>false</IsLiteralSupported> </DataTypes> 

这定义了关于 SQL Server 数据类型 XML 的信息。MinimumVersion 指示出,这个数据类型只在使用 SQL Server 2005 时可用。如果您要求 SqlConnection.GetSchema 提供数据库支持的数据类型列表,只有 SQL Server 2005 的各版本数据库(SQL Server 2005 是版本 9,当前的beta 2 版是 09.00.852.2)会报告它们支持 XML 数据类型。

对于通常被 INFORMATION_SCHEMA(如表、视图或存储过程)公开的集合,PopulationMechanism 和 PopulationString 是工作开始的地方。在这个实现中使用了三个 PopulationMechanisms:DataTableSQLCommandPrepareCollection。DataTable 用于填充元数据集合。使用 DataTable 意味着用于填充集合的信息是在 XML 资源文件本身当中。在每种情况下,PopulationString 是当 XML 资源文件加载到一个 .NET DataSet 时,生成的 DataTable 的名称。SQLCommand 意味着提供程序将使用一个 DbCommand 实例来发出对数据库的命令。如果您查看由 SQLCommand 产生的集合的一个 PopulationString:

清单 3. SQL Server 中的数据库(目录)项 — MetaDataCollection

<MetaDataCollections> <CollectionName>Databases</CollectionName> <NumberOfRestrictions>1</NumberOfRestrictions> <NumberOfIdentifierParts>1</NumberOfIdentifierParts> <PopulationMechanism>SQLCommand</PopulationMechanism> <PopulationString>select name as database_name, dbid, crdate as create_date from master..sysdatabases where name = {0}</PopulationString> </MetaDataCollections> 

当限制在 DbConnection.GetSchema 中使用时,很容易就能够推断出字符串替换将被应用到“基查询”中。如果没有限制被指定,那么实际上该谓词将被剥离出查询。

当 PopulationMechanism 的值为 PrepareCommand 时,提供程序编写者可以使用自定义机制。有一个 DbMetaDataFactory 的 PrepareCommand 方法,如果它被提供程序编写者覆盖,就可以被编码来使用提供程序选择的任何自定义语义。这一机制被用在 SqlClient 中来生成 DataTypes 元数据集合。PrepareCommandSqlMetaDataFactory 子类实现首先从 DataTable 中读取由 SQL Server 支持的内置数据类型,如同用其他元数据集合一样;然后,如果数据库是 SQL Server 2005 的话,使用自定义逻辑来将用户定义的类型添加到集合中。(注:SQL Server 2005 可以将 .NET 类作为用户定义的类型公开。更多信息请参阅 A First Look at SQL Server 2005 for Developers 的第 5 章。)

返回页首返回页首


用户自定义

除了提供程序自定义机制外,也有允许程序员在每个应用程序基础上自定义架构信息的挂钩!在加载嵌入资源之前,DbConnectionFactory CreateMetaDataFactory 将参考应用程序配置文件。每个提供程序都可以用任何它选择的方法实现 CreateMetaDataFactory 来为其 DbMetaDataFactory 获取 XML 流,但是那 4 个 Microsoft 提供程序遵循一个公共模式。每个 Microsoft 提供程序将查找一个按提供程序本身命名的应用程序配置设置(如 system.data.sqlclient)。在这个设置元素中您可以添加或删除名值对。DbMetaDataFactory 查找一个名称“MetaDataXml”。与特殊名称对应的值是一个文件的名称。这是一个简单的文件名 — 这个文件必须存在于 .NET 安装位置的 CONFIG 子目录中。这是 machine.config 和安全配置设置所在的目录。这个文件必须包含整个的架构配置信息集,而不仅仅是那些更改。

出于多个原因,对于支持此机制的提供程序,您可以使用这个机制。例如,您可以更改 OracleClient 提供程序中的架构查询来使用“USER”目录视图而不是“ALL”目录视图。因为“USER”视图不包含关于内部数据库表的信息,例如,表列表将会短很多并且更易于使用。另一个示例可能包括为所有的 .NET 数据提供程序编码输出元数据 XML 文件,这提供给您一致的标准元数据集,该元数据集可能正好符合 SQL-99 INFORMATION_SCHEMA 视图。这可能恰恰适合您的应用程序。

一个更具体的例子是,如果我想要公开 SQL Server 2005 中关于 SQL Server Service Broker 的元数据集合的信息。这些集合可以包括 QUEUE、SERVICE、CONTRACT 和消息类型。我会从嵌入的 XML 资源文件开始并用我的新集合上的信息修饰它。如果这个文件的名称是 SQLBrokerAware.xml,我会安装这个文件,我的应用程序配置文件将为如下所示:

<?xml version="1.0" encoding="utf-8" ?> <configuration> <system.data.sqlclient> <settings> <add name="MetaDataXml" value="SQLBrokerAware.xml"> </add> </settings> </system.data.sqlclient> </configuration> 

这就是所有的步骤。使用这个设置,我可以编写代码 — 在其中 Service Broker 元数据是可用于客户端的内置元数据的组成部分。对所有队列来说,代码可能如下所示:

using (SqlConnection conn = new SqlConnection(connstring)) { conn.Open(); // this includes Service Broker metadata collections Table t = conn.GetSchema(DbMetaDataCollectionNames.MetaDataCollections); // get all the queues in my database Table queues = conn.GetSchema("Queues"); } 

真是非常酷!本文包含一个添加 Service Broker 元数据的代码示例。尽管这是一个非常强大的功能,它还是有可能被滥用的。请记住您需要将元数据 XML 文件分发到每个使用它的应用程序,并说服系统管理员为您将其安装在 CONFIG 目录中。而且您需要用发布的提供程序的每个新版本来维护它。因为使用一般元数据 API 的理由之一是可拥有跨数据库和应用程序的一致元数据,所以这个功能不应被无故使用。还需注意此时您不能提供 PrepareCommand 的自定义实现。

关于自定义的最后要注意的,您可能已经猜到了,自定义和资源对于 OLE DB 和 ODBC 的桥提供程序的使用是不同的。当您使用这些提供程序时,默认的 Odbc 或 OleDb XML 资源文件被使用,然后您不仅可以自定义主要的 Odbc 或 OleDb 架构行为,而且可以在每个提供程序基础上自定义行为。如果您想要指定自己的提供程序或者驱动程序,在添加/删除设置子元素中使用的名称属性不能是 MetaDataXml,而应该是 [providershortname]:MetaDataXml。如果您想让您的文件成为 OleDb 或 Odbc 数据提供程序的默认文件,您甚至可以指定 defaultMetaDataXml 的名称。

返回页首返回页首


小结:元数据支持的最终部分

在结束前,我只想说说其他两个不是通过 DbConnection.GetSchema 公开的元数据扩展。 DbCommandBuilder 包含两个属性,QuoteIdentifierUnquoteIdentifier,它们允许您在由 CommandBuilder 生成的命令中自定义标识符。举个例子,根据您的会话设置,在 SQL Server 中您可以使用双引号 (“) 或括号(’[' 和 ']‘)来引用标识符。最后,在 System.Data.Sql 命名空间中的 SqlMetaData 类被用于公开 SQL Server DataReader 中的元数据并允许您在 SqlCommand 使用的 Parameters 中设置这个元数据。虽然不是任何地方在细节上都非常接近,但是这个元数据在概念上与在 DataTypes 元数据集合中公开的元数据相似。SqlMetaData 对 SqlClient 数据提供程序和 SQL Server 2005 包括的 in-database SqlServer 数据提供程序都是适用的。SqlMetaData 增大了当前通过 SqlDataReader.GetSchemaTable() 方法公开的行集合元数据。

此时您很可能同意 ADO.NET 2.0 元数据基础结构是您所见过的最强大、灵活和可自定义的结构。它将 ADO.NET 发展成为一个完全面向对象的数据库 API。工具供应商、提供程序编写者和 Visual Studio 数据工具用户将会高兴地在街上跳舞 — 我们那里见。

Bob Beauchemin,DevelopMentor 的讲师、课程作者和数据库课程联络员。作为以数据为中心的分布式系统的架构师、程序员和管理员,他有超过 25 年的经验。他一直为 Microsoft Systems Journal 和 SQL Server Magazine 等杂志撰写 ADO.NET、OLE DB 和 SQL Server 方面的文章,并且是 A First Look at SQL Server 2005 for DevelopersEssential ADO.NET 的作者。

转到原英文页面


返回页首返回页首

下载 ColumnDragDataGrid.msi 文件

本页内容
简介 简介
入门 入门
ScreenImage 类 ScreenImage 类
DraggedDataGridColumn 类 DraggedDataGridColumn 类
ColumnDragDataGrid 类 ColumnDragDataGrid 类
列跟踪 列跟踪
重写 DataGrid 的 OnPaint 方法 重写 DataGrid 的 OnPaint 方法
小结 小结


简介

几个月以前,当我初到 Microsoft 工作时,我的经理走进我的办公室,并且向我详细说明了我将在随后两个星期内将要从事的一个项目。我需要设想出一个应用程序,用于为 MSDN 内容策划人员整合衡量标准。其中一个功能要求是需要一个类似于 DataGrid 的控件,该控件使用户可以在将数据导出到 Microsoft Excel 电子表格之前,按照他们喜欢的顺序排列所有列。他在离开我的办公室之前说的最后一句话是:“将它变为有趣的用户体验。”

我知道为了能够重新排列 DataGrid 列,我必须操纵 DataGrid 的 DataGridColumnStyle 属性以反映新的列排序,但这并没有什么吸引人之处。我想要的是对整个拖动操作实现可视化表示。我在开始时使用了一些 System.Drawing 功能,并且达到了能够在屏幕间拖动图形的程度。我断定我需要让它更进一步。我可以让它看起来更像是用户在拖动列,而不是仅仅在 DataGrid 绘图表面上拖动枯燥乏味的矩形进行绘制。我对本机 GDI 库进行了一番寻根究底,经过几个小时的试验后,我终于弄清楚为了实现这一技巧而需要完成的工作。

dragdrop_datagrid

1. 拖动操作

返回页首返回页首


入门

我需要做的第一件事是弄清如何获得将要拖动列的屏幕快照。我完全清楚自己需要什么以及希望做什么,但是我不知道如何 去做。在发现 System.Drawing 命名空间下的类没有为我提供执行屏幕捕获所需的功能之后,我浏览了本机 GDI 库并且发现 BitBlt 函数正是我在寻找的东西。

下一步是编写该函数的托管包装。我将在本文中讨论的第一点是,我该如何实现 ScreenImage 类。

返回页首返回页首


ScreenImage 类

为了跨越互操作边界进行调用,我们需要声明非托管函数并且指明它们都来自哪些库,以便 JIT 编译器在运行时知道它们的位置。在完成这一工作后,我们只需像调用托管方法一样调用它们,就象下面的代码块所示。

public sealed class ScreenImage { [DllImport("gdi32.dll")] private static extern bool BitBlt( IntPtr handlerToDestinationDeviceContext, int x, int y, int nWidth, int nHeight, IntPtr handlerToSourceDeviceContext, int xSrc, int ySrc, int opCode); [DllImport("user32.dll")] private static extern IntPtr GetWindowDC( IntPtr windowHandle ); [DllImport("user32.dll")] private static extern int ReleaseDC( IntPtr windowHandle, IntPtr dc ); private static int SRCCOPY = 0x00CC0020; public static Image GetScreenshot( IntPtr windowHandle, Point location, Size size ) { ... } } 

该类只公开一个方法 — GetScreenshot,它是一个静态方法,返回一个含有与 windowHandle、location 和 size 参数相对应的颜色数据的图形对象。下一个代码块显示如何实现该方法。

public static Image GetScreenshot( IntPtr windowHandle, Point location, Size size ) { Image myImage = new Bitmap( size.Width, size.Height ); using ( Graphics g = Graphics.FromImage( myImage ) ) { IntPtr destDeviceContext = g.GetHdc(); IntPtr srcDeviceContext = GetWindowDC( windowHandle ); // TODO: throw exception BitBlt( destDeviceContext, 0, 0, size.Width, size.Height, srcDeviceContext, location.X, location.Y, SRCCOPY ); ReleaseDC( windowHandle, srcDeviceContext ); g.ReleaseHdc( destDeviceContext ); } // dispose the Graphics object return myImage; } 

让我们逐行地考察一下方法实现。我做的第一件事是创建一个尺寸与参数设置的大小相对应的新位图。

Image myImage = new Bitmap( size.Width, size.Height ); 

下面的代码行检索与刚刚创建的新位图相关联的绘图表面。

using ( Graphics g = Graphics.FromImage( myImage ) ) { ... } 

C# using 关键字定义了一个范围,在该范围的末尾将处理 Graphics 对象。因为 System.Drawing 命名空间中的所有类都是本机 GDI+ API 的托管包装,所以我们几乎总是在处理非托管资源,并且因此需要确保丢弃不再需要其服务的资源。该过程称为确定性终止,通过该过程将对象使用的资源以其他目的立即进行重新分配,而不是等待垃圾回收器到访来完成它该做的工作。每当您在处理实现了 IDisposable 接口的对象(如,这里使用的 Graphics 对象)时,都应该遵守这种习惯。

我检索了源和目标设备上下文的句柄,以便可以继续转换颜色数据。源是与参数设置的 windowHandle 句柄相关联的设备上下文,而目标是先前创建的位图中的设备上下文。

IntPtr srcDeviceContext = GetWindowDC(windowHandle); IntPtr destDeviceContext = g.GetHdc(); 

提示设备上下文是由 Windows 在内部维护的 GDI 数据结构,它定义了一组图形对象以及影响与这些对象相关的输出的图形模式。可以将其视为 Windows 提供的、可在上面绘图的画布。GDI+ 提供了三种不同的绘图表面:窗体(通常称为显示、打印机和位图)。在本文中,我们使用窗体和位图绘图表面。

现在,我们具有一个已定义的 Bitmap 对象 (myImage) 和一个表示该对象的画布(它在这一执行时刻是透明的)的设备上下文。本机 BitBlt 方法需要我们要向其复制位的画布部分的坐标和大小,以及我们要从源设备上下文上开始复制位的坐标。该方法还需要一个光栅操作代码值,以便定义位块的转换方式。

这里,我将目标设备上下文的起始坐标设置为左上角,并且将光栅操作代码值设置为 SRCCOPY(它表示要将源直接复制到目标)。十六进制等效值 (00×00CC0020) 可从 GDI 头文件中检索获得。

BitBlt( destDeviceContext, 0, 0, size.Width, size.Height, srcDeviceContext, location.X, location.Y, SRCCOPY ); 

一旦用完设备上下文,我们就需要将其释放。如果不这样做,将导致无法将该设备上下文用于随后的请求,并且可能导致引发运行时异常。

ReleaseDeviceContext( windowHandle, destDeviceContext ); g.ReleaseHdc( srcDeviceContext ); 

我确认了 ScreenImage 类能够按预期工作,然后,我需要做的下一件事情是创建一个简单的数据结构,以便帮助我跟踪与被拖动的列相关的所有数据。

返回页首返回页首


DraggedDataGridColumn 类

DraggedDataGridColumn 类是一个数据结构,用于监视所拖动列的各种状态,包括初始位置、当前位置、图像表示形式以及相对于该列的初始起点的光标位置。有关所有参数的详细说明,请参阅 DraggedDataGridColumn.cs 中的代码。

提示如果类封装了实现 IDisposable 的对象,那么您就可能间接抓住非托管资源。在这种情况下,类还应该实现 IDisposable 接口并且对每个可处置的对象调用 Dispose() 方法。DraggedDataGridColumn 类封装了一个 Bitmap 对象,该对象显式抓住非托管资源,因此我必须完成这一步骤。

在处理好这一问题之后,我就能够集中精力来解决有关难题的最主要部分了,即操纵 DataGrid 控件以获得我需要的可视效果。

返回页首返回页首


ColumnDragDataGrid 类

DataGrid 控件是一个功能强大的重量级控件,但它本身不会向我们提供拖放列的能力,所以我必须扩展它并且自己来添加该功能。我处理了三个不同的鼠标事件,并且重写了 DataGrid 的 OnPaint 方法来满足我的所有绘图需要。

首先,让我们看看所有用于跟踪应该在何处以及如何进行绘制的成员字段。

成员字段 定义

m_initialRegion

一个 DraggedDataGridColumn 对象,表示有关当前正在拖动的列的所有相关内容。我将在本文后面详细讨论 DraggedDataGridColumn 类的细节。

m_mouseOverColumnRect

一个 Rectangle 结构,用于标识一个矩形区域,该区域表示鼠标光标当前正在哪个列上方悬停。

m_mouseOverColumnIndex

鼠标光标当前正在其上方悬停的列的索引。

m_columnImage

在启动拖放操作时包含列的位图表示形式的 Bitmap 对象。

m_showColumnWhileDragging

一个 Boolean 值,表示拖动列时是否应该显示捕获到的列图像。通过 ShowColumnWhileDragging 属性公开。

m_showColumnHeaderWhileDragging

一个 Boolean 值,表示当列被拖动时是否应该显示该列的头部。这是通过 ShowColumnHeaderWhileDragging 属性公开的。

该类中的唯一构造函数是一个不带参数的构造函数,并且相当简单。但是,我觉得有一行代码值得一提:

this.SetStyle( ControlStyles.DoubleBuffer, true ); 

Windows 中的绘图过程分为两步。当应用程序进行绘图请求时,系统将生成绘图消息(先是 WM_ERASEBKGND,然后是 WM_PAINT)。这些消息被发送到应用程序消息队列中,然后,应用程序将在这里检查这些消息并将它们发送到适当的控件以便进行处理。WM_ERASEBKGND 消息的默认处理方式是用当前窗口背景色填充该区域。随后将处理 WM_PAINT,这会完成所有前景绘图。当您的操作序列涉及到清除背景以及在前景中绘图时,您将产生被称为闪烁 的令人不快的效果。值得庆幸的是,可以通过使用双缓冲 来减轻这一效果。

对于双缓冲,您有两种不同的可以写入的缓冲。一种是存储在视频 RAM 中的可见的屏幕缓冲;另一种是不可见的离屏缓冲,它由内部 GraphicsBuffer 对象表示,并且存储在系统 RAM 中。当绘图操作启动时,将在上述 GraphicsBuffer 对象上呈现所有图形对象。一旦系统确定该操作已完成,就会快速同步这两个缓冲区。

根据 .NET Framework 文档资料,为了在应用程序中实现双缓冲,您需要将 AllPaintingInWmPaintDoubleBufferUserPaintControlStyle 位设置为真。这里,我只需慎重考虑 DoubleBuffer 位。基类 DataGrid 已经将 AllPaintingInWmPaintUserPaint 位设置为真。

上面提到的另外两个 ControlStyle 位被定义为:

UserPaint该位设置为真时,会告诉 Windows 应用程序将完全负责该特定窗口(控件)的所有绘图。这意味着您将处理 WM_ERASEBKGND 和 WM_PAINT 消息。如果该位被设置为假,则应用程序仍将挂钩 WM_PAINT 消息,但它会将该消息发送回系统以进行处理,而不会执行任何绘图操作。当发生这种情况时,系统将尝试呈现该窗口,但是因为它不了解有关该窗口的任何信息,所以它的工作通常不会令人感到满意。

AllPaintingInWmPaint正如该位的名称所表明的那样,当该位被设置为真时,所有绘图都将由控件的 WmPaint 方法进行处理。即使挂钩了 WM_ERASEBKGND 消息,该消息也将被忽略,并且永远不会调用控件的 OnEraseBackground 方法。

在深入研究该类的其余部分之前,需要回顾两个重要的概念。

无效

当您使控件的特定区域无效时,该区域将被添加到控件的更新区域,以便告诉系统在下一个绘图操作过程中重新绘制哪个区域。如果更新区域未定义,则将重新绘制整个控件。

dragdrop_datagrid

2. 触发绘图操作前后无效区域的可视表示形式。在左侧,带有虚线边框的半透明灰色方形表示已定义的无效区域。右侧的方形显示了在执行绘图操作之后的外观。

正如前面提到的那样,当调用控件的无效方法时,系统将生成 WM_PAINT 消息并将其发送给控件。在收到该消息以后,该控件将引发 Paint 事件;如果已经注册了侦听该事件的处理程序,则会将该事件添加到该控件的事件处理队列的后面。

需要注意的是,被引发的 Paint 事件并不总是能够立即得到处理。这有很多原因,其中最重要的一点是 Paint 事件涉及到绘图中开销比较大的操作之一,并且通常是最后得到处理的事件。

网格样式

DataGridTableStyle 定义了将 DataGrid 绘制 到屏幕上的方式。即使它包含的属性类似于 DataGrid 的属性,它们也是互相排斥的。许多人错误地认为更改同名属性(如 DataGrid 的 RowHeadersVisible 属性)也会更改 DataGridTableStyle 的 RowHeadersVisible 属性的值。结果,当情况没有按预期的那样发展时,需要花费始料未及的时间来进行调试(不要担心,我也会犯这样的错误)。

您可以创建一个包含不同表格样式的集合,并且将它们交替用于不同的数据实体和源。

每个 DataGridTableStyle 都包含一个 GridColumnStylesCollection,它是在将数据绑定到 DataGrid 控件时自动创建的 DataGridColumnStyles 对象的集合。这些对象是 DataGridBoolColumnDataGridTextBoxColumn 或由第三方实现的列(它们都派生自 DataGridColumnStyle)的实例。如果您需要一个包含标签甚至图像的列,则您将必须通过创建 DataGridColumnStyle 的子类来创建一个自定义类。

提示您需要重写 OnDataSource 方法(该方法在 DataGrid 控件被绑定到数据源时调用)。这样,您就可以使用多个样式,并且将它们的映射名称与 DataGrid 的 DataMember 属性值(该值在控件被绑定到数据源时设置)相关联。

返回页首返回页首


列跟踪

绝大部分的列跟踪功能都发生在 MouseDownMouseMoveMouseUp 事件处理程序中。在下面的段落中,我将重点讨论这三个事件处理程序,并且对比较重要的代码段进行解释。这些处理程序所利用的 Helper 方法未予讨论。但是,如果看了代码,您就会发现我已经提供了这些方法的摘要。

MouseDown

当用户在网格上方单击鼠标时,我们需要做的第一件事就是确定单击鼠标的位置。为了启动拖动操作,必须在列标头的上方单击光标。如果证明该条件为真,则将收集一些列信息。我们需要知道该列的起点、宽度和高度,以及相对于列起点的鼠标光标位置。该信息用于建立在列被拖动时要跟踪的两个不同的列区域。

Private void ColumnDragDataGrid_MouseDown(object sender, MouseEventArgs e) { DataGrid.HitTestInfo hti = this.HitTest( e.X, e.Y ); if ( ( hti.Type & DataGrid.HitTestType.ColumnHeader ) != 0 && this.m_draggedColumn == null ) { int xCoordinate = this.GetLeftmostColumnHeaderXCoordinate( hti.Column ); int yCoordinate = this.GetTopmostColumnHeaderYCoordinate( e.X, e.Y ); int columnWidth = this.TableStyles[0].GridColumnStyles[hti.Column].Width; int columnHeight = this.GetColumnHeight( yCoordinate ); Rectangle columnRegion = new Rectangle( xCoordinate, yCoordinate, columnWidth, columnHeight ); Point startingLocation = new Point( xCoordinate, yCoordinate ); Point cursorLocation = new Point( e.X - xCoordinate, e.Y - yCoordinate ); Size columnSize = Size.Empty; ... } ... } 
dragdrop_datagrid

3. 列起点、列标头高度(通过 GetColumnHeaderHeight 方法计算)、列高度、列宽度和光标位置示意图

该事件处理程序的其余部分相当简单。执行了一个条件计算以了解是否已经将 ShowColumnsWhileDraggingShowColumnHeaderWhileDragging 属性设置为真。如果是,则计算列大小并且调用 ScreenImage 的 GetScreenshot 方法。我传递了 DataGrid 控件的句柄(记住,控件是子窗口)、起始坐标和列大小,而该方法返回一个包含所需捕获区域的图形对象。然后,所有信息都被存储在一个 DraggedDataGridColumn 对象中。

Private void ColumnDragDataGrid_MouseDown(object sender, MouseEventArgs e) { ... if ( ( hti.Type & DataGrid.HitTestType.ColumnHeader ) != 0 && this.m_draggedColumn == null ) { ... if ( ShowColumnWhileDragging || ShowColumnHeaderWhileDragging ) { if ( ShowColumnWhileDragging ) { columnSize = new Size( columnWidth, columnHeight ); } else { columnSize = new Size( columnWidth, this.GetColumnHeaderHeight( e.X, yCoordinate ) ); } Bitmap columnImage = ( Bitmap ) ScreenImage.GetScreenshot( this.Handle, startingLocation, columnSize ); m_draggedColumn = new DraggedDataGridColumn( hti.Column, columnRegion, cursorLocation, columnImage ); } else { m_draggedColumn = new DraggedDataGridColumn( hti.Column, columnRegion, cursorLocation ); } m_draggedColumn.CurrentRegion = columnRegion; } ... } 

MouseMove

每当鼠标光标在 DataGrid 上方移动时,都会引发 MouseMove 事件。在处理该事件的过程中,我首先跟踪被拖动的列当前在其上方悬停的列,以便可以向用户提供一些可视反馈。其次,我跟踪该列的新位置并发出无效指令。

让我们进一步考察一下代码。我需要做的第一件事是确保列被拖动,然后我通过从相对于控件的鼠标坐标中减去相对于列起点的鼠标坐标来获得该列的 x 坐标(图 4,刻度线标志 #1)。这可以为我提供该列的 x 坐标。因为 y 坐标永远不会更改,所以我不必花费功夫来检查它。

private void ColumnDragDataGrid_MouseMove(object sender, MouseEventArgs e) { DataGrid.HitTestInfo hti = this.HitTest( e.X, e.Y ); if ( m_draggedColumn != null ) { int x = e.X - m_draggedColumn.CursorLocation.X; ... } } 
dragdrop_datagrid

4. 刻度线标志 #1 显示了 m_draggedColumn.CursorLocation.X 中存储的值。该值从当前光标位置(其坐标相对于控件)中减去。

然后,我检查光标是否悬停在单元格的上方(列标头也被视为单元格)。如果不是,则我假设用户希望中止拖动操作。

private void ColumnDragDataGrid_MouseMove(object sender, MouseEventArgs e) { ... if ( m_draggedColumn != null ) { if ( hti.Column >= 0 ) { ... } else { InvalidateColumnArea(); ResetMembersToDefault(); } } } 

接下来,我希望向用户提供某种反馈,以便他们知道所拖动的列将在他们释放鼠标按键时放置到何处。

这是通过 m_mouseOverColumnIndex 成员字段进行跟踪的,该字段存储了以下列的索引:该列的边界包含光标在处理最后一个 MouseMove 事件之后的当前位置。如果该值不同于点击测试为我们提供的列索引,则用户正在将鼠标悬停在不同列的上方。如果是这样,则将使 m_mouseOverColumnRect 成员字段指示的区域无效,并且记录新区域的坐标。然后,使该新区域无效,以便 Windows 知道这一等待它关注区域的新绘图指令。

private void ColumnDragDataGrid_MouseMove(object sender, MouseEventArgs e) { ... if ( m_draggedColumn != null ) { ... if ( hti.Column >= 0 ) { if ( hti.Column != m_mouseOverColumnIndex ) { // NOTE: moc = mouse over column int mocX = this.GetLeftmostColumnHeaderXCoordinate( hti.Column ); int mocWidth = this.TableStyles[0].GridColumnStyles[hti.Column].Width; // indicate that we want to invalidate the old rectangle area if ( m_mouseOverColumnRect != Rectangle.Empty ) { this.Invalidate( m_mouseOverColumnRect ); } // if the mouse is hovering over the original column, we do not want to // paint anything, so we negate the index. if ( hti.Column == m_draggedColumn.Index ) { m_mouseOverColumnIndex = -1; } else { m_mouseOverColumnIndex = hti.Column; } m_mouseOverColumnRect = new Rectangle( mocX, m_draggedColumn.InitialRegion.Y, mocWidth, m_draggedColumn.InitialRegion.Height ); // invalidate this area so it gets painted when OnPaint is called. this.Invalidate( m_mouseOverColumnRect ); } ... } else { ... } } } 

随后,将变换焦点以有助于跟踪被拖动列的位置。我需要弄清楚是向左还是向右拖动它,以便我可以获得最左边的 x 坐标。在获得该值后,将使列的旧区域和新区域无效,并且将与新位置相关的数据存储在 m_draggedColumn 中。

private void ColumnDragDataGrid_MouseMove(object sender, MouseEventArgs e) { ... if ( m_draggedColumn != null ) { ... if ( hti.Column >= 0 ) { ... int oldX = m_draggedColumn.CurrentRegion.X; Point oldPoint = Point.Empty; // column is being dragged to the right if ( oldX < x ) { oldPoint = new Point( oldX - 5, m_draggedColumn.InitialRegion.Y ); // to the left } else { oldPoint = new Point( x - 5, m_draggedColumn.InitialRegion.Y ); } Size sizeOfRectangleToInvalidate = new Size( Math.Abs( x - oldX ) + m_draggedColumn.InitialRegion.Width + ( oldPoint.X * 2 ), m_draggedColumn.InitialRegion.Height ); this.Invalidate( new Rectangle( oldPoint, sizeOfRectangleToInvalidate ) ); m_draggedColumn.CurrentRegion = new Rectangle( x, m_draggedColumn.InitialRegion.Y, m_draggedColumn.InitialRegion.Width, m_draggedColumn.InitialRegion.Height ); } else { ... } } } 

MouseUp

当用户在单元格上方松开鼠标按键时,将执行条件计算,以确保将拖动的列放置到除了其发送方之外的列的上方。如果列索引中表达式计算为真(该列索引不是产生它的列的索引),则切换列。否则,将重新绘制该网格。

private void ColumnDragDataGrid_MouseUp(object sender, MouseEventArgs e) { DataGrid.HitTestInfo hti = this.HitTest( e.X, e.Y ); // is column being dropped above itself? if so, we don't want // to do anything if ( m_draggedColumn != null && hti.Column != m_draggedColumn.Index ) { DataGridTableStyle dgts = this.TableStyles[this.DataMember]; DataGridColumnStyle[] columns = new DataGridColumnStyle[dgts.GridColumnStyles.Count]; // NOTE: csi = columnStyleIndex for ( int csi = 0; csi < dgts.GridColumnStyles.Count; csi++ ) { if ( csi != hti.Column && csi != m_draggedColumn.Index ) { columns[csi] = dgts.GridColumnStyles[csi]; } else if ( csi == hti.Column ) { columns[csi] = dgts.GridColumnStyles[m_draggedColumn.Index]; } else { columns[csi] = dgts.GridColumnStyles[hti.Column]; } } // update TableStyle this.SuspendLayout(); this.TableStyles[this.DataMember].GridColumnStyles.Clear(); this.TableStyles[this.DataMember].GridColumnStyles.AddRange( columns ); this.ResumeLayout(); } else { InvalidateColumnArea(); } ResetMembersToDefault(); } 

在跨越了该功能的难点之后,触发必要的绘图操作将非常容易。

返回页首返回页首


重写 DataGrid 的 OnPaint 方法

迄今为止,您可能已经注意到没有在任何鼠标事件处理程序中执行任何绘图逻辑。这完全归结为个人喜好。我已经看到其他开发人员将他们的绘图逻辑与其余逻辑和并在一起使用,但我发现将所有绘图逻辑都放在 OnPaint 方法或 Paint 事件处理程序中会更为简单、更有条理。

需要重写 DataGrid 的 OnPaint 方法,以便容纳附加的绘图操作。首先要确保调用基本的 OnPaint 方法,以便绘制基础 DataGrid。这为我提供了可用来进行绘制的画布。

请记住,当您在画布上绘制对象时,z 排序要视对象的绘制顺序而定。了解这一点以后,我们需要首先绘制最底层的形状。

得到绘制的第一个图形是用于指示正在拖动哪个列的矩形( 5,刻度线标志 #1)。

dragdrop_datagrid

5. 不同的绘制步骤

通过使用 Graphics 对象的 m_draggedColumn 方法,我们在产生拖动操作的列的上方绘制了一个矩形。该区域信息是从 DraggedDataGridColumn 对象中检索到的。使用了半透明的画笔,以便使底层的列仍然可见。然后,在上述矩形的边框周围绘制了一个黑色矩形,以使其具有更为完整的修饰。

protected override void OnPaint( PaintEventArgs e ) { ... if ( m_draggedColumn != null ) { SolidBrush blackBrush = new SolidBrush( Color.FromArgb( 255, 0, 0, 0 ) ); SolidBrush darkGreyBrush = new SolidBrush( Color.FromArgb( 150, 50, 50, 50 ) ); Pen blackPen = new Pen( blackBrush, 2F ); g.FillRectangle( darkGreyBrush, m_draggedColumn.InitialRegion ); g.DrawRectangle( blackPen, region ); ... } } 

GDI 中的颜色被分解为四个 8 位的成分,其中的三个成分代表三原色:红色、绿色和蓝色。Alpha 成分(同样是 8 位)确定了颜色的透明度 — 它影响颜色与背景的融合方式。通过 Color.FromArgb 方法可以创建具有特定值的颜色。

Color.FromArgb( 150, 50, 50, 50 ) // dark grey with alpha translucency level set to 150 

我在本文前面提到的列反馈是以半透明的浅灰色矩形的形式完成的(图 5,刻度线标志 #2)。首先,我检查列索引以确保它不是 -1,然后使用 m_mouseOverColumnRect 中存储的矩形区域数据在该列上方填充一个矩形。

protected override void OnPaint( PaintEventArgs e ) { ... if ( m_draggedColumn != null ) { // user feedback indicating which column the dragged column is over if ( this.m_mouseOverColumnIndex != -1 ) { using ( SolidBrush b = new SolidBrush( Color.FromArgb( 100, 100, 100, 100 ) ) ) { g.FillRectangle( b, m_mouseOverColumnRect ); } } } } 

下一个焦点区域是正在拖动的列。如果用户已经选择在拖动操作发生时显示列或列标头,则绘制该图像。捕获的图像存储在 m_draggedColumn 中,并且可以通过 ColumnImage 属性访问。

protected override void OnPaint( PaintEventArgs e ) { ... if ( m_draggedColumn != null ) { ... // draw bitmap image if ( ShowColumnWhileDragging || ShowColumnHeaderWhileDragging ) { g.DrawImage( m_draggedColumn.ColumnImage, m_draggedColumn.CurrentRegion.X, m_draggedColumn.CurrentRegion.Y ); } ... } } 

最后,填充一个半透明矩形以代表拖动操作。这将使用与第一个图形类似的方式完成。从 m_draggedColumn 中读取列区域信息。然后,再绘制一个矩形以进一步增强前面的矩形( 5,刻度线标志 #3)。

protected override void OnPaint( PaintEventArgs e ) { ... if ( m_draggedColumn != null ) { ... g.FillRectangle( filmFill, m_draggedColumn.CurrentRegion.X, m_draggedColumn.CurrentRegion.Y, m_draggedColumn.CurrentRegion.Width, m_draggedColumn.CurrentRegion.Height ); g.DrawRectangle( filmBorder, new Rectangle( m_draggedColumn.CurrentRegion.X, m_draggedColumn.CurrentRegion.Y + Convert.ToInt16( filmBorder.Width ), width, height ) ); ... } } 
返回页首返回页首


小结

在本文中,我向您说明了我如何能够利用一些基本 GDI 功能,通过 DataGrid 控件获得可视化效果。通过跨越托管边界进行调用,我利用本机 GDI 功能来执行屏幕捕获,并且将该功能与 System.Drawing 中的绘图功能结合使用以产生吸引人的拖放体验。

Chris Sano 是一位使用 MSDN 的软件设计工程师。在狂热地编写代码之余,他喜欢打冰球并观看纽约 Yankees 队和费城 Flyers 队的比赛。如果您希望就本文与 Chris 联系,则可以通过 csano@microsoft.com 与他联系。

test window moving

 

test