65.9K
CodeProject 正在变化。 阅读更多。
Home

动态生成的 XML 数据编辑器

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.88/5 (72投票s)

2003年2月11日

CPOL

8分钟阅读

viewsIcon

482797

downloadIcon

9181

使用 XML 架构定义 (XSD) 文档,此实用工具可以动态生成一个数据录入表单,用于创建和编辑 XML 数据。

目录

引言

以前的实用工具 [^] 帮助您创建 XSD 文件。完成此操作后,我寻找了一个允许我管理关联 XML 文档数据的实用工具。具体来说,我想要两个功能:

  1. 一个通用的解决方案,允许我根据任何 XSD 架构创建 XML 数据
  2. 一个能够根据架构信息定制输入控件的东西

我没有找到任何接近满足这两个标准的东西。我找到的所有编辑器都侧重于编辑整个 XML 文档(包括标签),而不仅仅是数据。对我来说,标签是快速组织数据集的一个主要障碍。此外,.NET 解决方案(创建 CS 文件)是一种复杂、多步骤且非通用的方法。

因此,我决定编写自己的 XML 数据编辑器,并在过程中学习更多关于 XSD、XML 和 XPath 的知识。利用 XmlDataDocumentXmlSchemaDataTable 类,我创建了一个通用的编辑器,该编辑器根据架构定义和表关系动态创建用于数据录入的专用控件。

结果的示例是:(图片太长,抱歉!)

Screenshot - XmlDataEditor.jpg

...这是从架构派生的(我的 XML 架构编辑器的截图)

Screenshot - schema.jpg

示例文件

下载包含一个示例 XSD 和 XML 文件。首先使用“文件/加载 XSD”菜单项加载 sampleSchema2.XSD 文件。然后使用“文件/加载 XML”菜单项加载 sale5.xml 文件。

一些背景基础:导入架构和 XML 文档

鉴于

private XmlSchema schema=null;
private XmlDataDocument doc=null;

XSD 文件已加载

// get a stream reader for the XSD file
StreamReader tr=new StreamReader(dlgOpen.FileName);
// read the file into the XmlSchema object
schema=XmlSchema.Read(tr, 
  new ValidationEventHandler(SchemaValidationHandler));
tr.Close();
// report any problems with the schema by compiling it
CompileSchema();
// create the document
doc=new XmlDataDocument();
// open the schema again
tr=new StreamReader(dlgOpen.FileName);
// read it into the DataSet member
doc.DataSet.ReadXmlSchema(tr);

XML 文件已加载

XmlTextReader tr=new XmlTextReader(dlgOpen.FileName);
doc.Load(tr);
tr.Close();

动态 GUI 生成

一旦架构被加载到 DataSet 中,.NET 框架会自动创建相关的 DataTable 对象及其关系。此信息用于生成 GUI 的框架,该框架是一系列递归的 GroupBox 对象,封装了 Control 对象,所有这些都包含在一个面板中。面板为我们提供了自动滚动功能,从而减轻了我们自己管理滚动功能的负担。

获取根表

GUI 的生成始于检查所有根(非父)表,并确定定义它们的 XmlSchemaComplexType。这里的假设是,任何根表将在架构中表示为一个引用全局复杂类型的元素。

private void ConstructGUI(DataSet dataSet)
{
  Point pos=new Point(10, 10);
  // get all tables in the dataset
  foreach (DataTable dt in dataSet.Tables)
  {
    // but we're only interested in the toplevel tables
    if (dt.ParentRelations.Count==0)
    {
      /*
       * Rule 1:
       * A top level table will be a top level element in the schema that
       * is of a complex type.  The element name will be the table name.
       * What we want to identify is the complex 
       * type that the table references,
       * so that we can determine the data types of the columns.
       * 
       * Any other rules???
       */
       XmlSchemaElement el=GetGlobalElement(dt.TableName);
       XmlSchemaComplexType ct=GetGlobalComplexType(el.SchemaTypeName.Name);
       Point p2=ConstructGUI(pos.X, pos.Y, dt, pnlDynForm, ct);
       pos.Y+=p2.Y;
       }
    }
}

