.NET DataTable、DataSet 和 DataGrid 实战指南 - 第 4 部分






4.92/5 (85投票s)
2004 年 2 月 24 日
18分钟阅读

707917

3
本文档旨在提供使用 Microsoft .NET DataTables、DataSets 和 DataGrid 的实用指南
5 Data Grid
DataGrid 用于显示独立表格或 DataSet 中包含的表格集合。它还提供用于编辑、删除和插入记录的用户界面,以及一组用于程序化响应和数据更改跟踪的事件通知。此外,它还支持 DataGrid Table Styles 集合,为 DataSource 中的每个表格提供自定义表示。WebForms 和 WinForms 都可以显示绑定或未绑定到数据库的 DataGrid;然而,在使用上存在显著差异。
本节将介绍如何使用 DataGrid 及其众多功能和不同的使用模型。
5.1 方法和属性
| 方法 | 描述 | 
| 
 | 折叠指定的行或所有行。 | 
| 
 | 展开指定的行或所有行。 | 
| 
 | 如果指定的行已展开,则返回 true,否则返回 false。 | 
| 
 | 导航回网格中先前显示的表格。 | 
| 
 | 导航到网格中的表格。 | 
| 
 | 将数据集绑定到 DataSource 并选择一个表格。 | 
| 
 | 此方法选择一行并高亮显示。它不一定等同于当前行。 | 
| 
 | 此方法取消选择一行并关闭高亮显示。它不更改当前行。 | 
| 属性 | 描述 | 
| 
 | 设置为 True 可使用列标题进行列排序。 | 
| 
 | 允许使用例如箭头键或 Tab 键在数据网格内进行导航。 | 
| 
 | 此项设置 DataGrid 中交替行的背景颜色,使其更易于跨行阅读。 | 
| 
 | 此项返回或设置 DataGrid 的数据源。它要么是 DataSet,要么是 DataTable。 | 
| 
 | 此项设置要显示的表格。当 DataSource 是 DataSet 时,DataGrid 显示为层级模式;将其设置为 DataSet 中的某个 Table 会自动强制显示它。 | 
| 
 | 获取或设置当前活动的单元格,包括 CurrentRowIndex。 | 
| 
 | 获取或设置当前行。 | 
5.2 分配数据源
DataGrid 的 DataSource 要么是 Table,要么是 DataSet。例如,使用 DataGrid 的 DataSource 成员将先前创建的 DataTable dt 对象分配给 DataGrid,如下所示:
dg.DataSource = dt; // DataSource contains only a
// DataTable object
可以使用 DataGrid 的 DataSource 成员分配包含多个表格的 DataSet,如下所示:
DataSet ds = new DataSet();
ds.Tables.Add(dt1);
ds.Tables.Add(dt2);
...
ds.Tables.Add(dtn);
dg.DataSource = ds; // DataSource contains a DataSet object
默认情况下,将显示集合中的第一个表格,但可以使用 DataGrid 的 DataMember 属性选择特定表格,如下所示:
dg.DataMember = dt2.TableName;
或等效地:
dg.DataMember = ds.Tables[1].TableName; // zero-based index
或使用 SetDataBinding() 方法同时分配 DataSet 和选择一个表格,如下所示:
dg.SetDataBinding(ds, dt2.TableName);
注意:DataSet 的唯一限制是表格必须具有主键,否则在将 DataSet 分配给 DataSource 时将发生错误。
5.3 格式化
5.3.1 DataGridTableStyle - WinForms
DataTable 用于保存列定义集合和包含每列数据的行集合。DataSet 包含表格集合,DataGrid 提供交互式用户界面来呈现 DataTable 中的表格或 DataSet 中包含的表格。
DataGrid 管理的每个表格的渲染是通过各个表格样式(DataGridTableStyle)的集合完成的,每个表格样式包含一个列样式集合。有关这些关系的图示,请参见图 1。
Microsoft 的 .NET IDE 提供了 DataGridTextBoxColumn 或 DataGridBoolColumn 对象,顾名思义,它们分别是文本框和布尔值或复选框列。文本框列是声明 DataGrid 时使用的默认列类型。
通过列样式属性和方法可实现每列的独特渲染。此外,还可以修改 .NET 提供的列样式或定义继承自基类的新列样式,如 ComboBoxes 和 ImageControls。
注意:WebForms 不提供 DataGridTableStyle,必须使用 itemstyles – 请参见 WebForm 示例。
在以下示例中,假设:
已定义 DataTable dt。
- 已定义 DataSet ds,其 Tables 集合在索引 0 处包含 dt。
- 已定义 DataGrid dg,其 DataSource 包含 ds。
最初,DataGrid dg 的 TableStyles 集合中不包含任何 TableStyles,可以通过 Count 属性值看到。
dg.TableStyles.Count;
此外,dg.Controls 集合仅包含垂直和水平滚动条控件。
// First, a DataGridTableStyle object is declared
// that will hold a collection of
// GridColumnStyles.
System.Windows.Forms.DataGridTableStyle DGStyle =
   new DataGridTableStyle();
