因為兩個原則,把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甚至會破壞更多的規則。但,使用它所 產生的代碼開發效率,比起自己手寫更優美的代碼所花的時間,這只是其中一小 部份。
返回教程目錄