Effective C# DataSet优于自定义结构

因为两个原则,把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, ITypedList
{
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 或者交互排序,或者支持搜索。在你取得内容之前就陷于层次关系和导航关系中了。我也不准备为上面所有的代码添加任何例子了。

        几页过后,再问问你自己,还准备创建你自己的特殊集合吗?或者使用DataSe吗?如果我们需要的集合不是性能紧要的算法集的一部分,或者必须有一个可移植的格式,那就使用DataSet,尤其是强类型的DataSet。这将节约大量的时间,是的,你可以争辩说DataSet并不是一个基于面向对象设计的最好的例子,强类型的DataSet甚至会破坏更多的规则。但是,现实是我们因此获得的开放效率要远远高于自己编写的看上去更为优雅的设计。


 

posted @ 2008-11-13 13:48  瞪着你的小狗  阅读(973)  评论(0编辑  收藏  举报