// In this example the .NET DataGridTextBoxColumn class is used.
DataGridTextBoxColumn textColumn;
// Loop through each Column in table dt to get a DataColumn
// object that will be used
// to define properties for its TextBoxColumn style.
foreach (DataColumn dc in dt.Columns)
{
  textColumn = new DataGridTextBoxColumn();
  // the MappingName must correspond to the Table Column Name
  // in order to establish the relationship between them
  textColumn.MappingName = dc.ColumnName;
  // the HeaderText value is displayed in Header for the column, here
  // the Caption value is used.
  textColumn.HeaderText = dc.Caption;
  // specify some other property values
  textColumn.Width = 200;
  textColumn.Alignment = System.Windows.Forms.HorizontalAlignment.Left ;
  textColumn.ReadOnly = true;
  // Add this column object with its specifications to the
  // GridColumnStyles collection
  DGStyle.GridColumnStyles.Add(textColumn);
}
// The name of the DataGridTableStyle must match that of the table
// Since the DataGrid can contain multiple tables,
// similar the TableStyles collection
// can contain multiple DataGridTableStyles, one for each table.
DGStyle.MappingName = ds.Tables[0].TableName;
DGStyle.AlternatingBackColor = Color.Gainsboro;
DGStyle.AllowSorting = false;
DGStyle.ReadOnly = true;
// The Clear() method is called to ensure that
// the previous style is removed.
dg.TableStyles.Clear();
// Add the new DataGridTableStyle collection to
// the TableStyles collection
dg.TableStyles.Add(DGStyle);
5.3.2 仅更改单个表格的 DataGridTableStyles
以下展示了如何仅更改单个表格的 DataGridTableStyles。假设表格的样式在 TableStyles 集合的索引 0 处。
// Clear only a single table of its GridColumnStyles
dgConversionTable.TableStyles[0].GridColumnStyles.Clear();
DataGridTextBoxColumn textColumn;
// Loop through each Column in table dt to get a
// DataColumn object that will be used
// to define properties for its TextBoxColumn style.
foreach (DataColumn dc in dt.Columns)
{
  textColumn = new DataGridTextBoxColumn();
  // the MappingName must correspond to the Table Column Name
  // in order to establish the relationship between them
  textColumn.MappingName = dc.ColumnName;
  // the HeaderText value is displayed in Header for the column, here
  // the Caption value is used.
  textColumn.HeaderText = dc.Caption;
  // specify some other property values
  textColumn.Width = 200;
  textColumn.Alignment = System.Windows.Forms.HorizontalAlignment.Left ;
  textColumn.ReadOnly = true;
  // Add this column object with its specifications to
  // the GridColumnStyles collection
  // for TableStyles[0]
  dgConversionTable.TableStyles[0].GridColumnStyles.Add(textColumn);
}
5.3.3 更改表格 DataGridTableStyle 中的单个列
可以通过代码修改现有表格 DataGridTableStyles 集合中的一个或多个列,代码类似如下。在此示例中,第一个方法使用 HeaderText 查找要更改的列,该列位于名为“Demo”的表格中。获得所需对象后,将文本框列“old header text”的当前 HeaderText 值更改为“new header text”。此外,列的 Width 也更改为 150。在第二个示例中,如果 MappingName 与 HeaderText 不同,则 **必须** 使用 Column MappingName 而不是 HeaderText。
5.3.3.1 方法 1 – 遍历集合
在此示例中:
GridColumnStylesCollection gcsColl =
  dgConversionTable.TableStyles[“Demo”].GridColumnStyles;
for (int i=0; i< gcsColl.Count; i++)
{
  if(gcsColl[i].GetType() == typeof(DataGridTextBoxColumn))
  {
   DataGridTextBoxColumn textColumn = (DataGridTextBoxColumn)gcsColl[i];
   If (textColumn.HeaderText == “old header text”)
   {
     textColumn.Width = 150;
     textColumn.HeaderText = “new header text”;
     break;
   }
  }
}
5.3.3.2 方法 2 – 直接访问列字符串名称
如果只想更改特定列,可以使用此方法。请务必使用列的 MappingName,而不是其 Caption 或 HeaderText(如果它们不同)。
GridColumnStylesCollection gcsColl =
   dgConversionTable.TableStyles[“Demo”].GridColumnStyles;
