2005年07月10日

可以用几种方法在.NET中编程来生成打印输出结果(如报表)。对Windows程序员来说,Visual Studio提供的Crystal Reports实际上是人们常用的打印报表的工具,但对不太复杂的报表来说,这个工具就有些大材小用了。当然,你可以用很好的Win32 API调用,但是尽管API可以让你有完全的控制权,它同时也把你锁定到一个单一的平台上了。如果Microsoft或另外的公司(如拥有Mono项目的Ximian公司)把Framework转移到另外的平台上,那么运用API的程序仍会锁定在Windows上,除非你为新的平台重新编写它们。

.NET Framework可以让你以一种新的方式来使用这些打印方法,System.Drawing.Printing名字空间的类将Win32 API的细粒度控制(fine-grain control)与相对简单的Visual Basic传统的Printer对象结合了起来(见表1)。在此,我将重点讲述该名字空间的主要的类,它们可以让你创建复杂程度适度的报表。PrintDocument类是这些类中最重要的。它可以让你定义一个对象,该对象发送输出结果到一个目的地,可以是一台打印机,或者是显示一个打印预览(运用PrintPreviewDialog类)。我还会讲述PrinterSettings类,它可以让你控制文件是如何打印的。

你可以很容易地在C#中创建一个PrintDocument对象,并把它附到一个事件处理程序上(实际处理打印的代码):

PrintDocument doc = new
   PrintDocument();
   doc.PrintPage += new
      PrintPageEventHandler(
      doc_PrintPage);

PrintPage事件处理程序有一个PrintPageEventArgs类型的参数。这个参数包含与该事件相关的数据,包括当前页面的页面设置、页边距信息以及是否有更多的页需要打印。每个页面触发该事件一次,直到ev.HasMorePages等于false:

private void doc_PrintPage(object
   sender, PrintPageEventArgs ev)



将Graphics用于Fonts和Fills
ev参数引用了你用来输出数据的Graphics制图区。Graphics有用来打印图象、文本、形状和线条的方法。你可以用DrawString方法来定义你要用的font(字体)和brush(画刷)。一个brush定义了一个图形的内部是如何填充的。你也需要告诉DrawString方法你想在哪里打印文本。PrintPageEventArgs类有一个读写属性——HasMorePages——它指明是否需要打印更多的页面:

Font f = new Font("Arial", 12);
ev.Graphics.Drawstring("Hello,
   World.",
   f, Brushes.Black, 100, 100);
ev.HasMorePages = false;

现在,你已经创建了一个PrintDocument对象并把它和一个事件处理程序联系起来了。你已经给事件处理程序中添加了代码,来打印一个单独的字符串,它的位置是距页面左边一英寸、距页面顶部一英寸,用的是12磅、 Arial的字体。现在我们来打印文件:

doc.Print();

在缺省设置下,PrintDocument类的Print方法在缺省打印机上打印结果,除非你另外指定。你可以用PrintDialog类来另外指定,它是System.Windows.Forms.CommonDialog名字空间的一部分。PrintDialog可以让你选择打印机,选择打印文件的哪部分,选择打印份数,来随意地打印一个文件。

PrintDialog类的PrinterSettings属性可以让你将诸如copies、from pageto page的属性放置到一个特定文件的PrinterSettings对象中。换句话说,单独的PrintDocument对象有一个PrinterSettings对象,它指定打印的份数、打印的范围、要运用的打印机的名字以及关于打印机本身的信息。

IPrinterSettings类中的一个bug会导致Copies属性总是返回一个为1的值,而不管你在PrintDialog中输入了多少份数。然而,PrintDocument.Print()方法仍然打印正确的份数,所以只有当你想将这个值用于其它地方时,才显示这个bug。当你执行Print方法时,系统运用这些设置。你可以提供一个实际的PrinterSettings对象或提供一个Document对象:

PrintDialog pd = new PrintDialog();
pd.Document = doc;
pd.ShowDialog();

然后,运用CommonDialog名字空间的PageSetupDialog,你可以指定纸张大小、纸张来源、打印方向和页边距。PageSetupDialog和PrintDialog都需要你提供一个Document对象或一个PageSettings对象。PageSettings对象适用于一个单独的打印页面,可以处理页边距、纸张大小以及是否用颜色打印页面:

PageSetupDialog ps = new
   PageSetupDialog();
ps.Document = doc;
ps.ShowDialog();



打印一个文件
当然,许多应用程序打印存储在一个文件或某种数据库中的信息,所以现在我将讲述一个更复杂的例子:打印一个以逗号分隔的联系清单文本文件。你可以将一个数据集、甚至一个XML文件中数据所运用的方法用在这里。本例的数据来自Northwind数据库的Customers表,格式如下:

Name, Title, Phone, Fax

