VB.net 2010 视频教程 VB.net 2010 视频教程 python基础视频教程
SQL Server 2008 视频教程 c#入门经典教程 Visual Basic从门到精通视频教程
当前位置:
首页 > 编程开发 > Objective-C编程 >
  • Effective C# 原则41 选择DataSet而不是定义数据结构

制作者:剑锋冷月 单位:无忧统计网,www.51stat.net
 

  因为两个原则,把DataSet的名声搞的不好。首先就是使用XML序列化的DataSet与其它的非.Net代码进行交互时不方便。如果在Web服务的API中使用DataSet时,在与其它没有使用.Net框架的系统进行交互时会相当困难。其次,它是一个很一般的容器。你可以通过欺骗.Net框架里的一些安全类型来错误DataSet。但在现代软件系统中,DataSet还可以解决很多常规的问题。如果你明白它的优势,避免它的缺点,你就可以扩展这个类型了。

  DataSet类设计出来是为了离线使用一些存储在相关数据库里的数据。你已经知道它是用来存储DataTable的,而DataTable就是一个与数据库里的结构在行和列上进行匹配的内存表。或许你已经看到过一些关于DataSet支持在内部的表中建立关系的例子。甚至还有可能,你已经见过在DataSet里验证它所包含的数据,进行数据约束的例子。

  但不仅仅是这些,DataSet还支持AcceptChanges 和RejectChanges 方法来进行事务处理,而且它们可以做为DiffGrams存储,也就是包含曾经修改过的数据。多个DataSet还可以通过合并成为一个常规的存储库。DataSet还支持视图,这就是说你可以通过标准的查询来检测数据里的部份内容。而且视图是可以建立在多个表上的。

  然而,有些人想开发自己的存储结构,而不用DataSet。因DataSet是一个太一般的容器,这会在性能上有所损失。一个DataSet并不是一个强类型的存储容器,其实存储在里面的对象是一个字典。而且在里的表中的列也是字典。存储在里的元素都是以System.Object的引用形式存在。这使得我们要这样写代码:

int val = ( int )MyDataSet.Tables[ "table1" ].
 Rows[ 0 ][ "total" ];

  以C#强类型的观点来看,这样的结构是很麻烦的。如果你错误使用table1 或者total的类型,你就会得到一个运行时错误。访问里面的数据元素要进行强制转化。而这样的麻烦事情是与你访问里面的元素的次数成正比的,与其这样,我们还真想要一个类型化的解决方法。那就让我们来试着写一个DataSet吧,基于这一点,我们想要的是:

 

int val = MyDataSet.table1.Rows[ 0 ].total;

  当你看明白了类型化的DataSet内部的C#实现时,就会知道这是完美的。它封装了已经存在的DataSet,而且在弱类型的访问基础上添加了强类型访问。你的用户还是可以用弱类型API。但这并不是最好的。

  与它同时存在的,我会告诉你我们放弃了多少东西。我会告诉你DataSet类里面的一些功能是如何实现的,也就是在我们自己创建的自定义集合中要使用的。你可能会觉得这很困难,或者你觉得我们根本用上不同DataSet的所有功能,所以,代码并不会很长。OK,很好,我会写很长的代码。

  假设你要创建一个集合,用于存储地址。每一个独立的元素必须支持数据绑定,所以你我创建一个具有下面公共属性的结构:

public struct AddressRecord
{
 private string _street;
 public string Street
 {
  get { return _street; }
  set { _street = value; }
 }
 private string _city;
 public string City
 {
  get { return _city; }
  set { _city = value; }
 }
 private string _state;
 public string State
 {
  get { return _state; }
  set { _state = value; }
 }
 private string _zip;
 public string Zip
 {
  get { return _zip; }
  set { _zip = value; }
 }
}

  下面,你要创建这个集合。因为我们要类型安全的集合,所以我们要从CollectionsBase派生:

public class AddressList : CollectionBase
{
}

  CollectionBase 支持IList 接口,所以你可以使用它来进行数据绑定。现在,你就发现了你的第一个问题:如果地址为空,你的所有数据绑定行就失败了。而这在DataSet里是不会发生的。数据绑定是由基于反射的迟后绑定代码组成的。控件使用反射来加载列表里的第一个元素,然后使用反射来决定它的类型以及这个类型上的所有成员属性。这就是为什么DataGrid可以知道什么列要添加。它会在集合中的第一个元素上发现所有的公共属性,然后显示他们。当集合为空时,这就不能工作了。你有两种可能来解决这个问题。第一个方法有点丑,但是一个简单的方法:那就是不充许有空列表存在。第二个好一些,但要花点时间:那就是实现ITypedList 接口。ITypedList 接口提供了两个方法来描述集合中的类型。GetListName 返回一个可读的字符串来描述这个列表。GetItemProperties 则返回PropertyDescriptors 列表,这是用于描述每个属性的,它要格式化在表格里的:

 