if(gcsColl.Contains("mappingName"))
if(gcsColl["mappingName "].GetType() == typeof(DataGridTextBoxColumn))
{
  DataGridTextBoxColumn textColumn =
  (DataGridTextBoxColumn)gcsColl["mappingName"];
  textColumn.Width = 150;
  textColumn.HeaderText = “new header text”;
}
5.3.4 为基于其数据的表格创建 DataGridTableStyle
DataSet 中的每个表格都可以有自己的表示和属性样式。下面的 formatDG 方法提供了一个示例,说明如何根据每列的数据类型以编程方式定义样式。它强制执行以下业务规则:
列的宽度大于以下两者中的较大者——ColumnName 文本字符串的宽度,或该列中每个单元格内容字符串长度的最大值。最大宽度设置为 100 个字符。在每个字符串中添加字母 'W' 进行填充,因为 'W' 通常是字母表中宽度最大的字符。
- DataType 为 System.String或System.Guid的列具有 ColumnAlignment = HorizontalAlignment.Left 和Readonly = false。
- DataType 为 System.DateTime或typeof(int)的列具有 ColumnAlignment = HorizontalAlignment.Center 和Readonly = false。
- DataType 为 System.Double, System.Float或System.Decimal的列具有 ColumnAlignment = HorizontalAlignment.Right 和Readonly = false,以及“0.00”的格式。
- 将 NullText属性设置为string.empty。也就是说,当字段具有 null 值时,不显示任何文本。这仅适用于文本列,而不适用于布尔列。
- Style Column MappingName是列的ColumnName,它与相应数据库表中的字段名匹配,而其 HeaderText 则设置为列Caption属性中包含的可读性更强的字符串。
- DataGridTableStyle 的名称与表格的名称相同。也就是说,DataSet Table 和 DataGridTableStyle集合通过表格名称同步,这意味着当DataGrid显示表格时,它将使用相应的 TableStyle。
public void formatDG(DataGrid dg)
{
  // Assume DataGrid DataSource is a DataSet that
  // contains at least one table.
  if (dg.DataSource.GetType() == typeof(DataSet))
    {
    DataSet ds = (DataSet)dg.DataSource;
    if (ds != null)
    {
     if(dg.TableStyles.Count > 0)
      dg.TableStyles.Clear();
     float prevSumWidths = 0.0f;
     int maxHeight = 0;
     int calcH = 0;
     foreach (DataTable dt in ds.Tables)
     {
       // DataTable dt = ds.Tables[tableName];
       DataGridTableStyle DGStyle = new DataGridTableStyle();
       DGStyle.MappingName = dt.TableName;
       DGStyle.AllowSorting = false;
       DGStyle.AlternatingBackColor = Color.Gainsboro;
       DataGridTextBoxColumn textColumn;
       int nbrColumns = dt.Columns.Count;
       DataRow dr;
       ArrayList cWidths = new ArrayList();
       Graphics g = dg.CreateGraphics();
       Font f = dg.Font;
       SizeF sf;
       // get widths for each column header
       foreach (DataColumn c in dt.Columns)
       {
         // "W"is used as padding
         sf = g.MeasureString("W"+ c.Caption, f);
         cWidths.Add(sf.Width);
       }
       // loop through each row comparing against initial column
       // width and then against each new maximum width based upon
       // data contained in each row for that column.
       string strTemp;
       for(int i=0; i < dt.Rows.Count; i++)
       {
        dr = dt.Rows[i];
        for (int j=0; j < nbrColumns; j++)
       {
       if(!Convert.IsDBNull( dr[j]))
       {
        strTemp = dr[j].ToString();
        // careful strings can get
        // very long, limit it to 100 characters
        if(dr[j].GetType() ==
        typeof(System.String) && strTemp.Length > 100)
         sf = g.MeasureString("W"+
         strTemp.Substring(0,100), f);
       else
         sf = g.MeasureString("W"+ strTemp, f);
       if(sf.Width > (float)cWidths[j])
       cWidths[j] = sf.Width;
      }
    }
  }
  // set each column with its determined width
  // set alignment for each column
  // set format for decimal numbers
  float sumWidths = 0.0f;
  foreach (DataColumn c in dt.Columns)
   {
    if(c.DataType == typeof(bool))
    {
       DataGridBoolColumn boolColumn =
       new DataGridBoolColumn();
       boolColumn.MappingName = c.ColumnName;
       boolColumn.HeaderText = ""+ c.Caption;
       boolColumn.Width = 50;
       boolColumn.ReadOnly = false;
       boolColumn.Alignment = HorizontalAlignment.Center ;
       DGStyle.GridColumnStyles.Add(boolColumn);
       sumWidths += boolColumn.Width;
    }
   else
  {
    textColumn= new DataGridTextBoxColumn();
    textColumn.MappingName = c.ColumnName;
    textColumn.HeaderText = ""+ c.Caption;
    textColumn.ReadOnly = false;
    textColumn.NullText = string.empty;
    if(c.DataType == typeof(System.Guid) ||
       c.DataType == typeof(System.String))
    {
       textColumn.Alignment = HorizontalAlignment.Left ;
    }
    else
    if(c.DataType == typeof(System.DateTime))
       textColumn.Alignment =HorizontalAlignment.Center ;
    else
       if(c.DataType == typeof(int))
       {
        textColumn.Alignment =HorizontalAlignment.Center ;
       }
    else
    {
      textColumn.Alignment = HorizontalAlignment.Right ;
      if(c.DataType == typeof(System.Double)
      || c.DataType ==typeof(System.Decimal)
      || c.DataType == typeof(float))
      textColumn.Format = "0.00";
    }
    textColumn.Width = (int)(float)cWidths[c.Ordinal];
    DGStyle.GridColumnStyles.Add(textColumn);
    sumWidths += textColumn.Width;
    if (maxHeight < textColumn.TextBox.Height)
       maxHeight = textColumn.TextBox.Height;
    }
  }
  dg.TableStyles.Add(DGStyle);
  // Adjust width of DataGrid to match calculated widths
  // select maximum width of all tables in the dataset
  // Adjust width to fit inside the parent container’s width
  if (prevSumWidths < sumWidths)
  {
    prevSumWidths = sumWidths;
    prevSumWidths += dg.RowHeaderWidth;
    if (dg.VisibleRowCount < dt.Rows.Count)
       prevSumWidths += 16; // add width of scrollbar
    // check to see if it is greater than the width of its parent
    // e.g. a panel or tabpage control.
    if (prevSumWidths > (float)dg.Parent.Width)
    // allow room for the scroll bar and provide a little padding.
    prevSumWidths = (float)dg.Parent.Width - 16 - 5;
  }
  // Adjust height of DataGrid based upon row visiblity
  if (dg.VisibleRowCount >= dt.Rows.Count)
  {
    calcH = (dg.VisibleRowCount)*maxHeight+2*dg.PreferredRowHeight+16;
  }
  else
  {
   calcH = (dt.Rows.Count)*maxHeight+2*dg.PreferredRowHeight+16;
   // make a correction since all rows will now be visible
   if (calcH < dg.Parent.Height)
     prevSumWidths -= 16;
   }
  if (calcH >=dg.Parent.Height)
  {
    calcH = dg.Parent.Height – dg.Top - 25; // some arbitrary value
  }
 }
 dg.Size = new Size((int)prevSumWidths,calcH);
 }
}
5.3.5 通过 Visual Studio .NET IDE 定义 DataGridTableStyles
在将 DataGrid 控件放置到窗体上的 WinForm UI 编辑器中,可以选择 DataGridTableStyle 的集合属性,这将弹出如图 3 所示的 DataGridTableStyle Collection Editor 对话框。单击 Add 按钮将创建一个新的 DataGridStyle,可以为其命名。在该对话框的成员部分显示了两个样式名称 DGStyleElements 和 DGStyleIsotopes,其中显示了前者的属性值。请注意,DGStyleElements 的 MappingName 是 Elements,并且当 DataGrid MappingName 也为 Elements 时,此样式将用于呈现同名的 DataTable。
单击 GridColumnStyles 集合属性(由红色箭头指示)将启动如图 4 所示的 DataGridColumnStyle Collection Editor 对话框。在成员部分列出了列样式,对应于 Elements 表中的每一列。对话框中显示了 TextBoxColumn 样式,该样式通过 MappingName 属性链接到 Elements 表中的 Atomic Number 列。
图 3 和图 4 中的两个对话框清晰地展示了 DataGrid 在渲染表格时使用的 DataGrid 样式集合的集合模型。

图 3 DataGridTableStyle Collection Editor 对话框

图 4 DataGridColumnStyle Collection Editor 对话框
5.4 导航
5.4.1 CurrencyManager
CurrencyManager 在 DataSet 包含链接表并且需要确保记录正确添加或在它们之间导航时尤其有用。 CurrencyManager 的 Position 属性包含它所绑定表格的当前选定行的零基索引行号。
在 Element -> Isotope 表关系等链接系统中,并且 DataGrid DataMember 属性设置为 Isotope 表时,DataGrid CurrentRowIndex 反映 Isotope 表中当前选定的行,而要了解它所链接的 Element 表中的行索引,只能通过 Element 表的 CurrencyManger 来确定。
CurrencyManager 还可以通过使用 Count 属性结合更改 Position 属性的值来移动到其绑定表格中的不同行;因此,可以创建 MoveNext、MovePrev、MoveFirst 和 MoveLast 导航方法。此外,当位置更改时,可以监视 CurrencyManager 的 CurrentChanged 和 PositionChanged 事件。
例如,可以如下声明带有事件处理的 CurrencyManger 对象:
private CurrencyManger cmElements;
cmElements = (CurrencyManager)this.BindingContext[
  dg.DataSource, "Elements"];
cmElements.CurrentChanged += new System.EventHandler(
  this.cmElements_CurrentChanged);
cmElements.PositionChanged += new System.EventHandler(
  this.cmElements_PositionChanged);
5.4.2 CurrentCell
DataGridCell 类构造函数可用于更改由 dg.DataMember 指定的当前活动的表格中的当前选定单元格和行。它还有两个成员可用于独立地获取或设置当前表格列和行号。例如:
dg.CurrentCell = new DataGridCell(row, column);
row = dg.CurrentCell.RowNumber;
column = dg.CurrentCell.ColumnNumber;
5.4.3 选择行
在协调 DataGrid 与其他 UI 组件时,需要调用 CurrentRowIndex、Select 和 UnSelect 方法。
| 方法 | 操作 | 
| 
 | 设置或获取新当前行并将光标指示器移到该行。参见图 3。 | 
| 
 | Select() 高亮显示 NewRow。参见图 3。 | 
| 
 | UnSelect() 取消高亮显示 previousRow。 | 
5.4.4 展开和折叠链接表格
图 5 至 7 显示了调用 Collapse 和 Expand 方法后表格的不同状态。

图 5 dg.Collapse(-1); // 折叠所有行

图 6 dg.Expand(-1); // 展开所有行

图 7 dg.Expand(1); // 仅展开第 2 行(零基索引)
图 8 说明了使用 DataGrid NavigateTo(arg1, arg1) 方法的结果,其中 arg1 是主表中的整数行索引,arg2 是指定要导航到的表格名称的字符串。在此示例中,Mercury 在 Elements 表中的行索引为 79,同位素包含在“Isotopes”链接表中,通过 Atomic Number 主键-外键关系。

图 8 dg.NavigateTo(79, “Isotopes”);
5.5 将 DataGrid 复制到剪贴板
以下部分包含复制到剪贴板的例程,这些例程访问 DataGrid 中的所有表格或指定表格,然后调用一个例程(TableToString),该例程将表格数据格式化为字符串,然后复制到剪贴板。一旦剪贴板包含此字符串,就可以将其粘贴到 Excel 或任何接受文本数据的应用程序中。例如,在 Excel 或 Winword 中,字符串数据将被解析回表格。
每个方法都检查 DataGrid 的数据源成员,以确定它是包含表格集合的 DataSet,还是仅一个 Table 对象。
5.5.1 将 DataGrid 中选定的表格复制到剪贴板
public void CopyDGtoClipboard(DataGrid dg, string tableName)
{
  if (dg.DataSource != null)
  {
    DataTable dt = null;
    if (dg.DataSource.GetType() == typeof(DataSet))
    {
      DataSet ds = (DataSet)dg.DataSource;
      // need to use tableName when DataSet contains more than
      // one table
      if (ds.Tables.Contains(tableName))
      dt = ds.Tables[tableName];
    }
    else
    if (dg.DataSource.GetType() == typeof(DataTable))
    {
      dt = (DataTable)dg.DataSource;
      if (dt.TableName != tableName)
      {
       dt.Clear();
       dt = null;
    }
  }
  if (dt != null)
  Clipboard.SetDataObject(TableToString (dt), true );
 }
}
5.5.2 将 DataGrid 中所有表格复制到剪贴板
public void CopyDGtoClipboard(DataGrid dg)
{
  if (dg.DataSource != null)
  {
   if (dg.DataSource.GetType() == typeof(DataSet))
   {
    DataSet ds = (DataSet)dg.DataSource;
    if (ds.Tables.Count > 0)
    {
     string strTables = string.Empty;
     foreach (DataTable dt in ds.Tables)
     {
        strTables += TableToString (dt);
        strTables += "\r\n\r\n";
     }
    if (strTables != string.Empty)
     Clipboard.SetDataObject(strTables, true );
    }
  }
   else
    if (dg.DataSource.GetType() == typeof(DataTable))
    {
      DataTable dt = (DataTable)dg.DataSource;
      if (dt != null )
         Clipboard.SetDataObject(TableToString(dt),
         true );
    }
  }  
}
5.5.3 将表格数据格式化为字符串
此方法返回一个包含 DataTable 对象中数据的字符串。第一行包含表格名称,第二行包含每个列名称,用制表符分隔,其余行(每行对应表格中的一行)包含相应的列数据,用制表符分隔。
private string TableToString(DataTable dt)
{
  string strData = dt.TableName + "\r\n";
  string sep = string.Empty;
  if (dt.Rows.Count > 0)
  {
    foreach (DataColumn c in dt.Columns)
    {
      if(c.DataType != typeof(System.Guid) &&
      c.DataType != typeof(System.Byte[]))
      {
        strData += sep + c.ColumnName;
        sep = "\t";
      }
    }
    strData += "\r\n";
    foreach(DataRow r in dt.Rows)
    {
      sep = string.Empty;
      foreach(DataColumn c in dt.Columns)
      {
        if(c.DataType != typeof(System.Guid) &&
        c.DataType != typeof(System.Byte[]))
        {
          if(!Convert.IsDBNull(r[c.ColumnName]))
          strData += sep +
          r[c.ColumnName].ToString();
        else
          strData += sep + "";
          sep = "\t";
        }
      }
      strData += "\r\n";
    }
  }
  else
    strData += "\r\n---> Table was empty!";
return strData;
}
5.6 导出到制表符分隔的文本文件
以下两个方法 tabTextFile 和 SaveDataGridData 以及在另一个部分定义的 TableToString 方法可用于将 DataGrid 中包含的所有数据保存到通过“另存为”对话框指定的制表符分隔的文本文件中。
public void tabTextFile(DataGrid dg)
{
  OpenFileDialog openFileDialog1 = new OpenFileDialog();
  openFileDialog1.InitialDirectory = "c:\\";
  openFileDialog1.Filter =
    "Text Files (*.txt)|*.txt| All files (*.*)|*.*";
  openFileDialog1.RestoreDirectory = true ;
  openFileDialog1.Title="Export DataGrid to Text File";
  System.Windows.Forms.DialogResult res = openFileDialog1.ShowDialog();
  if(res == DialogResult.OK)
  {
    DataSet ds = new DataSet();
    DataTable dtSource = null;
    DataTable dt = new DataTable();
    DataRow dr;
    if(dg.DataSource != null)
    {
    if (dg.DataSource.GetType() == typeof(DataSet))
    {
      DataSet dsSource = (DataSet)dg.DataSource;
      // assume DataSet contains desired table at index 0
      if (dsSource.Tables.Count > 0)
      {
        string strTables = string.Empty;
        foreach (DataTable dt in dsSource.Tables)
        {
          strTables += TableToString (dt);
          strTables += "\r\n\r\n";
        }
      if (strTables != string.Empty)
      SaveDataGridData (strTables,openFileDialog1.FileName);
      }
    }
    else
    {
        if (dg.DataSource.GetType() == typeof(DataTable))
        dtSource = (DataTable)dg.DataSource;
        if (dtSource != null )
        SaveDataGridData (TableToString(dtSource),
        openFileDialog1.FileName);
    }
  }
 }
}
private void SaveDataGridData(string strData, string strFileName)
{
  FileStream fs;
  TextWriter tw = null;
  try
  {
    if (File.Exists(strFileName))
    {
      fs = new FileStream(strFileName, FileMode.Open);
    }
  else
  {
    fs= new FileStream(strFileName, FileMode.Create);
  }
  tw = new StreamWriter(fs);
  tw.Write(strData);
}
finally
{
  if (tw != null)
  {
    tw.Flush();
    tw.Close();
    MessageBox.Show("DataGrid Data has been saved to: "+ strFileName
    ,"Save DataGrid Data As", System.Windows.Forms.MessageBoxButtons.OK
    , System.Windows.Forms.MessageBoxIcon.Information);
  }
  }
}
5.7 克隆 DataGrid 中包含的表格
此示例展示了如何克隆 DataGrid 中的一个表格,然后用原始表格的内容填充克隆的表格,并将其返回到一个新的 DataSet 中。请注意,它会检查数据源是 DataSet 还是 Table。
private DataSet CloneDataTable(DataGrid dgTable)
{
 DataSet ds = new DataSet();
 DataTable dtSource = null;
 DataTable dt = new DataTable();
 DataRow dr;
 if(dgTable.DataSource != null)
  {
  if (dgTable.DataSource.GetType() == typeof(DataSet))
  {
   DataSet dsSource = (DataSet)dgTable.DataSource;
   // assume DataSet contains desired table at index 0
   dtSource = dsSource.Tables[0];
  }
  else
   if (dgTable.DataSource.GetType() == typeof(DataTable))
   dtSource = (DataTable)dgTable.DataSource;
   if (dtSource != null)
   {
    dt = dtSource.Clone();
    // dgConversionTable.CaptionText;
    dt.TableName = dtSource.TableName;
    dt.BeginLoadData();
    foreach (DataRow drSource in dtSource.Rows)
    {
     dr = dt.NewRow();
     foreach (DataColumn dc in dtSource.Columns)
     {
      dr[dc.ColumnName] = drSource[dc.ColumnName];
     }
     dt.Rows.Add(dr);
    }
   dt.AcceptChanges();
   dt.EndLoadData();
   ds.Tables.Add(dt);
   }
  }
return ds;
}
5.8 WinForms 和 WebForms 的比较
本节将介绍在 WinForms 和 WebForms 中使用 DataGrid 的区别。将使用一个 DataGrid 标题为 C-Terminal Groups 的表格进行比较。
5.8.1 WinForms
在图 9 和图 10 中可以看到,C-Terminal 表格包含文本、小数和整数,这些都已使用类似于 formatDG(DataGrid dg) 的过程进行了格式化,该过程使用了 DataGridTableStyles。此 DataGrid 的格式化如下:
整数在列内居中对齐。
- 文本字符串左对齐。在 Formula 列中,该列由元素分解列中的值生成,电子公式字符串在每个元素之间有一个空格,而不是使用数字下标。
- 小数有 4 位小数,右对齐。
- ID 列是表格的主键。
- 注意:dc.Caption 用作 DataGrid 中的列标题,在本例中 dc.Caption 等于 dc.ColumnName。

图 9 显示 C-Terminal Groups 的 WinForm DataGrid
在图 10 的第 3 行,Name 列中的 Hydrazine 正在被编辑。也就是说,编辑直接在每个单元格中进行,只需将光标移到单元格并单击鼠标左按钮即可设置焦点。表格左边距处的深蓝色箭头会移动到正在编辑的行。如果表格中的行数超过了 DataGrid 视图可显示的行数,则使用滚动条将其他行显示出来。

图 10 WinForm DataGrid 在单元格中直接编辑
要添加更多行,过程是将新值输入到以星号 (*) 标记的行中的每个单元格。Table 类管理所有新的单元格和行条目。可用的方法之一将返回一个值,指示表已发生更改,然后可以使用其他方法来确定更改的类型并创建一个仅包含更改的表。还可以实现事件处理程序来响应正在进行的更改。
5.8.2 WebForms
图 11-13 演示了与 Web Form 中相同的表格,但存在一些差异。最主要的是 DataGrid 实际上是一个HTML 表。通过查看图 11 和图 12 中的 Formula 列即可清楚地看到这一点。数字下标值使用 HTML <sub>…</sub> 标签进行格式化,数字从元素分解列 {C, H, N, O, S, P, Cl, Br, F, I} 中获取。也就是说,所有单元格条目都可以使用 HTML 中所有可用的样式元素进行修改。在图 11 下方列出了 Microsoft Visual Studio .NET IDE 为 DataGrid 生成的 HTML 代码。它包含用户定义和默认的属性值。请注意 HTML 代码中为标题以及用于修改行值的额外 Edit 和 Delete 列定义的蓝色背景和白色字母。此外,由于 HTML 表没有滚动条,因此可以使用分页机制(Prev, Next)并使用 PageSize 属性定义每页的行数。

图 11 WebForm DataGrid 显示为 HTML 表
Visual Studio .Net 生成的 DataGrid 的 HTML 代码
<asp:datagrid id="dgCTerminalGroups"style="Z-INDEX: 103; LEFT: 72px;
POSITION: absolute; TOP: 464px"runat="server"
Font-Size="Small"Font-Names="Arial"HorizontalAlign="Left"
Width="656px"AllowPaging="True"PageSize="10"CellPadding="3">
<AlternatingItemStyle BackColor="Gainsboro"></AlternatingItemStyle>
<ItemStyle Font-Size="Smaller"Font-Names="Arial"HorizontalAlign="Center">
</ItemStyle>
<HeaderStyle Font-Size="Smaller"HorizontalAlign="Center"
ForeColor="White"BackColor="Navy">
</HeaderStyle>
<Columns>
<asp:EditCommandColumn ButtonType="LinkButton"UpdateText="Update"
HeaderText="Edit"CancelText="Cancel"EditText="Edit">
</asp:EditCommandColumn>
<asp:ButtonColumn Text="Delete"HeaderText="Delete"
CommandName="Delete"></asp:ButtonColumn>
</Columns>
<PagerStyle NextPageText="Next"Font-Size="Smaller"PrevPageText="Prev"
HorizontalAlign="Center"ForeColor="White"BackColor="Navy"></PagerStyle>
</asp:datagrid>
5.8.2.1 编辑行
当单击行的 Edit 链接按钮时,该行中的每个单元格都会转换为一个 TextBox 控件,其 TextBox.text 属性初始化为在表格单元格中获取的值。这在图 12 的第 3 行中有所说明,其中每个 TextBox 控件都按照图 13 后面 dg_PreRender(object sender, System.EventArgs e, string strTableName) 过程代码部分所示进行格式化,其中 EditItemIndex 成员大于 -1。请注意,ID 和 Formula 列的 TextBox 控件是只读的,显示为深灰色文本,边框宽度为零。
同样,当单击行的 Edit 链接按钮时,会调用 EditCommand 处理程序。此处理程序所做的只是设置 EditItemIndex 成员,然后调用 DataBind() 方法。
private void dgCTerminalGroups_EditCommand(object source,
System.Web.UI.WebControls.DataGridCommandEventArgs e)
{
  dgNTerminalGroups.EditItemIndex = e.Item.ItemIndex;
  dgNTerminalGroups.DataBind();
}
5.8.2.2 取消编辑行
通过单击 Cancel 链接按钮可以取消编辑操作,其 CancelCommand 事件处理程序将 EditItemIndex 设置为 -1,然后调用 DataBind() 方法。
private void dgNTerminalGroups_CancelCommand(object source,
System.Web.UI.WebControls.DataGridCommandEventArgs e)
{
  dgNTerminalGroups.EditItemIndex = -1;
  dgNTerminalGroups.DataBind();
}

图 12 编辑 C-Terminal Group 行 ID 等于 3
5.8.2.3 输入新行
在 WinForm DataGrid 中,可以在标有星号的行中输入新行,但 WinForms 中不存在这种机制。此实现创建了一个特殊的最后一行,其中包含一个关键词“[New CTerm-Group]”,让用户知道此行类似于 WinForm 中带星号的行,并且 Edit 的行为与其他行相同。此空行在从数据库查询填充对象后添加到 DataTable 中,如果该行没有任何更改,更新处理程序将在更新数据库之前删除此行,否则将将其添加到数据库表中。

图 13 添加新的 C-Terminal Group 行
5.8.2.4 更新行
此 UpdateCommand 事件处理程序执行以下操作:
如果被更新的不是最后一行([New Cterm-Group]),则将其从 DataSet Table 中删除。
- 如果已被删除,则调用 AcceptChanges(),这样就不会尝试从数据库表中删除它,因为它实际上并不存在于数据库中。
- 接下来,从每个 TextBox 控件获取值,并更新 DataSet Table 中相应的单元格。请注意,TextBox 控件在 Cells 控件集合中的索引等于零。
- 使用行更改或新行添加更新 CTerminal 数据库表。
- 将 EditItemIndex 重置为 -1,将 DataSource 设置为已修改的 DataSet,然后调用 DataBind()。
注意:如果更新看似无效,即使所有事件处理程序都已调用,请检查您是否在 Page_OnLoad() 事件处理程序中使用了 IsPostBack 方法。例如:
if (!Page.IsPostBack)
  dgCTerminalGroups.DataBind();
private void dgCTerminalGroups_UpdateCommand(object source,
System.Web.UI.WebControls.DataGridCommandEventArgs e)
{
  DataRow dr;
  DataSet ds;
  ds = (DataSet)dgCTerminalGroups.DataSource;
  DataTable dt = ds.Tables["CTerminal"];
  // this code segment simply deletes the blank row at the
  // end of the table if it is not the row being modified/updated.
  if (e.Item.DataSetIndex < dt.Rows.Count-1)
  {
   dt.Rows[dt.Rows.Count-1].Delete();
   dt.AcceptChanges();
  }
  dr = dt.Rows[e.Item.DataSetIndex];
  for(int i=0; i< dt.Columns.Count; i++)
  {
   // The TextBox is the 0th element of the Controls collection.
   // the edit, delete columns are at cells i=0
   // and i=1 respectively, skip those
   TextBox tbox = (TextBox)e.Item.Cells[i+2].Controls[0];
  if (!tbox.ReadOnly) // skip readonly cells;
  //set in PreRender() event handler.
  dr[dt.Columns[i].Caption] = tbox.Text;
   }
  if (ds.HasChanges())
  {
   DataSet dsChanges = ds.GetChanges();
   // call an update database function (code for method not shown)
   // merge the changes
   // with the DataSet contained by the DataGrid
   UpdateTerminalTable(dsChanges, "CTerminal") != null)
   ds.Merge(dsChanges);
  }
 // Switch out of edit mode.
 dgCTerminalGroups.EditItemIndex = -1;
 dgCTerminalGroups.DataSource=ds;
 dgCTerminalGroups.DataBind();
}
5.8.2.5 设置单元格样式值
当 DataGrid 表中的单个单元格需要超出全局网格和列设置进行格式化时,可以定义一个用户定义的 DataGrid PreRender 事件处理程序。以下代码提供了一个用于设置单个单元格样式值的处理程序示例。通过比较图 13 和图 14,可以看到基于 DataGrid 中包含的数据类型进行的格式化的结果。格式化包括:
- 设置所有单元格的水平对齐属性。
- 将 [New Term Group] 行设置为斜体,并将其 ForeColor和BackColor属性分别设置为Color.DarkGray和Color.White。
- 将小数的 ForeColor和BackColor分别设置为Color.DarkBlue和Color.LightGray。
这是一种强大的技术,例如可用于通过更改已更改单元格的字体和颜色来反映单元格编辑更改。
此示例在代码段中进一步说明了使用 HTML CssStyles 和控件属性进行格式化,该代码段专门处理当前正在编辑的行,该行包含在 EditItemIndex 成员中。
| 注意 | 
| 在  for (int j=0; j < dg.Items.Count; j++)Items 的最大数量将小于或等于 dg.PageSize。要使用 j 作为表格行的索引,需要将 j 加上 dg.PageSize*dg.CurrentPageIndex。例如,表格 dt 中的 Item j 的列 i 访问方式为: 
 其中 dg. | 