通过在类的级别声明一个StreamReader对象,然后将一个文件分配给它,你就可以从一个文本文件打印该例子了。打开文件,修改PrintPage事件处理程序来打印文件的每一行,按需要格式化数据。

PrintDocument类有好几个事件。你已经用了PrintPage事件了;现在我们来添加更多的事件。当开始一个打印任务时(在实际打印任何东西前),触发BeginPrint事件。相应的事件是EndPrint,该事件是在所有页面结束打印时触发的。创建PrintPage事件需要的任何对象,或在BeginPrint中打开任何数据源,然后在EndPrint中关闭或释放(deallocate)它们。不要忘记将这些事件绑定到PrintDocument对象(见列表1)。


<br />
				图1.
图1. 显示你的报表

作为选择,你也可以把所有的打印功能封装到PrintDocument派生的一个单独的类中,然后重载触发相应事件的方法。你可以用你自定义的类,而不用实例化一个PrintDocument对象(见列表2)。这是一个很好的方法,但是为了保持一致,使代码清晰,我将继续讲述事件绑定方法。

PrintDocument类没有制图工具,所以创建报表需要花些时间——但这是值得的。创建一个新的报表的最好的方法是用一张空白的纸。在了解了报表的目的后,你就可以在纸上通过画方框来代表不同的区域进行设计了,如页眉、正文和页脚(见图1)。然后花些时间详细编写每个区域的数据将来自哪里,以及各个数据将放置在什么位置。可以说这个时候是你确保最终用户所看到的页面与你设计的页面是否是一致的最佳时刻。用户对报表比对应用程序的任何部分的挑剔都要多,所以你应该尽可能多花些时间来设计它。一旦你花了很多时间来设计报表并对它的打印结果了如指掌后,那么写代码就不会花很多时间了。



写代码
Rectangle对象提供了最快、最简单的方法来将你的页面设计转换成代码。这些对象可以让你几乎很准确地在屏幕上模拟你的设计。在页面上定义一个区域,指定坐标(有一定的高度和宽度)。Rectangle对象可以给你提供指定的点,在这个点上,你可以用Graphics类的Draw方法放置文本和图形。特别的是,你可以用DrawRectangle方法来查看该区域在页面的什么位置。该方法需要一个Pen对象(用来画直线和曲线)。将所有的版面设计代码放在PrintPage事件过程中:

// print a red line around the border
// of the page
ev.Graphics.DrawRectangle(
   Pens.Red, leftMargin, topMargin,
   pageWidth, pageHeight);

.NET Framework 1.0不能得到一台打印机的“hard”margins(实际可以打印到的页面最外面的区域)。但是,PrintPageEventArgs可以让你通过MarginBounds属性得到类似的功能。不幸的是,MarginBounds不考虑hard margins,所以你的输出可能不能在你期望的位置上结束。你必须用P/Invoke并调用Win32 GetDeviceCaps函数来得到打印机的hard margins(见列表3)。在得到hard margins后,你就可以开始创建Rectangle对象并在你的报表上打印信息了(见列表4)。

现在你已经创建了长方形边框(rectangles),你就可以在这个位置上打印你的数据了。一次打印一页数据,所以你需要定义一个页面有多大。用一个标准行的高度来划分可打印的区域(本例中你的正文区),从而计算每个页面的行数。通过用你运用的Font对象的GetHeight方法来确定一个行的高度。GetHeight是个属于Font类的重载的方法。你运用的方法需要一个Graphics对象参数。PrintPage事件的PrintPageEventArgs参数提供了这个Graphics对象:

int linesPerPage =
   Convert.ToInt32(body.Height /
   bodyFont.GetHeight(ev.Graphics));

一旦你确定了构成一个页面的行数,你就只需要进行简单的循环就行了,直到一个页面结束。然后设置ev.HasMorePages为true,或者一直等到打印的数据结束。在循环内部运用Graphics对象的Draw方法来打印你的数据。

你也需要确保DrawString方法将文本放置在了正确的位置上。通过用你选择的字体的高度乘以你用来跟踪已经打印了多少行的计数器,你就可以在每打印一行时计算下一行的位置了。然后为顶部的页边距添加值(见列表5)。



你运用的DrawString方法也接受一个StringFormat对象。StringFormat类可以让你控制文本的布局——包括对齐和行间距——以及省略符号的插入(如果一个给定的字符串对于你的长方形边框来说太长了时)。通过创建StringFormat对象,并设置其Alignment属性为StringAlignment.Center,你就可以使你的文本居中;然后将对象用于你调用的DrawString方法中。为你的联系清单页眉写以下代码:

StringFormat sf = new StringFormat();
sf.Alignment = StringAlignment.Center;
ev.Graphics.DrawString("Northwinds
   Customer Contacts", headerFont,
   defaultBrush, body, sf);

正如你所看到的,一旦你确定了报表的布局并创建了长方形边框来包含数据,实际的打印并不是很难。


<br />
				图2.