public class AddressList : CollectionBase
{
 public string GetListName(
  PropertyDescriptor[ ] listAccessors )
 {
  return "AddressList";
 }
 public PropertyDescriptorCollection
  GetItemProperties(
  PropertyDescriptor[ ] listAccessors)
 {
  Type t = typeof( AddressRecord );
  return TypeDescriptor.GetProperties( t );
 }
}

  这稍微好一点了,现在你你已经有一个集合可以支持简单的数据绑定了。尽管,你失去了很多功能。下一步就是要实现数据对事务的支持。如果你使用过DataSet,你的用户可以通过按Esc键来取消DataGrid中一行上所有的修改。例如,一个用户可能输入了错误的城市,按了Esc,这时就要原来的值恢复过来。DataGrid同样还支持错误提示。你可以添加一个ColumnChanged 事件来处理实际列上的验证原则。例如,州的区号必须是两个字母的缩写。使用框架里的DataSet,可以这样写代码:

ds.Tables[ "Addresses" ].ColumnChanged +=new
 DataColumnChangeEventHandler( ds_ColumnChanged );
private void ds_ColumnChanged( object sender,
 DataColumnChangeEventArgs e )
{
 if ( e.Column.ColumnName == "State" )
 {
  string newVal = e.ProposedValue.ToString( );
  if ( newVal.Length != 2 )
  {
   e.Row.SetColumnError( e.Column,
    "State abbreviation must be two letters" );
   e.Row.RowError = "Error on State";
  }
  else
  {
   e.Row.SetColumnError( e.Column,
    "" );
   e.Row.RowError = "";
  }
 }
}

 

  为了在我们自己定义的集合上也实现这样的概念,我们很要做点工作。你要修改你的AddressRecord 结构来支持两个新的接口,IEditableObject 和IDataErrorInfo。IEditableObject 为你的类型提供了对事务的支持。IDataErrorInfo 提供了常规的错误处理。为了支持事务,你必须修改你的数据存储来提供你自己的回滚功能。你可能在多个列上有错误,因此你的存储必须包含一个包含了每个列的错误集合。这是一个为AddressRecord做的更新的列表:

public class AddressRecord : IEditableObject, IDataErrorInfo
{
  private struct AddressRecordData
  {
   public string street;
   public string city;
   public string state;
   public string zip;
  }
  private AddressRecordData permanentRecord;
  private AddressRecordData tempRecord;
  private bool _inEdit = false;
  private IList _container;
  private Hashtable errors = new Hashtable();
  public AddressRecord( AddressList container )
  {
   _container = container;
  }
  public string Street
  {
   get
   {
    return ( _inEdit ) ? tempRecord.street :
     permanentRecord.street;
   }
   set
   {
    if ( value.Length == 0 )
     errors[ "Street" ] = "Street cannot be empty";
    else
    {
     errors.Remove( "Street" );
    }
    if ( _inEdit )
     tempRecord.street = value;
    else
    {
     permanentRecord.street = value;
     int index = _container.IndexOf( this );
     _container[ index ] = this;
    }
   }
  }
  public string City
  {
   get
   {
    return ( _inEdit ) ? tempRecord.city :
     permanentRecord.city;
   }
   set
   {
    if ( value.Length == 0 )
     errors[ "City" ] = "City cannot be empty";
    else
    {
     errors.Remove( "City" );
    }
    if ( _inEdit )
     tempRecord.city = value;
    else
    {
     permanentRecord.city = value;
     int index = _container.IndexOf( this );
     _container[ index ] = this;
    }
   }
  }
  public string State
  {
   get
   {
    return ( _inEdit ) ? tempRecord.state :
     permanentRecord.state;
   }
   set
   {
    if ( value.Length == 0 )
     errors[ "State" ] = "City cannot be empty";
    else
    {
     errors.Remove( "State" );
    }
    if ( _inEdit )
     tempRecord.state = value;
    else
    {
     permanentRecord.state = value;
     int index = _container.IndexOf( this );
     _container[ index ] = this;
    }
   }
  }
  public string Zip
  {
   get
   {
    return ( _inEdit ) ? tempRecord.zip :
     permanentRecord.zip;
   }
   set
   {
    if ( value.Length == 0 )
     errors["Zip"] = "Zip cannot be empty";
    else
    {
     errors.Remove ( "Zip" );
    }
    if ( _inEdit )
     tempRecord.zip = value;
    else
    {
     permanentRecord.zip = value;
     int index = _container.IndexOf( this );
     _container[ index ] = this;
    }
   }
  }
  public void BeginEdit( )
  {
   if ( ( ! _inEdit ) && ( errors.Count == 0 ) )
    tempRecord = permanentRecord;
   _inEdit = true;
  }
  public void EndEdit( )
  {
   // Can't end editing if there are errors:
   if ( errors.Count > 0 )
    return;
   if ( _inEdit )
    permanentRecord = tempRecord;
   _inEdit = false;
  }
  public void CancelEdit( )
  {
   errors.Clear( );
   _inEdit = false;
  }
  public string this[string columnName]
  {
   get
   {
    string val = errors[ columnName ] as string;
    if ( val != null )
     return val;
    else
     return null;
   }
  }
  public string Error
  {
   get
   {
    if ( errors.Count > 0 )
    {
     System.Text.StringBuilder errString = new
      System.Text.StringBuilder();
     foreach ( string s in errors.Keys )
     {
      errString.Append( s );
      errString.Append( ", " );
     }
     errString.Append( "Have errors" );
     return errString.ToString( );
    }
    else
     return "";
   }
  }
 }

 

  花了几页的代码来支持一些已经在DataSet里实现的了的功能。实际上,这还不能像DataSet那样恰当的工作。例如,交互式的添加一个新记录到集合中,以及支持事务所要求的BeginEdit, CancelEdit, 和EndEdit等。 你要在CancelEdit 调用时检测一个新的对象而不是一个已经修改了的对象。CancelEdit 必须从集合上移除这个新的对象,该对象应该是上次调用BeginEdit时创建的。对于AddressRecord 来说,还有很多修改要完成,而且一对事件还要添加到AddressList 类上。

  最后,就是这个IBindingList接口。这个接口至少包含了20个方法和属性,用于控件查询列表上的功能描述。你必须为只读列表实现IBindingList 或者交互排序,或者支持搜索。在你取得内容之前就陷于层次关系和导航关系中了。我也不准备为上面所有的代码添加任何例子了。

  几页过后,再问问你自己,还准备创建你自己的特殊集合吗?或者你想使用一个DataSet吗?除非你的集合是一个基于某些算法,对性能要求严格的集合,或者必须有轻便的格式,就要使用自己的DataSet,特别是类型化的DataSet。这将花去你大量的时间,是的,你可以争辩说DataSet并不是一个基于面向对象设计的最好的例子。类型化的DataSet甚至会破坏更多的规则。但,使用它所产生的代码开发效率,比起自己手写更优美的代码所花的时间,这只是其中一小部份。

 

 