private void dg_PreRender(object sender,
  System.EventArgs e, string strTableName)
{
  DataGrid dg = (DataGrid)sender;
  DataTable dt = null;
  if (dg.DataSource.GetType() == typeof(DataSet))
  {
   DataSet ds = (DataSet)dg.DataSource;
   dt = ds.Tables[strTableName];
  }
  else
  if (dg.DataSource.GetType() == typeof(DataTable))
   dt = (DataTable)dg.DataSource;
   if (dt != null)
  {
  for (int j=0; j <dg.Items.Count; j++)
   {
    if (j == (dg.Items.Count-1) &&
     ((string)dt.Rows[j+dg.PageSize*dg.CurrentPageIndex][
     "Name"]).IndexOf("[New") > -1)
    bNewGroupRow = true;
  else
    bNewGroupRow = false;
  for(int i=0; i<(dt.Columns.Count); i++)
  {
   // accentuate [New Term Group] row with font
   // and color modifications
   if(bNewGroupRow && dg.EditItemIndex == -1)
   {
    dg.Items[j].Cells[i+2].ForeColor = Color.DarkGray;
    dg.Items[j].Cells[i+2].BackColor = Color.White;
    FontInfo fi = dg.Items[j].Cells[i+2].Font;
    fi.Italic = true;
   }
   if(dt.Columns[i].DataType == typeof(System.Guid) ||
   dt.Columns[i].DataType == typeof(System.String))
   {
    dg.Items[j].Cells[i+2].HorizontalAlign =
      HorizontalAlign.Left ;
    dg.Items[j].Cells[i+2].Wrap = true ;
   }
   else
   if(dt.Columns[i].DataType == typeof(System.DateTime))
    dg.Items[j].Cells[i+2].HorizontalAlign =
    HorizontalAlign.Center ;
  else
   if(dt.Columns[i].DataType == typeof(int))
   {
    dg.Items[j].Cells[i+2].HorizontalAlign =
     HorizontalAlign.Center ;
   }
  else
 {
  if(dt.Columns[i].DataType == typeof(System.Double)
  || dt.Columns[i].DataType ==typeof(System.Decimal)
  || dt.Columns[i].DataType == typeof(float))
  {
   dg.Items[j].Cells[i+2].HorizontalAlign =
   HorizontalAlign.Right;
   // set the text color to DarkBlue and background color
    // LightGray
   dg.Items[j].Cells[i+2].ForeColor = Color.DarkBlue;
   dg.Items[j].Cells[i+2].BackColor = Color.LightGray;
   }
  }
 }
}
// check to see if a row is being edited, if it is
// then the EditItemIndex member is non-negative
// Each cell in the row now has an edit control,
// this code gets the EditControl object for
// the cell and then sets its cell specific attributes.
// This example uses the Caption text as
// a filter to select the cell type and then sets its
// individual style properties. Two cells
// ID and Forumla are readonly and their text color is
// set to DarkGray. The example also uses
// CssStyle to set global style attributes for each cell.
if (dg.EditItemIndex > -1)
{
  string strCaption;
  for(int i=2; i<=(dt.Columns.Count+1); i++)
  {
   TextBox tbox =
   (TextBox)dg.Items[dg.EditItemIndex].Cells[i].Controls[0];
   tbox.BackColor = Color.White;
   tbox.ForeColor = Color.DarkBlue;
   System.Web.UI.AttributeCollection
     tboxAttributes = tbox.Attributes;
   tboxAttributes.CssStyle.Add("font-weight", "bold");
   tboxAttributes.CssStyle.Add("text-align", "center");
   strCaption = dt.Columns[i-2].Caption.Trim();
   if (strCaption == "ID")
   {
      tbox.ReadOnly = true;
      tbox.ForeColor = Color.DarkGray;
      tbox.BorderWidth = 0;
      tbox.Width = 30;
   }
 else
  if (
   strCaption == "C"|| strCaption == "H"||
   strCaption == "N"|| strCaption == "O"||
   strCaption == "S"|| strCaption == "P"||
   strCaption == "Br"|| strCaption == "Cl"||
   strCaption == "F"|| strCaption == "I")
   {
    tbox.Width = 30;
   }
 else
   if (strCaption == "Formula")
   {
    tbox.Width = 150;
    tbox.ReadOnly = true;
    tbox.ForeColor = Color.DarkGray;
    tbox.BorderWidth = 0;
    tbox.Text = string.Empty;
   }
  else
   if (strCaption == "AvgMass"|| strCaption == "MonoMass")
   {
    tbox.Width = 75;
   }
 else
 if (strCaption == "Name")
 {
   tbox.Width = 150;
 }
 else
   tbox.Width = 75;
   }
  }
 }
}

图 14 使用 PreRender 方法格式化 DataGrid
6 参考资料
- 1. Microsoft Visual Studio .NET IDE 帮助系统
- Microsoft Developer Network 在线帮助系统
- Charles Petzold 著,《Programming Microsoft Windows with C#》,Microsoft Press,2002。
- Scott Mitchell 著,《ASP.NET Data Web Controls》,SAMS,2003。
