本文适用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[].CellTemplate = cell;//将要使用特殊单元格的列的CellTemplate指定为单元格实例
dgv.Columns[].CellTemplate = cell;//多个列可以共用一个实例
...
对于本例而言,做完上述工作即可解决dgv单元格全选的问题。完整的自定义单元格控件的套路请自行参考MSDN。
应猿友要求,放上demo:http://pan.baidu.com/s/1qWzKf60
-文毕-