1.引言 BindingSource組件是數據源和控件間的一座橋,同時提供了大量的API和Event供我們使用。使用這些API我們可以將Code與各種具體類型數據源進行解耦;使用這些Event我們可以洞察數據的變化。 2.簡單綁定 DataTable myTable = myTableAdapter.GetData();//創建Table BindingSource myBindingSource= new BindingSource();//創建BindingSource DataGridView myGrid = new DataGridView();//創建GridView myGrid.DataSource = myBindingSource;//將BindingSource綁定到GridView myTable;//綁定數據到BindingSource 注: 1)綁定到DataTable,其實是綁定到DataTable提供的DataView上。每個DataTable都有一個缺省的DataView 2)DataView是綁定的實質,正如其名,它是DataTable的數據的展現。因此可以對同一個DataTable ,構建多個DataView,進而可以對這同樣的數據實施不同的過濾、排序等方法,從不同側面展示DataTable。這也體現了一定的MVC思想。 3)BindingSouce也可作為數據(其實是數據引用)的容器在不同窗體間傳遞,從而實現在彈出窗體中對數據的編輯 3.主細表 image 以上圖所示數據為例: 1)DataSet:myDataSet 2)DataTable:ParentTable、ChildTable、GrandChildTable 3)Relation:FK_Parent_Child、FK_Child_GrandChild //綁定父數據 parentBindingSource.DataSource = myDataSet; parentBindingSource.DataMember = "ParentTable"; m_GrandParentGrid.DataSource = m_GrandParentBindingSource; //綁定子數據。 childBindingSource.DataSource = parentBindingSource;//綁定到“父BindingSource”,而不是父Table childBindingSource.DataMember = "FK_Child_GrandChild";//綁定到“父-子Relation” //綁定孫子數據。 grandChildBindingSource.DataSource = childBindingSource;//綁定到“子BindingSource” grandChildBindingSource.DataMember = "FK_Child_GrandChild";//綁定到“子-孫Relation” 這樣你就可以在Form上擺上3個DataView,分布綁定到這3個BindingSouce,很容易就實現了主細表關聯展現。 4.數據操縱 要操縱數據,首先需要獲取當前數據項。BindingSource的Current屬性返回DataRowView類型的對象(就像DataView是對 DataTable的封裝一樣,DataRowView是對DataRow的封裝),它是對當前數據項的封裝,可以通過類型轉換變成你想要的對象。 DataRowView currentRowView = myBindingSource.Current;//獲取當前RowView CustomersRow custRow = currentRowView.Row as CustomersRow;//類型轉換為當前數據項 string company = custRow.CompanyName;//使用當前數據項 string phoneNo = custRow.Phone; 5.用BindingSource做數據容器 BindingSource還可以用作數據容器,即便它沒有綁定到數據源上,它內部有一個可以容納數據的list。 5.1Add方法 調用Add方法會在BindingSource的list中插入數據項。如果這時第一次插入數據,並且沒有綁定數據,那么插入數據的類型就決定了今后此list中數據的類型。 注: 1)此時再插入其它類型對象會拋出InvalidOperationException異常 2)設置DataSource屬性時會刷新list,造成Add方法添加到list中的數據丟失 5.2AddNew方法 AddNew方法返回BindingSourc所容納數據類型的對象;如果之前沒有容納數據,則會返回Object對象。 AddNew方法會調用EndEdit方法,並將提交對當前數據的操縱;然后新數據項就成為當前項。 AddNew方法會引發AddingNew事件,可以在此事件中為數據項賦值,或者創建新數據項 private void OnAddingNew(object sender, AddingNewEventArgs e) { e.NewObject = new MyCustomObject();// } 6.用BindingSource對數據排序、過濾、搜索 6.1 Sort 為Sort屬性賦上Sort表達式,可以對數據進行排序 myBindingSource.Sort = "ContactName ASC";//對ContanctName列按ASC進行排序 myBindingSource.Sort = "Region ASC, CompanyName DESC"//先按Region、再按CompanyName排序 6.2 Find Find方法根據指定屬性和關鍵字進行查找,並返回第一個匹配對象的Index int index = m_CustomersBindingSource.Find("CompanyName",IBM);//按CompanyName查找IBM if (index != -1) { myBindingSource.Position = index;//定位BindingSource } 6.3 Filter 為Filter屬性賦上表達式,可以對數據進行過濾 m_CustomersBindingSource.Filter = "Country = 'Germany'";//過濾出Country屬性為Germany的數據 7.用Event監控數據 7.1 Event 1)AddingNew 調用AddNew()方法時觸發。 2)BindingComplete 當控件完成數據綁定時觸發,說明控件已經從數據源中讀取當前數據項的值。當BindingSource重新綁定或當前數據項改變時,會觸發此事件 注: * 當有多個控件綁定到同一數據源時,這個事件會觸發多次 3)CurrrentChanged 當前數據項改變時觸發此事件。觸發此事件的情況如下 * Position屬性改變時 * 添加、刪除數據時 * DataSource或DataMember屬性改變時 4)CurrentItemChanged 當前數據項的值改變時觸發 5)DataError 通常輸入無效數據時,由CurrencyManage拋出異常,從而觸發此事件。 6)PositionChanged Position屬性改變時觸發此事件。 7)ListChanged 數據集合改變時觸發。觸發此事件的情況如下 * adding, editing, deleting, 或 moving 數據項時 改變那些會影響List行為特征的屬性時,如AllowEdit屬性 * 替換List時(綁到新數據源) 8.限制數據修改 BindingSource不僅是數據源與控件間的“橋梁”,同時也是數據源的“看門人”。通過BindingSource,我們可以控制對數據的修改。 BinidingSource的AllowEdit, AllowNew和AllowRemove屬性可以控制客戶端代碼和控件對數據的修改 9.復雜數據類型的Binding 對於String類型的數據,直接Binding到Text控件即可,對於復雜類型有下面幾種情況 * 對於DateTime、Image等類型的數據,它們存儲的格式與顯示要求並不一致。 * 有時,你並不想顯示客戶ID,而是希望顯示客戶名稱 * 數據庫中的Null值 9.1 Binding類 解決以上問題的關鍵是要理解Binding類,了解它是如何控制數據Binding的過程。 DataTable table = customersDataSet.Customers; //將TextBox的Text屬性Binding到table的CustomerID列 customerIDTextBox.DataBindings.Add("Text", table,"CustomerID", true); //上面一行代碼等同下面兩行代碼 Binding customerIDBinding = new Binding("Text", table,"CustomerID", true); customerIDTextBox.DataBindings.Add(customerIDBinding); 從代碼可以看出,Binding是數據源(table)和控件(customerIDTextBox)間的中介人,它有以下功能 * 從數據源取數據,並按照控件要求的數據類型對此數據進行格式化(Formatting),然后傳給控件 * 從控件取數據,並按照數據源的數據類型要求對此數據進行解析(Parsing),然后返回給數據源 * 自動對數據進行格式轉換 9.2Binding類構造函數和屬性 Binding構造函數有多個重載版本,下面介紹其重要的參數,這些參數同時存在於Binding對象的屬性中。下面介紹中,參數名和屬性名都列出來 1)formattingEnabled(屬性FormattingEnabled) o true,Binding對象自動在數據源類型和控件要求的類型間進行轉換 o false,反之 2)dataSourceUpdateMode 決定控件上數值的改變在何時提交回數據源 3)nullValue DBNull、 null和Nullab<T>對應的值。 4)formatString 格式轉換 5)formatInfo 一個實現IFormatProvider接口的對象引用,用來自定義格式轉換 要了解類型如何轉換的,請學習Type Conversions and Format Providers相關內容。關於上面屬性的應用,請看下面介紹 9.3基於Binding類的內置機制(屬性、參數)進行類型轉換 通過Binding類構造時的參數,或屬性設置,可以控制它進行類型轉換的機制。 1)DateTime 下面先介紹一個DateTime類型的例子,使用DateTimePicker控件 //創建Binding,設置formattingEnabled為true birthDateTimePicker.DataBindings.Add("Value",m_EmployeesBindingSource, "BirthDate", true); //設定為使用自定義格式 birthDateTimePicker.Format = DateTimePickerFormat.Custom; //設定格式 birthDateTimePicker.CustomFormat = "MM/dd/yyyy"; 2)Numeric salaryTextBox.DataBindings.Add("Text", employeesBindingSource,"Salary", true, DataSourceUpdateMode.OnValidation,"<not specified>", "#.00"); 以上代碼做了以下處理 * 設定formattingEnabled為true:代表自動類型轉換 * 設定DataSourceUpdateMode為OnValidation: * 設定nullValue為"<not specified>":這些DBNull就顯示為,"<not specified>", 同時用戶錄入,"<not specified>"時,數據值為DBNull * 設定formatString為"#.00":數值保留2位小數 9.4. 事件 下面介紹Binding的主要事件,以及如何基於這些事件進行類型轉換的控制。 主要事件: 1)Format事件 發生在從數據源獲取數據后,控件顯示此數據之前。在這個事件里將數據源的數據類型轉換為控件要求的數據類型。 2)Parse事件 與Event相反。它發生控件值改變后,數據更新回數據源之前。在這個事件里將控件的數據類型轉換為數據源要求的數據類型。 這兩個事件為我們控制數據提供了機制,它們都聲明為ConvertEventHandler類型, void ConvertEventHandler(object sender, ConvertEventArgs e); 有兩個參數,第二個參數ConvertEventArgs e 提供了我們要formatting和parsing的數據。它有兩個屬性 * e.DesiredType是數值要轉換的目標類型 * e.Value是要轉換的數值。我們可以替換此Value 9.5. 基於事件的類型轉換 9.5.1 處理Format Event void OnCountryFromFormat(object sender, ConvertEventArgs e) { if (e.Value == null || e.Value == DBNull.Value) { pictureBox.Image = null; return; } //綁定的是數據源的CountryID字段,因此e.Value返回的ID號,通過此ID號取得對應數據行 CountriesRow countryRow = GetCountryRow((int)e.Value); //將e.Value賦值為CountryName,從而在控件中顯示名稱 e.Value = countryRow.CountryName; // 數據轉換 ImageConverter converter = new ImageConverter(); pictureBox.Image = converter.ConvertFrom(countryRow.Flag) as Image; } 9.5.2 處理Format Event void OnCountryFromParse(object sender, ConvertEventArgs e) { // Need to look up the Country information for the country name ExchangeRatesDataSet.CountriesRow row = GetCountryRow(e.Value.ToString()); if (row == null) { string error = "Country not found"; m_ErrorProvider.SetError(m_CountryFromTextBox, error); m_CountryFromTextBox.Focus(); throw new ArgumentException(error); } e.Value = row.CountryID; } 10 完成數據編輯 經常會遇到這種情況,你在一個控件中錄入或選擇一些數據,只有當年離開此控件時,關聯的數據才能同步更新。這個問題是由DataRow內部機制決定的。 DataRowView類實現IEditableObject接口,支持對象的事務性編輯(當你確認完成編輯前,可以回滾數據)。我們通過BeginEdit()方法來開始數據編輯,通過EndEdit()方法提交編輯。 不要將DataRowView的EndEdit()與DataSet、DataTable、DataRow的AcceptChanges()方法混淆。 DataRow有original和current版本,同時IEditableObject的caching機制讓它有transient版本,在調用 EndEdit()方法前,數據修改是不會提交到數據源。這就是前面問題的內在原因。 如果希望編輯的數據立即提交,那調用 EndEdit()函數的最佳位置就是Validated事件。Validate事件在控件錄入的數據parsed,並且通過validate后觸發,在這個事件中觸發EndEdit()就會通知綁定到同一數據源的所有控件,從而實現數據同步更新。 private void OnCountryTextValidated(object sender, EventArgs e) { exchangeRatesBindingSource.EndEdit(); } 當然,當前數據項改變時,也會觸發EndEdit()事件 11 使用AutoComplete 當你希望TexbBox或ComboBox中會自動提示功能,那你應該學習一下AutoComplete功能。下面以TextBox為例介紹相關步驟 1)設定TextBox的AutoCompleteSource屬性:FileSystem, HistoryList, RecentlyUsedList 2)如果希望使用自定義的列表,則設定AutoCompleteSource屬性為CustomSource 3)設定AutoCompleteMode為SuggestAppend。這意味着你輸入部分字符時,控件在下拉列表中提示所有相近的數據 4)如果不想使用內置的提示源,你可以自己創建一個AutoCompleteStringCollection類的列表, 5)創建這個列表后,將它賦給TextBox的AutoCompleteCustomSourc屬性 12 DataBinding的生命周期 BindingSource的DataSourceUpdateMode屬性是關鍵,它有以下三種可能值,下面分布以TextBox控件為例介紹此屬性不同時DataBinding的生命周期 1)OnValidating(缺省值) * DataBinding的生命周期: TextBox.Leave, TextBox.Validating, Binding.Parse, TextBox.Validated * 此時若將控件的CausesValidation屬性設為false,那么Validating事件就不會發生 2)OnPropertyChanged * DataBinding的生命周期: 此時,每次控件值發生改變時都會觸發Binding.Parse。對TextBox控件來說,每次錄入字符都會觸發Binding.Parse。 3)Never 此時Parse事件不會觸發,也就是說控件將成為只讀的。 13 子父綁定 前面介紹了主細綁定,它其實是一個父子綁定。有時我們希望由子到父的關聯綁定,下面我們就一起來實現這個機制。實現這個機制的關鍵還是Event,這個Event就是BindingSource的CurrentChanged事件 private void OnCurrentChanged(object sender, EventArgs e) { // 獲取當前的子DataRow ExchangeRatesDataSet.ExchangeRatesRow currentRow = (ExchangeRatesDataSet.ExchangeRatesRow) ((DataRowView)m_ExchangeRatesBindingSource.Current).Row; // 獲取關聯的父DataRow ExchangeRatesDataSet.CountriesRow fromCountryRow = currentRow.CountriesRowByFK_ExchangeRates_CountriesFrom; ExchangeRatesDataSet.CountriesRow toCountryRow = currentRow.CountriesRowByFK_ExchangeRates_CountriesTo; //顯示父DataRow的信息 if (fromCountryRow != null && toCountryRow != null) { m_FromCountryCombo.SelectedValue = fromCountryRow.CountryID; m_ToCountryCombo.SelectedValue = toCountryRow.CountryID; } } 14 綁定到數據的多個復本 有 時,我們希望以不同角度看到同一數據,這時需要綁定到同一數據的多個復本。這里的關鍵是CurrencyManager類,每個 BindingSource管理着一個CurrencyManager。如果多個控件綁定到同一個BindingSource,那么只有一個 CurrencyManager,因此也就只有一個CurrentItem,這樣就造成這些綁定到同一BindingSource的控件同步刷新。要解決這個問題,我們需要多個CurrencyManager,也就是說我們可以創建多個BindingSource,且綁定到同一個數據源。 9.5 處理Null類型 這里有兩個概念要弄清楚,.Net內置的Null類型與代表數據庫中的Null類型,以及它們的區別。 1).Net內置的Null類型 * Nullable,引用類型 * Nuallable<T>,值類型 2).Net用來代表數據庫中的Null類型 * DBNull,它有一個屬性Value,可以用來判斷數據是否為DBNull if (northwindDataSet.Employees[0].Country == DBNull.Value) { // Handle null case here } 對強類型數據集 if (northwindDataSet.Employees[0].IsCountryNull()) { // Handle null case here } 1)AddNew()函數:用來添加一條數據,返回類型由綁定的DataSource決定。 1)綁定到DataSet/DataTable時,返回DataRowView對象。 注意: a)返回的不是DataSet或DataTable或DataRow。 b)如果希望獲取添加的數據,需要進行類型轉換 //bs為你創建的BindingSource DataRow row=(DataRow)((DataRowView) bs.AddNew()).Row; c)使用TypedDataSet時,轉換方法與上面類似,只是用TypedDataRow而已 //MyDataRow為你定義的TypedDataRow MyDataRow row=(MyDataRow)((DataRowView) bs.AddNew()).Row;