本文適用Winform開發,且DataGridView的數據源為DataTable/DataView的情況。
理解前提:熟知DataTable、DataView
求:更好方案
考慮這樣一個場景:
某DataTable(下稱dt)的B列是計算列(設置了Expression屬性),是根據A列的數據計算而來,該dt被綁定到某個DataGridView(下稱dgv),A、B兩列都要在dgv中顯示,其中A列可編輯(ReadOnly=false)。需求是對A列進行編輯時(輸入或刪除),B列能實時變化。例如下面的例子:

【目標文件名】是根據【款號】和【色號】計算而來(連接字符串),當編輯款號/色號時,目標文件名能實時變化。
熟悉dgv的猿友都知道,如果不做特別處理,是達不到上述效果的。原因是dgv默認是等焦點離開編輯單元格(CurrentCell),才會提交更改到數據源,而且就算焦點離開,但如果焦點仍在同一行(即CurrentCell改變,但CurrentRow沒變)的話,該行的源行也仍然處在編輯狀態(DataRowView.IsEdit為true),計算列也同樣不會更新。非得是焦點離開這一行(去到別的行,或者其它控件),計算列才會更新。——這段話信息量略大,不熟悉dgv提交機制的猿友可能得借助下面進一步的說明才能明白~老鳥請繞道。先認識幾個概念:
- dgv單元格:DataGridViewCell
- dgv行:DataGridViewRow
- dgv行的源行:DataRowView。當dgv綁定數據源后,它的每一行就對應了數據源中的一行(或叫一項),這就是我所謂的【源行】。可以通過DataGridViewRow.DataBoundItem屬性獲得,該屬性類型是object,當dgv的數據源為DataTable或DataView(下稱dv)時,DataBoundItem的真實類型就是DataRowView,可以理解為DataView的行。而dv又是根據dt來的,所以dv背后又對應一個dt,所以DataRowView背后也對應一個DataRow,可通過DataRowView.Row獲得該DataRow。簡單表示就是,DataGridViewRow(訪問DataBoundItem屬性)→DataRowView(訪問Row屬性)→DataRow
- dgv有單元格的概念和實體類(DataGridViewCell),但dt和dv沒有,后者只到行這一級,雖然可以通過DataRow[x]或DataRowView[x]訪問單元格的值,但在類層級上並不存在DataCell這樣的表示單元格的實體類,也就是dt和dv的編輯/提交等操作是以【行】為單元
下面是dgv的常規提交流程:
①編輯dgv單元格→②完成編輯(離開焦點)→③提交數據源(源行仍處於編輯狀態)→④焦點離開dgv行→⑤源行結束編輯狀態→⑥源行更新計算列(其實完整流程還包括別的環節,比如單元格數據驗證,但這里只說與提交直接相關的環節)。
可以看到,計算列得到更新的關鍵有兩處:
- dgv單元格的數據要提交到數據源相應單元格
- 源行結束編輯狀態
按常規提交流程,必須使焦點離開單元格所在的行(只離開單元格都不行哦)才能達到目的,而我們的需求是,編輯的過程中就要實時更新,不要說離開行,連單元格都不想離開。
一、解決實時更新計算列的問題
可以通過dgv的CurrentCellDirtyStateChanged事件達到目的:
private void dgv_CurrentCellDirtyStateChanged(object sender, EventArgs e) { //判斷當前單元格是否存在未提交的更改,只有存在才繼續。 //此判斷有必要,因為下面的dgv.CommitEdit也會觸發該事件,但此時IsCurrentCellDirty已為false, //如果不做判斷,將會重復進入,造成無謂消耗 if (dgv.IsCurrentCellDirty) { //將單元格值提交給數據源,dgv.EndEdit()也能做到提交,但那樣會使單元格結束編輯狀態 //而dgv.CommitEdit()則會保持編輯狀態 //參數是提供給DataError等事件的原因 dgv.CommitEdit(DataGridViewDataErrorContexts.Commit); //人工結束源行的編輯狀態。只有這樣,源行的計算列才會更新 (dgv.CurrentRow.DataBoundItem as DataRowView).EndEdit(); //或者執行DataRow的EndEdit()也能達到同樣目的 //(dgv.CurrentRow.DataBoundItem as DataRowView).Row.EndEdit(); } }
通過這個事件做了上面要做的兩個事,即①將dgv單元格值更新到數據源;②結束源行編輯狀態。按說到這里就搞掂了,事實上也的確能使計算列實時反映輸入,但卻存在另一個體驗層面的問題,就是單元格會在每次鍵入后內容全選,如圖:

也就是如果要連續輸入,必須在每次輸入后用鼠標或方向鍵取消全選並將光標定位到正確的位置~這不蛋疼嗎,必須解決!首先為什么會全選的原因不明,我猜是由於數據源的更新反過來影響dgv所致。嘗試過用CellEnter、CellBeginEdit、EditingControlShowing、dgv.EditingControl等東西都不理想,不是根本沒用,就是輸入焦點不對,總之着實折騰了一番,最后總算另辟蹊徑,完美解決。
二、解決鍵入后自動全選的問題
我是從控件消息這塊打的主意,dgv的單元格實際上承載了某種編輯控件(如TextBox,CheckBox),所以甭管它是什么原因全選,最后總該是收到了什么消息它才全選,那么我就用spy++截獲消息,果然有發現:

粗略一看,是EM_SETSEL,經過了解,就是EM_SETSEL,所以接下來要做的就是自定義一個文本編輯控件,讓它忽略這個消息,完了讓這個控件成為dgv單元格中的文本編輯控件。了解一番,有如下套路:
- 編寫承載控件。需繼承基礎控件,並實現System.Windows.Forms.IDataGridViewEditingControl接口。由於我只是想屏蔽現有控件的某個消息,並不是要從頭編寫功能控件,所以直接繼承DataGridViewCell承載的文本框控件DataGridViewTextBoxEditingControl即可,因為該控件已經實現上述接口:
public class DataGridViewTextBoxUnSelectableEditingControl : DataGridViewTextBoxEditingControl { protected override void WndProc(ref Message m) { //EM_SETSEL消息的常量是0xb1 if (m.Msg == 0xb1) { return; } base.WndProc(ref m); } }
- 編寫承載上述控件的DataGridViewCell。需繼承自DataGridViewCell或其子類。同樣,本例我只需繼承自DataGridViewTextBoxCell即可:
public class DataGridViewTextBoxUnSelectableCell : DataGridViewTextBoxCell { //僅需重寫該屬性,指明承載的控件類型即可 public override Type EditType { get { return typeof(DataGridViewTextBoxUnSelectableEditingControl); } } }
- 設置要使用上述單元格的dgv列(DataGridViewColumn)的CellTemplate屬性,為上述單元格的實例,多個列可以設為同一實例。CellTemplate最好盡早設置,比如在窗體構造函數中,緊跟InitializeComponent()方法設置;
InitializeComponent(); var cell = new DataGridViewTextBoxUnSelectableCell(); dgv.Columns[0].CellTemplate = cell;//將要使用特殊單元格的列的CellTemplate指定為單元格實例 dgv.Columns[1].CellTemplate = cell;//多個列可以共用一個實例 ...
對於本例而言,做完上述工作即可解決dgv單元格全選的問題。完整的自定義單元格控件的套路請自行參考MSDN。
應猿友要求,放上demo:http://pan.baidu.com/s/1qWzKf60
-文畢-