int val = MyDataSet.table1.Rows[ 0 ].total;

  当你看明白了类型化的DataSet内部的C#实现时,就会知道这是完美的。它封装了已经存在的DataSet,而且在弱类型的访问基础上添加了强类型访问。你的用户还是可以用弱类型API。但这并不是最好的。

  与它同时存在的,我会告诉你我们放弃了多少东西。我会告诉你DataSet类里面的一些功能是如何实现的,也就是在我们自己创建的自定义集合中要使用的。你可能会觉得这很困难,或者你觉得我们根本用上不同DataSet的所有功能,所以,代码并不会很长。OK,很好,我会写很长的代码。

  假设你要创建一个集合,用于存储地址。每一个独立的元素必须支持数据绑定,所以你我创建一个具有下面公共属性的结构:

public struct AddressRecord
{
 private string _street;
 public string Street
 {
  get { return _street; }
  set { _street = value; }
 }
 private string _city;
 public string City
 {
  get { return _city; }
  set { _city = value; }
 }
 private string _state;
 public string State
 {
  get { return _state; }
  set { _state = value; }
 }
 private string _zip;
 public string Zip
 {
  get { return _zip; }
  set { _zip = value; }
 }
}

  下面,你要创建这个集合。因为我们要类型安全的集合,所以我们要从CollectionsBase派生:

public class AddressList : CollectionBase
{
}

  CollectionBase 支持IList 接口,所以你可以使用它来进行数据绑定。现在,你就发现了你的第一个问题:如果地址为空,你的所有数据绑定行就失败了。而这在DataSet里是不会发生的。数据绑定是由基于反射的迟后绑定代码组成的。控件使用反射来加载列表里的第一个元素,然后使用反射来决定它的类型以及这个类型上的所有成员属性。这就是为什么DataGrid可以知道什么列要添加。它会在集合中的第一个元素上发现所有的公共属性,然后显示他们。当集合为空时,这就不能工作了。你有两种可能来解决这个问题。第一个方法有点丑,但是一个简单的方法:那就是不充许有空列表存在。第二个好一些,但要花点时间:那就是实现ITypedList 接口。ITypedList 接口提供了两个方法来描述集合中的类型。GetListName 返回一个可读的字符串来描述这个列表。GetItemProperties 则返回PropertyDescriptors 列表,这是用于描述每个属性的,它要格式化在表格里的:



相关教程