有两个非常简单的辅助函数用于获取适当的对象,此处显示是因为获取它们需要不同的机制。

GetGlobalElement()

private XmlSchemaElement GetGlobalElement(string name)
{
  XmlQualifiedName qname=new XmlQualifiedName(name, schema.TargetNamespace);
  XmlSchemaObject obj=schema.Elements[qname];
  return obj;
}

GetGlobalComplexType()

private XmlSchemaComplexType GetGlobalComplexType(string name)
{
  for (int i=0; i < schema.Items.Count; i++)
  {
    XmlSchemaComplexType obj=schema.Items[i] as XmlSchemaComplexType;
    if (obj != null)
    {
      if (obj.Name==name)
      {
        return obj;
      }
    }
  }
  return null;
}

创建 GroupBox:记录集合

GroupBox 以一种直接的方式创建,并附带一个名为 tableInfo 的辅助哈希表。最初,组框没有设置大小,因为大小是在放置完所有控件后确定的。此外,还会创建一个导航栏并将其与组框关联,提供第一条、上一条、下一条、最后一条、新建和删除记录按钮,以及 n of m 记录信息文本。

private Point ConstructGUI(int absx, int absy, DataTable dt, 
  Control gbParent, XmlSchemaComplexType ct)
{
  GroupBox gb1=new GroupBox();
  gb1.Font=new Font(gb1.Font, FontStyle.Bold);
  gb1.Text=dt.TableName;
  gb1.Location=new Point(absx, absy);
  gb1.Parent=gbParent;
  gb1.Visible=true;

  tableInfo[dt]=new TableInfo();
  CreateRecordNavigationBar(10, 15, gb1, dt);
  ...

一旦放置了组框,就会对 DataTable "dt" 中的所有列执行以下步骤:

// For each column in the table...
foreach (DataColumn col in dt.Columns)
  {...

1. 放置列名和适合非隐藏列的控件

// if it's not an internal ID...
if (col.ColumnMapping != MappingType.Hidden)
{
  // display its name
  CreateLabel(relx, rely, col.ColumnName, gb1);
  TypeInfo ti=GetTypeInfo(ct, col.ColumnName);
  if (ti != null)
  {
    Control editCtrl=CreateEditControl(relx+120, rely, ti, gb1, dt, col);
  }
}

2. 放置列名和只读文本控件以用于隐藏字段。这可以省略,但我将其保留下来,以便我能够检查 DataSet 的隐藏功能。与子表有关系的列显示为蓝色,与父表有关系的列显示为红色(此处未显示设置颜色)。由于架构的性质,这些关系总是 1:1(据我所见!)。

if (col.ColumnMapping==MappingType.Hidden)
{
  Label lbl=CreateLabel(relx, rely, col.ColumnName, gb1);
  Control editCtrl=CreateEditControl(relx+120, 
    rely, new TypeInfo("string"), gb1, dt, col);
  editCtrl.Size=new Size(50, 20);
  ((TextBox)editCtrl).ReadOnly=true;

3. 如果发现任何子关系,将递归调用相同的函数。

// Get child relationships, which are displayed as indented groupboxes
foreach (DataRelation childRelation in dt.ChildRelations)
{
  DataTable dt2=childRelation.ChildTable;
  XmlSchemaElement el2=GetLocalElement(ct, dt2.TableName);
  XmlSchemaComplexType ct2=GetGlobalComplexType(el2.SchemaTypeName.Name);
  if (ct2==null)
  {
    ct2=GetLocalComplexType(el2);
  }
  Point p=ConstructGUI(relx+20, rely+20, dt2, gb1, ct2);

还有一些关于缩进和计算外部组框大小的简单计算,我将不详细讨论。

控件创建

组框内的控件是根据从架构收集的信息中的一些简化规则创建的。这个区域可以大大扩展,因为我不想实现所有基本类型。规则如下:

  1. 如果元素包含枚举,则使用枚举数据创建一个 ComboBox
  2. 如果元素包含 minInclusivemaxInclusive 分面,则创建一个 NumericUpDown 控件。
  3. 如果元素是布尔类型,则创建一个 CheckBox 控件。
  4. 如果元素是十进制或正整数类型,则创建一个右对齐的 TextBox 控件。
  5. 其他所有都是 TextBox 控件。

目前,我忽略了长度和模式等其他分面。

记录导航

记录导航需要我们手动选择由父记录 ID 索引的子记录。

基础知识

这些行存储在 tableInfo 中作为数组,以便于查找。有两个辅助函数被广泛使用。第一个函数用于调整指定导航器的 TextBox 的“记录 m of n”显示。如果表是根表,这里有一个测试来设置位置跟踪器。这可能不是这段代码的最佳位置!

private void UpdateRecordCountInfo(DataTable dt)
{
  // update the text box to reflect the record #
  TableInfo ti=tableInfo[dt] as TableInfo;
  if (dt.ParentRelations.Count==0)
  {
    ti.pos=BindingContext[dt].Position+1;
  }
  ti.tb.Text="   record "+ti.pos.ToString()+" of "+ti.rows.ToString();
}

GetMatchingRows 辅助函数在确定与父 ID 匹配的子行方面非常有用。给定子表,代码获取父表和建立关系的列。可以(而且我认为正确地)假定,总是只有一个列定义关系。由此,BindingContext 用于定位当前的父行。给定父行,我们可以获取 ID 的值并对与关系 ID 匹配的子行执行“select”查询。

private int GetMatchingRows(DataTable dt)
{
  TableInfo ti=tableInfo[dt] as TableInfo;
  // get parent relationship
  DataRelation parentRelation=dt.ParentRelations[0];
  // get the parent table
  DataTable dt2=parentRelation.ParentTable;
  // get the parent column (1:1 relationship always)
  DataColumn dcParent=parentRelation.ParentColumns[0];
  // get the current record # of the parent
  int n=BindingContext[dt2].Position;
  if (n != -1)
  {
    // get the ID
    string val=dt2.Rows[n][dcParent].ToString();
    // search the child for all records where child.parentID=parent.ID
    string expr=dcParent.ColumnName+"="+val;
    // save the rows, as we'll use them later on when navigating the child
    ti.dataRows=dt.Select(expr);
  }
  // return the length
  return ti.dataRows.Length;
}

移动

每个处理程序 - 第一条、上一条、下一条和最后一条 - 的形式基本相同。我将演示“下一条”处理程序。任何给定导航栏中的每个按钮都用它操作的 DataTable 进行“标记”。利用此信息,可以访问 tableInfo 对象以更新内部记录位置。请注意,对于子记录,此位置是相对于由父 ID 选择的行,而不是相对于子表中的所有记录。

private void NextRecord(object sender, EventArgs e)
{
  Button btn=sender as Button;
  DataTable dt=btn.Tag as DataTable;
  TableInfo ti=tableInfo[dt] as TableInfo;
  if (ti.pos < ti.rows)
  {
    ((TableInfo)tableInfo[dt]).pos++;
    NextRecord(dt, ti);
    UpdateRecordCountInfo(dt);
  }
}

出于这个原因,需要进行一些操作才能获取所需的记录。如果表是根表,则我们的内部位置与 DataTable 中的行之间存在 1:1 的对应关系。但是,如果它是子表,那么我们必须在 DataTable 中找到与我们在**已选择**行中的行相匹配的行。

private void NextRecord(DataTable dt, TableInfo ti)
{
    if (dt.ParentRelations.Count==0)
    {
        BindingContext[dt].Position++;
    }
    else
    {
        // get the next row that matches the parent ID
        SetPositionToRow(dt, ti.dataRows[ti.pos-1]);

    }
    ResetAllChildren(dt);
}

这是通过相当粗暴的方式完成的,可以优化为从当前位置向前或向后查找。好吧,下一个版本……

private void SetPositionToRow(DataTable dt, DataRow row)
{
  for (int i=0; i < dt.Rows.Count; i++)
  {
  if (dt.Rows[i]==row)
    {
      BindingContext[dt].Position=i;
      break;
    }
  }
}

更新子记录

现在,当我们切换到父表中的另一条记录时,所有子记录都需要更新以显示与父记录相关的行集。这是通过递归完成的。获取所有子记录的匹配行,并将子记录设置为第一条记录(如果存在)。然后对其子记录执行相同的操作。

private void ResetAllChildren(DataTable dt)
{
  // update all children of the table to match our new ID
  foreach (DataRelation dr in dt.ChildRelations)
  {
    DataTable dtChild=dr.ChildTable;
    ResetChildRecords(dtChild);
  }
}

private void ResetChildRecords(DataTable dt)
{
  int n=GetMatchingRows(dt);
  TableInfo ti=tableInfo[dt] as TableInfo;
  ti.pos=1;
  ti.rows=n;
  UpdateRecordCountInfo(dt);
  if (n != 0)
  {
    SetPositionToRow(dt, ti.dataRows[0]);
  }
  foreach (DataRelation dr in dt.ChildRelations)
  {
    DataTable dtChild=dr.ChildTable;
    ResetChildRecords(dtChild);
  }
}

添加记录

添加记录相当麻烦。记录被添加到集合的末尾,这有点帮助。任何与父相关的字段都必须设置为父 ID,并且所有子关系都必须创建新记录……当然是递归地。

private void NewRecord(object sender, EventArgs e)
{
  Button btn=sender as Button;
  DataTable dt=btn.Tag as DataTable;
  dt.Rows.Add(dt.NewRow());
  int newRow=dt.Rows.Count-1;
  BindingContext[dt].Position=newRow;

  TableInfo ti=tableInfo[dt] as TableInfo;

  // Set the child relationship ID's to the parent!
  // There will be only one parent relationship except
  // for the root table.
  if (dt.ParentRelations.Count != 0)
  {
    DataRelation parentRelation=dt.ParentRelations[0];
    DataTable dt2=parentRelation.ParentTable;
    int n=BindingContext[dt2].Position;

    // this is always a 1:1 relationship
    DataColumn dcParent=parentRelation.ParentColumns[0];
    DataColumn dcChild=parentRelation.ChildColumns[0];
    string val=dt2.Rows[n][dcParent].ToString();
    dt.Rows[newRow][dcChild]=val;

    n=GetMatchingRows(dt);
    ti.pos=n;
    ti.rows=n;
  }
  else
  {
    ti.pos=newRow+1;
    ti.rows=newRow+1;
  }

  UpdateRecordCountInfo(dt);

  // for each child, also create a new row in the child's table
  foreach (DataRelation childRelation in dt.ChildRelations)
  {
    DataTable dtChild=childRelation.ChildTable;
    NewRecord(dt, dtChild, childRelation);
  }
}

...以及针对每个子表...

private void NewRecord(DataTable parent, DataTable child, DataRelation dr)
{
  // add the child record
  child.Rows.Add(child.NewRow());

  // get the last row of the parent (this is the new row)
  // and the new row in the child (also the last row)
  int newParentRow=parent.Rows.Count-1;
  int newChildRow=child.Rows.Count-1;

  // go to this record
  BindingContext[child].Position=newChildRow;

  // get the parent and child columns
  // copy the parent ID (auto sequencing) to the child to establish
  // the relationship.  This is always a 1:1 relationship
  DataColumn dcParent=dr.ParentColumns[0];
  DataColumn dcChild=dr.ChildColumns[0];
  string val=parent.Rows[newParentRow][dcParent].ToString();
  child.Rows[newChildRow][dcChild]=val;

  ((TableInfo)tableInfo[child]).pos=1;
  ((TableInfo)tableInfo[child]).rows=1;
  UpdateRecordCountInfo(child);

  // recurse into children of this child
  foreach (DataRelation childRelation in child.ChildRelations)
  {
    DataTable dt2=childRelation.ChildTable;
    NewRecord(child, dt2, childRelation);
  }
}

删除记录

有趣的是,删除记录非常简单。这要归功于级联删除,DataSet 会自动启用该功能。

private void DeleteRecord(object sender, EventArgs e)
{
  Button btn=sender as Button;
  DataTable dt=btn.Tag as DataTable;
  int n=BindingContext[dt].Position;
  dt.Rows.RemoveAt(n);
  ...

...然后是一堆代码来显示有效的记录并重置子记录。

数据绑定

数据绑定使将控件与 DataTable 中的列关联变得异常简单,只需一行代码即可完成。

ctrl.DataBindings.Add("Text", dt, dc.ColumnName);

这会将控件的“Text”字段绑定到指定的表和列。反射不是很棒吗?BindingContext 也是一个救星。它允许我们指定用于绑定控件的每个 DataTable 中的行。太棒了!例如,给定表,我们可以用一行代码指定绑定行。

BindingContext[dt].Position=dt.Rows.Count;

XPath 查询

Screenshot - XPath.jpg

我实现了 XPath 功能,以便能够尝试如何从 XML 数据中提取信息。我来自数据库背景,我想要一些东西可以展平层次结构,这样我就可以一次看到所有数据,而无需通过层级进行鼠标点击等操作。使用 XmlDataDocumentSelectNodes 方法

XmlNodeList nodeList=null;
try
{
  nodeList=doc.SelectNodes(edXPath.Text);
  DlgXPathResult dlg=new DlgXPathResult(nodeList);
  dlg.ShowDialog(this);
}

我提取一个节点列表,如果成功,则将其传递给对话框。数据在 ListView 控件中显示,其标题从 XmlNode 中提取。显示节点数据有三个基本步骤:

  1. 获取标题。
  2. 用数据填充行。
  3. 清理空列。

获取头部信息

标题信息从节点列表的属性和元素中提取。此函数对元素进行递归操作,因此可能会生成不必要的列。此外,还会忽略重复的元素名称,如果您的架构在两个不同的区域使用相同的元素名称,而您又恰好查询这些元素,则可能导致覆盖问题。

private void ProcessChildHeaders(XmlNode node, XmlNode child)
{
  while (child != null)
  {
    // process attributes
    if (child.Attributes != null)
   {
      foreach(XmlAttribute attr in child.Attributes)
      {
        if (!cols.Contains(attr.Name))
        {
        cols.Add(attr.Name, colIdx);
        colHasData[colIdx]=false;
        lvResults.Columns.Add(attr.Name, -2, HorizontalAlignment.Left);
        ++colIdx;
        }
      }
    }
    // if this child is an element, get its children
    if (child.FirstChild is XmlElement)
    {
      ProcessChildHeaders(node, child.FirstChild);
    }
    else
    {
      // if not, then either it or its child is a text element
      string name=child.Name=="#text" ? node.Name : child.Name;
      if (!cols.Contains(name))
      {
        // add the column header
       cols.Add(name, colIdx);
       colHasData[colIdx]=false;
       lvResults.Columns.Add(name, -2, HorizontalAlignment.Left);
       ++colIdx;
      }
    }
    child=child.NextSibling;
  }
}

用数据填充行

此函数检查每个子节点的属性值和文本值,并递归进入子元素。使用了一些逻辑来防止空行。

private void ProcessChildData(XmlNode node, XmlNode child)
{
  while (child != null)
  {
    ProcessAttributes(child);

    // if this child is an element, get its children
    if (child.FirstChild is XmlElement)
    {
      ProcessChildData(child, child.FirstChild);
      if (hasAttributes | hasData)
      {
        lvi=CreateLVI(cols.Count);
        lvResults.Items.Add(lvi);
        hasData=false;
        hasAttributes=false;
      }
    }
    else
    {
      // if not, then either it or its child is a text element.
      string name=child.Name=="#text" ? node.Name : child.Name;
      int n=(int)cols[name];
      // set the data for the column
      lvi.SubItems[n].Text=child.InnerText;
      hasData=true;
      colHasData[n]=true;
    }
  child=child.NextSibling;
  }
}

结论

写完所有这些之后,我终于觉得我拥有了一套可以用来生成架构和操作 XML 数据的工具。虽然没有解决架构、分面等的所有问题(“choice”架构元素看起来特别棘手),但我认为我创建了一套很好的工具,可以完成我需要架构和 XML 文件的 90% 的工作。如果您觉得有任何特定的功能是“必备”的,请告诉我,我会尝试将其纳入。

历史

2003 年 10 月 15 日 - 添加了对更复杂架构的支持,修复了一些小错误

© . All rights reserved.