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甚至會破壞更多的規則。但是,現實是我們因此獲得的開放效率要遠遠高於自己編寫的看上去更爲優雅的設計。


 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章