博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

DataTable,DataView和DataGrid中一些容易混淆的概念

Posted on 2007-02-27 11:11  .NET酷  阅读(265)  评论(0编辑  收藏  举报
 

一、DataTable

    DataTable表示内存中数据的一个表,它完全是在内存中的一个独立存在,包含了这张表的全部信息。DataTable可以是从通过连接从数据库中读取出来形成的一个表,一旦将内容读到DataTable中,此DataTable就可以跟数据源断开而独立存在;也可以是完全由程序自己通过代码来建立的一个表。

◆ DataColumn

一个表是由行和列组成的一个两维的结构。表的结构是由DataColumn 对象的集合组成,DataColumn 对象集合可由DataTable.Columns 属性中能获取到,通过定义每一列的数据类型来确定表的架构,类似数据库中定义表。定义完表的结构就可以根据结构来生成DataRow,用DataTable.NewRow()方法来生成此DataTable结构的新行。
一个DataTable是由DataRow的集合组成的, DataRow的集合这个可以由DataTable.Rows 属性来访问。

DataTable还可以通过现有的列用Expression 属性的表达式创建一些列。

1、创建计算出的列
比如:已经有了一个表结构,表中有一个DataColumn的集合,其中有一个叫UnitPrice的列,你可以新建一个DataColumn,设置好ColumnName,再设置此列的表达式,DataColumn.Expression = "UnitPrice * 0.086",这个列的值就是名字为UnitPrice的列计算出来的,在创建表达式时,使用 ColumnName 属性来引用列。

2、第二个用途是创建聚合列
聚合列聚合通常沿着关系执行(有关关系的描述见下面DataRelation部分),如果order表有名为 detail 的子表,两个表之间通过order.orderid和detail.orderid两个列建立一个关系 DataRelation 对象名为“order2detail”,在主表order中就可以建立一个聚合列,将计算每个order在detail表中含有的所有item的价格的和:DataColumn.Expression = “sum(child(order2detail).price)",child(order2detail)表示通过关系order2detail联系到的子表,child(order2detail).price就表示子表的price列。

◆ DataRow

DataRow对象没有直接在代码中使用的构造函数,一般是从具有一定结构的DataTable用NewRow()方法来新建一个DataRow对象。一个DataRow根据其是独立的,还是属于某个DataTable,是否修改过,是否被DataTable删除等等不同的情况有不同的状态,由DataRow.RowState属性公开,如下表:

成员名称
说明
Added 该行已添加到 DataRowCollection 中,AcceptChanges 尚未调用。Deleted 该行已通过 DataRow 的 Delete 方法被删除。
Deleted 该行已通过 DataRow 的 Delete 方法被删除。
Detached 该行已被创建,但不属于任何 DataRowCollection。DataRow 在以下情况下立即处于此状态:创建之后添加到集合中之前;或从集合中移除之后。
Modified 该行已被修改,AcceptChanges 尚未调用。
Unchanged 该行自上次调用 AcceptChanges 以来尚未更改。

一个DataRow对象刚被创建之后其状态是Detached,是孤立的一个存在,所以建立了DataRow之后在DataRow中的单元填充了数据后还要通过DataTable.Rows.Add(DataRow)方法将此DataRow添加到DataTable,DataRow添加到DataTable后, 这个DataRow的状态就转变为Added。当修改了这个DataRow后,这个DataRow状态转为Modified,当用DataRow.Delete()方法删除DataRow后,DataRow状态将转为Deleted,不过此行还存在在DataTable中的,只是状态改变了,这时用DataTable.Rows.Count查看行数,跟删除前是一样的。只有在调用了DataTable.Remove(DataRow)方法后,此DataRow才被从DataTable移除,状态也回复到Detached孤立状态。

一旦调用了DataTable.AcceptChanges()方法后,所有的行将根据不同的状态做不同的处理,Added、Modified、Unchanged将保留当前值,Deleted的行将从DataTable中移除,最后所有的行的状态都置为Unchanged。当DataTable是从DataAdapter.Fill(DataSet,DataTable)方法填充而形成的,Fill()方法将自动调用AcceptChanges()方法,将DataTable的行状态都置为Unchanged。并且,如果Fill方法中指定的那个DataTable在要填充的那个DataSet不存在时,会生成一个跟数据源表同样的结构的DataTable并填充数据。

◆ DataRelation

表示两个 DataTable 对象之间的父/子关系。可以类比于数据库中的表之间的关系,父表相当于关系列为主键的表,子表相当于关系列为外键的表。DataRelation 构造函数一般为:DataRelation(String, DataColumn, DataColumn) ,string为关系名,第一个DataColumn为建立关系的父表列,第二个DataColumn为建立关系的子表列,建立关系的两个列的 DataType 值必须相同。

建立好了关系,必须把这个关系加入到DataTable的ParentRelations属性或ChildRelations 属性,这两个属性包含这个表的所有的跟父表的关系和跟子表的关系。若关系中此表是父表则将此关系加入到ChildRelations集合中,否则加入到ParentRelations集合中。

二、DataView

DataView表示用于排序、筛选、搜索、编辑和导航的 DataTable 的可绑定数据的自定义视图。可以将DataView同数据库的视图类比,不过有点不同,数据库的视图可以跨表建立视图,DataView则只能对某一个DataTable建立视图。DataView一般通过DataTable.DefaultView 属性来建立,再通过通过RowFilter 属性和RowStateFilter 属性建立这个DataTable的一个子集。

RowFilter属性用来筛选要查看DataTable中哪些行的表达式,这个表达式同上面所说的建立计算列的表达式相同。例如:"LastName = 'Smith'",这就是只查看列LastName的值为'Smith'的那些数据行。

RowStateFilter 属性用来设置 DataView 中的行状态筛选器,上面介绍DataRow时介绍了DataRow的状态,一个DataRow可能有五种状态,RowStateFilter就是可以通过这些状态来筛选要查看的行集。其实DataRow不仅有五种状态,DataRow还有版本的问题,比如当DataRow的状态为Modified,即这行已经被修改了,这时这个DataRow就会有两个版本,Current版本和Original版本(修改前的)。实际上RowStateFilter属性是综合了DataRow的状态和版本来筛选的(RowStateFilter确省值是CurrentRows)见下表:

成员名称
说明
Added 一个新行。
CurrentRows 包括未更改行、新行和已修改行的当前行。
Deleted 已删除的行。
ModifiedCurrent 当前版本,原始数据(请参阅 ModifiedOriginal)的修改版本。
ModifiedOriginal 原始版本(尽管它后来已被修改并以 ModifiedCurrent 形式存在)。
None 无。
OriginalRows 包括未更改行和已删除行的原始行。
Unchanged 未更改的行。

DataView.Count属性得到的计数是在应用了 RowFilter 和 RowStateFilter 之后,获取 DataView 中记录的数量。

DataView是建立在DataTable基础上的,DataView.Table 属性可以得到此DataView对应的那个DataTable。DataView的行叫DataRowView,可以从DataRowView直接通过DataRowView.Row 属性得到此DataRowView对应的DataRow。

三、DataGrid

这里说的DataGrid是winform中的DataGrid,一般都是跟DataView绑定来显示DataTable中的数据,和修改DataTable中的数据。
DotNet的DataGrid的功能强大,可是在使用上与以前的习惯不太一样,有时还比较麻烦,所以很多人都对这个DataGrid感到有些摸不着头脑,有一种无从下手的感觉,其实把一些概念搞清楚了许多问题就会迎刃而解了。

DataGrid通过DataSource 和 DataMember 属性来绑定其要显示的数据源。数据源一般是DataTable、DataView、DataSet等,不过将这些数据源绑定到DataGrid时实际上是绑定的DataView。若数据源是DataTable时,实际上是绑定了此DataTable的DefaultView,若数据源是DataSet时,则可以向 DataMember 属性设置一个字符串,该字符串指定要绑定到的表,然后再将DataMember指定的那个DataTable的DefaultView绑定到DataGrid。

所以DataGrid实际显示的是DataTable经过筛选的DataView。

◆ DataGrid以何种方式显示DataView的数据

DataGrid绑定到一个DataView后,由DataGrid.TableStyles中的DataGridTableStyle 对象的集合来控制这个DataView的哪些列要显示,列的宽度多少,列标头的文本是什么等等。确省的DataGrid.TableStyles中不包含任何对象,这时DataGrid将会按照DataView列的顺序将所有的列都显示出来。一般应用中都会设置TableStyles来控制显示的内容及格式。

例如DataGrid绑定到一张叫order的DataTable,这个DataTable包含了OrderID、CustomerID、OrderDate、ShipName、ShipAddress等字段,如果不用TableStyles来控制显示的列和格式,将得到以下的显示结果:

figure-1

可以看到DataGrid将会按照DataView列的顺序将所有的列都显示出来

我们只想显示OrderID、CustomerID、OrderDate这三个字段,并且想将OrderID的列表头显示为“订单号”,CustomerID显示为“客户号”,OrderDate显示为“订单日期”,这就要用TableStyles来控制了。
新建一个TableStyle,将此TableStyle.MappingName属性对应到这个TableStyle要控制的那个DataTable的名字:

  DataGridTableStyle myTableStyle = new DataGridTableStyle();
myTableStyle.MappingName = "myDateTable";

再建立三个DataGridColumnStyle,分别用来控制将要显示的三个列:

 

DataGridColumnStyle myColumnStyle1 = new DataGridTextBoxColumn();
myColumnStyle1.MappingName = "OrderID";
myColumnStyle1.HeaderText = "订单号";

DataGridColumnStyle myColumnStyle2 = new DataGridTextBoxColumn();
myColumnStyle2.MappingName = "CustomerID";
myColumnStyle2.HeaderText = "客户号";

DataGridColumnStyle myColumnStyle3 = new DataGridTextBoxColumn();
myColumnStyle3.MappingName = "OrderDate";
myColumnStyle3.HeaderText = "订单日期";

将这三个DataGridColumnStyle添加到TableStyle中:

 

myTableStyle.GridColumnStyles.Add(myColumnStyle1);
myTableStyle.GridColumnStyles.Add(myColumnStyle2);
myTableStyle.GridColumnStyles.Add(myColumnStyle3);

最后将TableStyle添加到DataGrid中:

 

dataGrid1.TableStyles.Add(myTableStyle);

将 TableStyle添加到DataGrid后,再绑定数据源,这时我们就会看到这样的数据显示了:

figure-2

◆ DataGrid的编辑修改

DataGrid支持对DataGrid所显示的DataTable的编辑修改,只要DataGrid的ReadOnly属性为False,就可以在DataGrid中直接修改单元中的内容,修改完后数据将直接反应到此DataGrid对应的那个DataTable的单元。

如果这个DataTable是通过vs.net的可视化数据设计器新建DataAdapter,并生成了SelectCommand、InsertCommand、UpdateCommand、DeleteCommand这四个命令,用DataAdapter的Fill方法得来的,那么事情就简单了,修改过的DataTable你可以直接用DataAdapter的UpDate方法写回到数据库。下面看一下vs.net的可视数据数据器生成的InsertCommand命令:

this.sqlInsertCommand1.CommandText = @"INSERT INTO Customers(CustomerID, CompanyName, ContactName, ContactTitle, Address, City, Region, PostalCode, Country, Phone, Fax) VALUES (@CustomerID, @CompanyName, @ContactName, @ContactTitle, @Address, @City, @Region, @PostalCode, @Country, @Phone, @Fax); SELECT CustomerID, CompanyName, ContactName, ContactTitle, Address, City, Region, PostalCode, Country, Phone, Fax FROM Customers WHERE (CustomerID = @CustomerID)";
this.sqlInsertCommand1.Connection = this.sqlConnection2;
this.sqlInsertCommand1.Parameters.Add(new System.Data.SqlClient.SqlParameter("@CustomerID", System.Data.SqlDbType.NVarChar, 5, "CustomerID"));
this.sqlInsertCommand1.Parameters.Add(new System.Data.SqlClient.SqlParameter("@CompanyName", System.Data.SqlDbType.NVarChar, 40, "CompanyName"));
this.sqlInsertCommand1.Parameters.Add(new System.Data.SqlClient.SqlParameter("@ContactName", System.Data.SqlDbType.NVarChar, 30, "ContactName"));
this.sqlInsertCommand1.Parameters.Add(new System.Data.SqlClient.SqlParameter("@ContactTitle", System.Data.SqlDbType.NVarChar, 30, "ContactTitle"));
this.sqlInsertCommand1.Parameters.Add(new System.Data.SqlClient.SqlParameter("@Address", System.Data.SqlDbType.NVarChar, 60, "Address"));
this.sqlInsertCommand1.Parameters.Add(new System.Data.SqlClient.SqlParameter("@City", System.Data.SqlDbType.NVarChar, 15, "City"));
this.sqlInsertCommand1.Parameters.Add(new System.Data.SqlClient.SqlParameter("@Region", System.Data.SqlDbType.NVarChar, 15, "Region"));
this.sqlInsertCommand1.Parameters.Add(new System.Data.SqlClient.SqlParameter("@PostalCode", System.Data.SqlDbType.NVarChar, 10, "PostalCode"));
this.sqlInsertCommand1.Parameters.Add(new System.Data.SqlClient.SqlParameter("@Country", System.Data.SqlDbType.NVarChar, 15, "Country"));
this.sqlInsertCommand1.Parameters.Add(new System.Data.SqlClient.SqlParameter("@Phone", System.Data.SqlDbType.NVarChar, 24, "Phone"));
this.sqlInsertCommand1.Parameters.Add(new System.Data.SqlClient.SqlParameter("@Fax", System.Data.SqlDbType.NVarChar, 24, "Fax"));

DataAdapter的SelectCommand是用来DataAdapter.Fill()方法来填充DataTable的,SelectCommand选择的数据表行集将被填充到DataTable中,然后DataGrid将它显示出来。

DataGrid在经过编辑修改后,其对应的DataTable中的行就可能出现文章上面所述的那五种状态,可能是新加的(Added),可能是修改了的(Modified),可能是删除的(Deleted),DataAdapter.UpDate()方法将通过调用InsertCommand命令将状态为Added的行插入到数据库,UpdateCommand将状态为Modified的行在数据库中做修改,DeleteCommand将状态为Deleted的行在数据库真正的删除。

如果不是通过vs.net的可视化数据设计器新建DataAdapter,没有自动生成SelectCommand、InsertCommand、UpdateCommand、DeleteCommand这四个命令,那么就可能需要自己写InsertCommand、UpdateCommand、DeleteCommand命令,有一种情况就是当SelectCommand至少返回一个主键列或唯一的列时,可以通过SqlCommandBuilder来自动根据SelectCommand命令来自动生成另外三个更新命令,例如:

SqlConnection myConn = new SqlConnection(myConnection);
SqlDataAdapter myDataAdapter = new SqlDataAdapter();
myDataAdapter.SelectCommand = new SqlCommand(mySelectQuery, myConn); //建立DataAdapter的SelectCommand命令
SqlCommandBuilder custCB = new SqlCommandBuilder(myDataAdapter);//建立此DataAdapter的CommandBuilder,
         //这样系统就会给此DataAdapter自动生成InsertCommand、UpdateCommand、DeleteCommand三个命令。

否则,要用DataAdapter.UpDate()方法更新数据库就要自己写InsertCommand、UpdateCommand、DeleteCommand这三个命令,可以参考上面给出的vs.net自动生成的InsertCommand命令的写法。

◆ 数据绑定的同步

WinForm中很多控件都可以与数据源绑定,绑定又分两种情况:

简单数据绑定
简单数据绑定指将一个控件绑定到单个数据元素(如数据集表的列中的值)的能力。这是用于控件,如 TextBox 控件或 Label 控件(即通常只显示单个值的控件)的典型绑定类型。事实上,控件上的任何属性都可以绑定到数据库中的字段。

复杂数据绑定
复杂数据绑定指将一个控件绑定到多个数据元素的能力,通常绑定到数据库中的多条记录,或者绑定到多个任何其他类型的可绑定数据元素,一般是绑定到一个DataView。支持复杂绑定的控件的示例有DataGrid、ListBox 和 ErrorProvider 控件。

一般DataGrid控件都是跟一个DataView绑定,DataGrid的数据绑定属于复杂绑定,因为它绑定到有多条记录的表,DataGrid有两个属性同数据绑定有关:

DataGrid.DataSource 属性:获取或设置DataGrid所显示数据的数据源。一般是跟DataTable 、DataView 、DataSet 绑定,如果DataSource设定为DataSet,则引用包含的表不止一个,则必须向 DataMember 属性设置一个字符串,该字符串指定要绑定到的表。

DataGrid.DataMember 属性:获取或设置 DataSource中的特定列表,就是上述DataSource设定为DataSet时,要设定此属性来指定要绑定到的表。

经常有这种需求,一个窗体中有一个DataGrid,显示了一些数据,窗体上还有一些TextBox控件,用来显示DataGrid中的当前行的数据,一个TextBox控件对应DataGrid行的一个列,当DataGrid的当前行移动时,TextBox控件中的值也会跟着显示改变后的DataGrid的当前行。

要保证这些数据绑定控件保持同步就要一个统一管理数据绑定的机制来保证这些控件的同步,DotNet中负责数据同步的是BindingManagerBase,它是用来管理数据源的,绑定到同一个数据源的数据绑定控件都可以由BindingManagerBase统一管理。BindingManagerBase可以由Form.BindingContext.Item属性获得,此属性有两种重载:

public BindingManagerBase this[object DataSource] //获取与指定数据源关联的 BindingManagerBase
public BindingManagerBase this[object DataSource, string DataMember] //获取与指定数据源和数据成员相关联的一个 BindingManagerBase

所有的数据绑定控件的数据源同建立BindingManagerBase时传递的对象一样的,都将属于这个BindingManagerBase管理,比如,建立一个如下的BindingManagerBase:

BindingManagerBase myBindingManagerBaseParent = this.BindingContext[myDataSet,"customers"];

如果Form上有个DataGrid的DataGrid.DataSource = myDataSet;DataGrid.DataMember = "customers",那么这个DataGrid的数据源就在myBindingManagerBaseParent的管理之下了。
同样简单数据绑定的控件的DataSource也是跟 BindingManagerBase的DataSource一样,DataMember是BindingManagerBase的DataMember指定的那个表的某一列时,这个控件的数据源也在这个myBindingManagerBaseParent管理之下了:

dataGrid1.DataSource = myDataSet;
dataGrid1.DataMember = "customers";
textCustomerId.DataBindings.Add(new Binding("Text",myDataSet,"customers.customerid")); //TextBox的Text属性跟
            //myDataSet的customers表的customerid字段绑定

BindingManagerBase控制的数据源有个当前行的概念,控件一旦跟数据源绑定后,DataGrid将显示数据源表的所有数据,不过在DataGrid的行标头里有个黑色的三角箭头用来指示当前行。简单绑定控件中显示的值将是数据源当前行的内容。

所以,只要我们改变BindingManagerBase的指针就行了,这个可以在界面上通过点击要到的那一行来改变当前行,也可以在程序中改变当前行的设置:

myBindingManagerBaseParent.Position = 10;

BindingManagerBase.Position属性的变化就会引起BindingManagerBase当前行的变化,也就是跟这个数据源绑定的DataGrid的当前行的变化,简单绑定控件的显示内容也就随之改变了,如下图。

figure-3
当前行是第一行,上面的三个
TextBox控件也显示第一行的数据

figure-4
当前行是第四行,上面的三个
TextBox控件也显示第四行的数据

BindingManagerBase的DataSource可以是DataSet,DataSet中可以有多个DataTable,这些DataTable可以通过DataRelaton(关系)联系在一起,形成父表/子表的关系。比如,还是上面举过的例子,一个DataGrid显示Customer表,同时还想要有一个DataGrid来显示当前Customer所有的order。这样我们就会需要两个BindingManagerBase了,一个BindingManagerBase对应Customer表,另一个BindingManagerBase对应order表,而且这个order表还要考虑到同Customer表的关系。

对应Customer的BindingManagerBase上面我们已经建立好了,下面我们来建立对应order的BindingManagerBase:

首先我们要建立Customer表和order表之间的关系myRelation:

DataColumn ParentColumn = myDataSet.Tables["customers"].Columns["customerid"]; //要建立关系的父表的列,相当于主键
DataColumn ChildColumn = myDataSet.Tables["orders"].Columns["customerid"]; //要建立关系的子表的列,相当于外键
DataRelation myRelation = new DataRelation("myRelation",ParentColumn,ChildColumn,false); //根据父表,子表的相关列建立关系

然后,通过关系,建立对应order表的BindingManagerBase:

myBindingManagerBaseChild = this.BindingContext[myDataSet,"customers.myRelation"]; //这个数据源将解析为一个父表中的客户对应的所有的order

这样,当对应Customer的BindingManagerBase的当前行改变时,对应order的BindingManagerBase也将跟着变化,他们之间的关系是由myRelation决定的,如下图:



figure-5
对应子表order的DataGrid显示对应父表中ALFKI 客户的所有order,当点击了“主表下移一行”按钮时,父表中的当前客户改变为ANATR时, 子表同样也改变为显示对应ANATR的所有order了

◆ 在程序中访问DataGrid中的内容

DataTable中有数据行DataRow,而在DataGrid中没有行这个对象,这让人感到很不习惯,也觉得不够自然。在DataTable中,一张表的层次结构很清楚,DataTable.Rows属性可以得到这张表所包含的所有行的行集,通过行集的索引DataRowCollection[index]就可以得到具体的一个DataRow,数据行的索引DataRow[index]又可以得到这一行的具体某一列的内容。
而DataGrid中就没有这么方便了,DataGrid只有两个属性可用,DataGrid.CurrentCell 属性,此属性返回一个DataGridCell类型的结构,DataGridCell结构指明此Cell所在的行号和列号。还有一个DataGrid.Item 属性,此属性有两个重载:

 

public object this[DataGridCell] //获取或设置指定的 DataGridCell 的值
public object this[int, int] //获取或设置位于指定行和列的单元格的值

可见,DataGrid中访问都是针对某个Cell进行的。经常的,我们需要从当前的Cell获得此Cell所对应的DataRow,比如界面中可能先选中DataGrid的某一行,或者某一个Cell,然后点击一个按钮,弹出一个新的窗口,窗口中显示这一行的所有单元的内容,并允许修改单元的值,最后保存关闭窗口。这就需要从当前的DataGrid所在的单元找到其所对应的DataTable所在的行和列。

而DataGrid中显示的数据可能经过DataView的DataView.RowFilter属性、DataView.RowStateFilter属性的过滤,还可能经过DataGrid本身根据各个列的正向和反向排序,所以DataGrid的CurrentRowIndex属性所指示的行索引跟其对应的DataTable的行索引有很大的机会是不一样的,不能够根据DataGrid的CurrentRowIndex去获取其对应的DataTable的行。

这时BindingManagerBase又将发挥作用了,我们可以先建立一个对应此DataGrid绑定的数据源的BindingManagerBase,这样这个BindingManagerBase就可以管理这个数据源。

  //设置DataGrid的数据源
dataGrid1.DataSource = myDataSet;
dataGrid1.DataMember = "customers";
//建立同DataGrid同样数据源的BindingManagerBase
BindingManagerBase myBindingManagerBaseParent = this.BindingContext[myDataSet,"customers"];

一旦建立了这个BindingManagerBase,就可以通过BindingManagerBase的当前行的属性来获取当前数据源的记录:

  //BindingManagerBase的Current返回数据源的对象,对于绑定到DataView的数据源,需要将此对象显式
//的转换为 DataRowView类型
DataRowView myDataRowView =(DataRowView) myBindingManagerBaseParent.Current

这样,我们就可以从当前的Cell得到此Cell所在的DataRowView,DataRowView又可以通过DataRowView.Row属性及其方便的得到DataRow。

如果还要进一步,想要得到此Cell所对应的DataTable的具体单元,就是不光要得到DataRow,还要知道这个Cell所对应的列。

这又分两种情况:

一是DataGrid未使用TableStyles来设置DataGrid要显示的列和格式,数据源DataView的所有列都将按照DataView本身的顺序显示出来,这样可以直接取得对应的列索引:

  //获取当前DataGrid单元的列索引,这个索引跟DataTable的索引是一样的
Int ColumnNumber = DataGrid.CurrentCell.ColumnNumber;

另一种情况是DataGrid使用了TableStyles来设置DataGrid要显示的列和格式,这样DataGrid单元的列索引跟DataTable的索引就可能是不一样的了,这就要用DataGrid的TableStyles了:

  Int ColumnNumberDataGrid = DataGrid.CurrentCell.ColumnNumber; //获取当前DataGrid单元的列索引
Int ColumnNumberDataTable = DataGrid.TableStyles[0].GridColumnStyles[ColumnNumberDataGrid].MappingName