图2. 预览打印

PrintPreviewControl和PrintPreviewDialog类在Printing名字空间中提供了很好的功能。PrintPreviewDialog封装了PrintPreviewControl类,并提供了一个很好的用户界面,用来在页面间导航、改变缩放比例、选择一次可以预览的页数(见图2)。你可以用PrintPreviewControl来创建一个与你的应用程序其它部分相一致的打印预览窗口。一旦你定义了你的PrintDocument,并为必需的打印事件编写了代码后,添加打印预览就很简单了:

private void preview_Click(object
   sender, System.EventArgs e) {
   PrintPreviewDialog pd = new
      PrintPreviewDialog();
   pd.Document = doc;
   pd.ShowDialog();
}

我希望我已经给你们提供了研究System.Drawing.Printing名字空间的动力。我发现这个名字空间是用来自动生成许多重复性的打印项目(尤其是报表)的最好的方法。尝试用这种方法来完成一个打印任务吧,我敢打赌在用过一次后,你就会立刻将这个方法用于所有的打印任务了。

2005年07月07日

如果根据设计一个类可以被sealed,则我们应该这样做。

 

比如说,如果基类(base classB中定义了虚函数,而sealed class SB类衍生。对于一个类型为S的名为s的变量调用虚函数的代码,编译器可以确信s一定是类型为S的。但是如果类S实际上没有被sealed,则这个变量s可能是类S的衍生类的实例而这个衍生类同时又重写(override)了该虚函数。这时为了正确性,编译器必须以虚函数调用的方式执行该代码。这比直接执行的成本要高。

 

另外一个例子是attribute属性类。有一个FxCop的规则(Avoid unsealed attributes)专门检查定义的属性类是不是sealed。除了上面谈及的原因,还特别提到Attribute.GetCustomAttributeAPI. 其解释如下:The .NET Framework class library provides methods for retrieving custom attributes. These methods search the attribute inheritance hierarchy by default; for example System.Attribute.GetCustomAttribute searches for the specified attribute type, or any attribute type that extends the specified attribute type. Sealing the attribute eliminates the search through the inheritance hierarchy, and can improve performance.

 

关于FxCop的讨论,请参看定制FxCop规则示例之一

2005年07月05日
public class DataGridTextBoxColumn: System.Windows.Forms.DataGridTextBoxColumn
{
 private bool m_IsRedIfOverDue = false;
  /// <summary>
  /// 构造函数
  /// </summary>
  /// <param name="format">格式化</param>
  /// <param name="headerText">头部显示</param>
  /// <param name="mappingName">映射的字段</param>
  /// <param name="width">列的宽度</param>
  public DataGridTextBoxColumn(string format, string headerText, string mappingName, int width)
  {
   base.Format = format;
   base.HeaderText = headerText;
   base.MappingName = mappingName;
   base.Width = width;
  }
  /// <summary>
  /// 构造函数
  /// </summary>
  /// <param name="format">格式化</param>
  /// <param name="headerText">头部显示</param>
  /// <param name="mappingName">映射的字段</param>
  /// <param name="width">列的宽度</param>
  /// <param name="isRedIfOverDue"></param>
  public DataGridTextBoxColumn(string format, string headerText, string mappingName,
   int width, bool isRedIfOverDue)
  {
   base.Format = format;
   base.HeaderText = headerText;
   base.MappingName = mappingName;
   base.Width = width;
   m_IsRedIfOverDue = isRedIfOverDue;
  }
  /// <summary>
  /// 重写基类方法,由于这里只是用于显示数据,所以实现为空
  /// </summary>
  /// <param name="source"></param>
  /// <param name="rowNum"></param>
  /// <param name="bounds"></param>
  /// <param name="isReadOnly"></param>
  /// <param name="instantText"></param>
  /// <param name="cellIsVisible"></param>
  protected override void Edit(System.Windows.Forms.CurrencyManager source, int rowNum,
   System.Drawing.Rectangle bounds, bool isReadOnly, string instantText, bool cellIsVisible)
  {
   // 这只是一个只读类型的DataGrid
  }
  /// <summary>
  /// 使用 Paint 方法来绘制单元格
  /// </summary>
  /// <param name="g"></param>
  /// <param name="bounds"></param>
  /// <param name="source"></param>
  /// <param name="rowNum"></param>
  /// <param name="backBrush"></param>
  /// <param name="foreBrush"></param>
  /// <param name="alignToRight"></param>
  protected override void Paint(System.Drawing.Graphics g, System.Drawing.Rectangle bounds,
   System.Windows.Forms.CurrencyManager source, int rowNum, System.Drawing.Brush backBrush,
   System.Drawing.Brush foreBrush, bool alignToRight)
  {
   object bVal = GetColumnValueAtRow(source, rowNum);
   if (this.Format == "d")
   {
    try
    {
     bVal = String.Format("{0:d}", Convert.ToDateTime(bVal));
    }
    catch
    {
     // 忽略其他格式
    }
   }
   // 如果当前行就是被选择的行,则用选定的颜色绘制这行
   if (this.DataGridTableStyle.DataGrid.CurrentRowIndex == rowNum)
   {
    g.FillRectangle(new SolidBrush(this.DataGridTableStyle.SelectionBackColor), bounds);
    g.DrawString(Convert.ToString(bVal), this.DataGridTableStyle.DataGrid.Font,
     new SolidBrush(this.DataGridTableStyle.SelectionForeColor), bounds.X + 2, bounds.Y + 2);
   }
   else
   {
    g.FillRectangle(backBrush, bounds);
    if (m_IsRedIfOverDue)
    {
     try
     {
      if (Convert.ToDateTime(bVal).Date < DateTime.Now.Date)
       g.DrawString(Convert.ToString(bVal), this.DataGridTableStyle.DataGrid.Font,
        new SolidBrush(Color.Red), bounds.X + 2, bounds.Y + 2);
      else
       g.DrawString(Convert.ToString(bVal), this.DataGridTableStyle.DataGrid.Font, foreBrush,
        bounds.X + 2, bounds.Y + 2);
     }
     catch
     {
      g.DrawString(Convert.ToString(bVal), this.DataGridTableStyle.DataGrid.Font, foreBrush,
       bounds.X + 2, bounds.Y + 2);
     }
    }
    else
    {
     g.DrawString(Convert.ToString(bVal), this.DataGridTableStyle.DataGrid.Font, foreBrush,
      bounds.X + 2, bounds.Y + 2);
    }  
   }
  }
 }
用法,使用一个类来控制DataGrid的列模式:
public sealed class TableStyleGenerator
{
  /// <summary>
  /// 默认构造函数
  /// </summary>
  public TableStyleGenerator()
  {
   
  }
  /// <summary>
  /// 物料选择窗体的列控制
  /// </summary>
  /// <returns></returns>
  public static DataGridTableStyle GetStyleForMateriel()
  {
   // 创建用于物料选择窗体使用的列样式
   DataGridTableStyle dgTableStyle = new DataGridTableStyle();
   dgTableStyle.GridColumnStyles.Add(new DataGridTextBoxColumn(null,"物料名称","MaterielName",150));
   dgTableStyle.GridColumnStyles.Add(new DataGridTextBoxColumn(null,"物料代码","MaterialCode",100));
   dgTableStyle.GridColumnStyles.Add(new DataGridTextBoxColumn(null,"制式","MobClassName",50));
   dgTableStyle.GridColumnStyles.Add(new DataGridTextBoxColumn(null,"型号","MobTypeName",50));
   dgTableStyle.GridColumnStyles.Add(new DataGridTextBoxColumn(null,"物料类型","MaterielTypeName",60));

   dgTableStyle.HeaderForeColor = System.Drawing.SystemColors.ControlText;
   dgTableStyle.MappingName = "Materiel";
   dgTableStyle.RowHeadersVisible = false;
   dgTableStyle.AlternatingBackColor = System.Drawing.Color.LemonChiffon;
   return dgTableStyle;
  }
}
具体的DataGrid如下使用:
this.dataGrid1.TableStyles.Clear();
this.dataGrid1.TableStyles.Add(TableStyleGenerator.GetStyleForMateriel());
2005年06月29日

/// <summary>
 ///  Windows Forms 数据邦定辅助类.
 /// </summary>
 public class DataBinding
 {
  /// <summary>
  /// 绑定控件的属性到数据源的属性
  /// </summary>
  /// <param name="control">控件引用</param>
  /// <param name="controlProperty">控件属性名称.</param>
  /// <param name="dataSource">数据源引用</param>
  /// <param name="sourceProperty">数据源的字段,属性,或者列名</param>
  public static void Bind(Control control, string controlProperty, object dataSource, string sourceProperty)
  {
   Binding binding = control.DataBindings[controlProperty];
   if (binding != null)
   {
    control.DataBindings.Remove(binding);
   }
   
   control.DataBindings.Add(controlProperty, dataSource, sourceProperty);
   
  }


  /// <summary>
  /// 绑定控件的属性到数据源的属性
  /// </summary>
        /// <param name="control">控件引用</param>
        /// <param name="controlProperty">控件属性名称.</param>
        /// <param name="dataSource">数据源引用</param>
        /// <param name="sourceProperty">数据源的字段,属性,或者列名</param>
  /// <param name="format">表示将处理 Binding 对象的 Format 事件的方法(委托)</param>
  /// <param name="parse">表示将处理 Binding 对象的 Parse 事件的方法(委托)</param>
  public static void Bind (Control control, string controlProperty, object dataSource, string sourceProperty, ConvertEventHandler format, ConvertEventHandler parse)
  {
   Binding binding = control.DataBindings[controlProperty];

   if (binding != null)
   {
    control.DataBindings.Remove(binding);
   }
   
   binding = new Binding(controlProperty, dataSource, sourceProperty);
   if (format != null)
   {
    binding.Format += new ConvertEventHandler(format);
   }
   if (parse != null)
   {
    binding.Parse += new ConvertEventHandler(parse);
   }
   
   control.DataBindings.Add(binding);
   
  }
  
 }

2005年06月25日

Ted GraHam 提到了39 条 CheckList, 我觉得还是总结的挺全面.

  • Are exceptions used to indicate error rather than returning status or error codes?
  • 使用异常来只是错误而不是使用状态或者错误代码值
  • Are all classes and public methods commented with .NET style comments?? Note that comments should discuss the "what" of public methods.? Discussion of "how" should be in blocks or in-line with the code in question.
  • 所有类以及 public 方法 都使用.NET 样式的注释, 即 /// summary 格式. 注意 Summary 中说明代码有那些功能,而不是这个功能如何实现的. 可以在 Remarks 块或者代码中说明.
  • Are method arguments validated and rejected with an exception if they are invalid?
  • 所有方法的参数的合法性是否做验证, 对非法的参数是否抛出异常?
  • Are Debug.Asserts used to verify assumptions about the functioning of the code?? Comments like, "j will be positive" should be rewritten as Asserts.?
  • 是否使用 Debug.Asserts 来验证代码中的假设? “ j 应该是正数?“之类的注释应该用 Debug.Asserts 来重写.
  • Do classes that should not be instantiated have a private constructor?
  • 不需要实例化的类有 私有构造函数吗?
  • Are classes declared as value types only infrequently used as method parameters, returned from methods or stored in Collections?
  • 值类型的类用于参数,方法返回值以及存放在集合中?
  • Are classes, methods and events?that are specific to an assembly marked as internal?
  • Assembly 特有的类,方法,事件的访问修饰符是否已经标记为 Internal ?
  • Are singletons that may be accessed by multiple threads instantiated correctly?? See the Enterprise Solution Patterns book, p. 263.
  • 多线程同时访问的单件对象是否正确的初始化?
  • Are methods that must be overriden by derived classes marked as abstract?
  • 必须被衍生类重写的方法申明为 Abstract 了吗?
  • Are classes that should not be overriden marked as sealed?
  • 不能重写的类是否标记为 Sealed?
  • Is "as" used for possibly incorrect downcasts??
  • 可能失败的转换是否使用了 AS 运算符?
  • Do classes override ToString?instead of defining a Dump method for outputting the object’s state?
  • 输出对象的状态的时候应该重写 ToString 方法而不是加一个类似 Dump 之类的方法.
  • Are log messages sent to the logging component instead of Console?
  • 所有log 的消息都有 log 组建处理,而不是仅仅输出到 控制台.
  • Are finally blocks used for code that must execute following a try??
  • finnally 代码块用于try 后必须执行的代码
  • Is foreach used in preference to the for(int i…) construct?
  • 尽可能的采用 foreach 而不是 for(int i…)
  • Are properties used instead of implementing getter and setter methods?
  • 是否属性没有实现getter 和 setter 方法
  • Are readonly variables used in preference to?properties without setters?
  • 只读的属性应该没有 setter 方法
  • Is the override keyword used on all methods that are overriden by derived classes?
  • 衍生类重写的方法是否都使用了 override 关键字
  • Are interface classes used in preference to abstract classes?
  • 正确的使用interface 和抽象类.
  • Is code written against an interface rather than an implementing class?
  • 接口实现和抽象类继承
  • Do all objects that represent "real-world" or expensive resources implement the IDisposable pattern?
  • 操作系统资源的类是否实现了 IDisposable 接口?
  • Are all objects that implement IDisposable instantiated in a using block?
  • 是否所有实现IDisposable 的类初始化的时候使用了 Using 语句?
  • Is the lock keyword used in preference to the Monitor.Enter?construct?
  • 使用lock 语句而不是 monitor.enter
  • Are threads awakened from wait states by events or the Pulse construct, rather than "active" waiting such as Sleep()?
  • 线程使用事件或者pulse 唤醒, 而不是使用 sleep 主动的唤醒.
  • If equals is overridden, is it done correctly?? The rules for overriding equals are complex, see Richter p153-160 for details.
  • 是否正确的重写了 equals
  • If == and != are overridden, so they redirect to Equals?
  • == 和 != 操作符号被重写
  • Do all objects that?override Equals also provide an overloaded version of GetHashCode that?provides the same semantics as Equals?? Note that overrides to GetHashCode should?take advantage of the objects member variables, and must?return an unchanging hash code.
  • Equals GethashCode 的重写问题
  • Do all exception classes?have a constructor that takes a string and and another constructor that takes a string and an exception?
  • 异常类的构造问题
  • Do all exception classes derive from the base Matrix exceptions and fit correctly into the exception hierarchy?
  • 自定义异常类的继承层次问题
  • Are all classes that will be marshaled or remoted marked with the Serializable attribute?
  • 所有被 Marshal 或者远程处理的对象有序列化标志
  • Do all classes marked with the?Serializable attribute have a default constructor?? This includes Exception and?EventArgs?classes.
  • 所有标记有 Serializable 属性的类是否有默认的构造函数, 包括常见的 Exception 和 EventArgs 类.
  • Do all classes that explicitly implement ISerializable provide both the required GetObjectData and the implied constructor that takes a SerializationInfo?and a?StreamingContext?
  • 实现 Iserializable 接口的类是否显式的实现 GetObjectData 和 隐式的构造函数,例如 Serializaioninfo 和 StreamingContext 作为参数
  • When doing floating point calculations,?are all constants doubles rather than integers?
  • 做浮点运算的时候,所有的常量都是double 类型而不是整数
  • Do all delegates have a void return type and avoid using output or ref parameters?
  • 委托是否都有 void 返回值,避免使用 out 或者 ref 类型的参数
  • Do?all delegates send the sender (publisher) as the first argument?? This allows the subscriber to tell which publisher fired the event.?
  • 所有的委托又有 sender 对象作为第一个参数
  • Are all members of derived EventArg classes read-only?? This prevents one subscriber from modifying the EventArgs, which would affect the other subscribers.
  • 从 EventArg 继承的类是否是只读的, 只读的参数可以避免一个订阅者对参数的修改影响其他的参数订阅者
  • Are delegates published as events?? This prevents the subscribers from firing the event, see Lowy, p. 102?for details.
  • 所有的委托发布事件?
  • Is common setup and teardown nUnit code isolated in?Setup and Teardown methods that are marked with the appropriate attribute?
  • 单元测试的时候, 常见的驱动代码和测试代码分开.
  • Do negative nUnit tests use the ExpectedException attribute to indicate that an exception must be thrown?
  • 使用 ExpectedExcetpion 来指示异常必须抛出?
2005年06月09日

该计划名为Kickstart,是付费服务,不同数量不同定价。Mono本身也是Novell支持的项目,目前看来开发得已经不错了。但要想得到用户的承认,大公司在背后的支持服务是不可少的,Novell正是抓住了这个机会。目前可以支持的项目有Mono运行环境(RH系列,Suse及MacosX和Solaris平台),C#编译器,GTK#等。

2005年06月08日

本文介绍了在AOP编程中常见的几种Weave时机,并详细说明了这几种Weave时机的差别和适用场合。

在本文中,我们将采用三种重要的实现的例子,来实践本文提出的概念。这三种AOP实现是AspectJ,Spring和JBoss。通过比较他们在Weave时机方面的不同,来获得对于如何选择Weave时机进行判定的准则。由于AspectWerk已经合并到AspectJ中,我们将不再对其进行单独的评论。

对于AOP编程而言,程序的主要逻辑部分和Aspect功能部分的具体实现都可以采用传统的OO技术等实现,这里没有什么新东西。AOP最为特别并使其相对其它方法具有明显优点的部分就在于它能够以多样的方式将程序中用到的多个方面灵活的Weave到一起,形成一个完整的应用程序。因而在学习AOP编程时,如何以准确、简洁、灵活的方式将各个不同的方面Weave到一起,就成为了我们最需要注意的关键点。接下来,我们将阐述Weave操作发生的不同时机,并介绍其适用的场合。

大致上,Weave操作可以发生在如下几个阶段:

  • 编译时:在对源代码进行编译时,特殊的编译器允许我们通过某种方式指定程序中各个方面进行Weave的规则,并根据这些规则生成编译完成的应用程序。
  • 编译后:根据Weave规则对已经完成编译的程序模块进行Weave操作。
  • 载入时:在载入程序模块的时候进行Weave操作
  • 运行时:在程序运行时,根据程序运行时的情况Weave程序中的对象和方面

在表1中列出了目前几种主流的AOP系统所支持的Weave操作时机。


编译时Weave
对于普通应用程序而言,在编译时进行Weave操作是最为直观的做法。由于源程序中包含了应用的所有信息,因此这种方式通常支持最多种类的联结点。利用编译时Weave,我们能够使用AOP系统进行细粒度的Weave操作,例如读取获写入字段。源代码编译之后形成的模块将丧失大量的信息,因此通常采用粗粒度的AOP方法。同时,对于传统的编译成为本地代码的语言如C++、Fortran等来说,编译完成后的模块往往跟操作系统平台相关,这就给建立统一的编译后、载入时以及运行时Weave机制造成了困难。对于编译成为本地代码的语言而言,只有在编译时进行Weave最为可行。尽管编译时Weave具有功能强大、适应面广泛等优点,但他的缺点也很明显。首先,它需要程序员提供所有的源代码,因此对于模块化的项目就力不从心了。即使能够提供所有模块的源代码,它也造成了程序不能进行增量编译、编译时间变慢等不利之处。

后编译时Weave
为了解决模块化编程的要求,有些AOP框架开始支持后编译时Weave的功能。程序员只需要获得编译完成之后的模块,就能进行Weave操作。在AspectJ中,不管是程序的主逻辑部分还是方面都可以先编译成为模块之后进行Weave,而且主逻辑部分完全可以采用普通的JavaC编译。而在AspectC中,进行后编译时Weave的要求是所有的程序模块都采用AspectC进行编译。可以看出,使用Java这样基于虚拟机的语言对于编写AOP程序是有优势的。

载入时Weave
尽管后编译时Weave已经解决了不能获得所有源代码时进行AOP编程的需要,但是在这个框架流行的时代,我们需要更为灵活的安排我们的Weave操作。如果程序的主逻辑部分和Aspect作为不同的组件开发,那么最为合理的Weave时机就是在框架载入Aspect代码之时。因此我们可以看到,在JBOSS和Spring中都提供了这样的方式进行Weave操作。在进行载入时Weave时,Weave操作之后的结果不会被保存。程序的主逻辑部分和Aspect部分可以分别进行开发和编译,而Weave操作在程序别载入时发生。AspectJ、Spring和JBoss都支持载入时Weave。在Spring和JBoss的AOP实现中,框架先于应用程序启动,由框架来负责Weave操作的进行。而在AspectJ中,一个特殊的类加载器被用于这个目的。这个类加载器可以方便的嵌入到框架应用程序中,从而能够为任意的框架提供AOP支持。使用AspectJ进行载入时Weave需要几个步骤:

1. 在编译时为编译器指定-Xreweavable选项来使得AspectJ编译器在.class文件中保存额外的Weave相关信息。

2. 在.jar文件中添加META-INF/aop.xml来指定Weave策略。

3. 在运行时指定AspectJ提供的类加载器。对于jdk 5,我们可以为java虚拟机指定-javaagent选项。而JDK 1.4可以通过系统属性-Djava.system.class.loader来指定类加载器。

在下图中给出了一个AspectJ中配置运行时Weave的配置文件aop.xml,我们在图中给出了详细的注释,感兴趣的读者朋友可以很容易的了解这段代码的用途:




     1:<aspectj>
   2:    <aspects>
   3:              <!-- 声明两个将要使用的Aspects -->
   4:        <aspect name="com.MyAspect"/>
   5:        <aspect name="com.MyAspect.Inner"/>
   6:
   7:              <!-- 在配置文件中定义一个Aspect -->
   8:        <concrete-aspect name="com.xyz.tracing.MyTracing" extends="tracing.AbstractTracing">
   9:            <pointcut name="tracingScope" expression="within(org.maw.*)"/>
  10:        </concrete-aspect>
  11:
  12:              <!-- 使用任何匹配"com..*"的Aspect进行Weave-->
  13:        <include within="com..*"/>
  14:
  15:              <!-- 不使用任何具有@CoolAspect注解的Aspect进行Weave -->
  16:        <exclude within="@CoolAspect *"/>
  17:
  18:    </aspects>
  19:
  20:    <weaver options="-verbose -XlazyTjp">
  21:              <!-- 对javax.*包和org.aspectj.*包中的内容进行Weave操作。并对foo包中
  22:                   所有不具有@NoWeave注解的类型进行Weave操作。-->
  23:        <include within="javax.*"/>
  24:        <include within="org.aspectj.*"/>
  25:        <include within="(!@NoWeave foo.*) AND foo.*"/>
  26:    </weaver>
  27:</aspectj>
  28:
  

在载入时进行Weave的过程中,AspectJ有一些必须遵守的限制:

1. 要求所有将要被Weave的代码通过AspectJ提供的类加载器载入。

2. Aspect代码必须对Weave类加载器可见,也就是说Aspect必须由Weave类加载器自身或其父加载器载入。

3. 在Weave操作发生之前,所有的Aspect代码都已经被载入

运行时Weave
运行时Weave可能是所有Weave方式中最为灵活的,程序在运行过程中可以为单个的对象指定是否需要Weave特定的Aspect。在JBoss项目中,利用运行时Weave的特性完成了JBoss Cache项目。在JBoss Cache中,如果一个对象被放置到Cache中,它的状态就将被CacheAOP监视并且它的状态会被自动同步到一个分布式的缓存中。如果这个对象不需要被缓存,那么它就和AOP不发生任何关系。对它的修改不会引发Cache的同步操作。值得一提的是,尽管AspectJ没有明确提供运行时Weave的能力,在AspectJ中可以通过一个简单的pattern实现运行时Weave。具体请参见Adrian的Blog:http://www.aspectprogrammer.org/blogs/adrian/2005/03/perinstance_asp.html

小结
选择合适的Weave时机对于AOP应用来说是非常关键的。针对具体的应用场合,我们需要作出不同的抉择。可以看到,AspectJ为我们提供了最多的选择,即时没有直接支持的运行时Weave也可以通过一个简单的模式来实现。在使用Spring或JBoss提供的AOP框架时,我们可以利用AspectJ来补足这两个框架的不足之处,从而获得更为灵活的Weave策略。

参考资料

  1. 本文中的例子和讨论基于以下 AOP 实现:
  2. Ramnivas Laddad 撰写的 AspectJ in Action(Manning,2003 年)是一本非常好的关于AspectJ的书籍。
  3. Adrian的Blog是一个深入了解AOP以及AspectJ的好地方。
关于作者
甘志,IBM 中国软件实验室(CSDL BJ)的成员,主要研究方向为AOP、SOA和Security,他还对羽毛球运动很感兴趣。他在上海交通大学计算机系攻读博士学位,期间发表了多篇论文和技术书籍。你可以通过联系他。 ganzhi@cn.ibm.com
2005年06月01日

我的网站的架构采用Castle+Ibatis+Asp.net,业务逻辑层的事务处理采用Castle Facility:Automatic Transaction Management ,这是AOP事务方面的一个应用吧,下面介绍一下如何在业务逻辑层中使用这个Facility.这个Faciity作用就是管理事务,依赖于是否抛出异常的结果进行Commit还是rollback.

网站架构中同时用到了iBatis.Net facility ,iBatis.Net facility 实继承接口ITransactionManager,注意主要继承与ITransactionManager接口的facility都可以同Automatic Transaction Management Facility协同工作。目前在Castle的Facility中有NHibernate facility, iBatis.Net facility and db4o facility都是实现了ITransactionManager借口的,也就是说它可以同这几个Facility协同工作,当然你自己也可以实现更多的Facility来使用。

下面介绍一下它的用法:

1、在IOC容器WindsorContainer中注册这个Facility

WindsorContainer container = new WindsorContainer(store);
container.AddFacility( "auto.transaction", new TransactionFacility() );

2、业务逻辑类中使用Transactional attribute 和每一个希望使用事务的方法,例子如下(这是我的网站留言本的业务逻辑类):

using System;
using System.Collections;

using Castle.Services.Transaction;
using Castle.Facilities.IBatisNetIntegration;

using FrameWork.Domain;
using FrameWork.DAO;
using FrameWork.DAO.IDAO;

namespace FrameWork.Service
{
 /// <summary>
 /// LiuyanService 的摘要说明。
 /// </summary>
 [Transactional]
 [UsesAutomaticSessionCreation]
 public class LiuyanService
 {
  private ILiuyanDao _liuyanDao;
  private ISequenceDao _sequenceDao;

  public LiuyanService(ILiuyanDao liuyanDao,ISequenceDao sequenceDao)
  {
   this._sequenceDao = sequenceDao;
   this._liuyanDao = liuyanDao;
  }

  public IList GetLiuyanList()
  {
   return this._liuyanDao.GetLiuyanList();
  }

  [Transaction(TransactionMode.Requires)]
  public virtual  void AddLiuyan(Liuyan liuyan)
  {
   this._liuyanDao.AddLiuyan(liuyan);
  }

  [Transaction(TransactionMode.Requires)]
  public virtual void ModifyLiuyan(Liuyan liuyan)
  {
   this._liuyanDao.ModifyLiuyan(liuyan);
  }

  [Transaction(TransactionMode.Requires)]
  public virtual void DeleteLiuyan(int id)
  {
   this._liuyanDao.DeleteLiuyan(id);
  }

  public Liuyan GetLiuyanInfo(int id)
  {
   return this._liuyanDao.GetLiuyanInfo(id);
  }
 }
}
这里要注意的是,如果你没有定义业务逻辑的接口,则必须将使用事务的方法声明为virtual ,这主要是由于他们的基础是DynamicProxy

2005年05月28日

按照软件开发框架实现的网站(IbatisNet + Castle + ASP.NET),今天调试完毕正式发布。网站现在实现可以支持Access/MS SQL数据库,当然实现其他的数据库也很容易,只要修改Data Mapping文件,主要是各个数据库的对SQL标准的支持不好。欢迎各位到http://www.keyusoft.cn溜溜。

2005年05月20日

boo 是一种类 Python 的新语言,主页在:http://boo.codehaus.org

请注意,它不是Python的实现,而是从Python获得灵感而来的新的语言,虽然与Python很象,但它是dotnet上的开发语言。

db4oboobrowser is a simple boo based object browser for db4o (http://www.db4o.com/) database files that allows you to view and manipulate your live objects either through a GUI or scripts written in the boo programming language