动态生成的 XML 数据编辑器






4.88/5 (72投票s)
使用 XML 架构定义 (XSD) 文档,此实用工具可以动态生成一个数据录入表单,用于创建和编辑 XML 数据。
目录
引言
我 以前的实用工具 [^] 帮助您创建 XSD 文件。完成此操作后,我寻找了一个允许我管理关联 XML 文档数据的实用工具。具体来说,我想要两个功能:
- 一个通用的解决方案,允许我根据任何 XSD 架构创建 XML 数据
- 一个能够根据架构信息定制输入控件的东西
我没有找到任何接近满足这两个标准的东西。我找到的所有编辑器都侧重于编辑整个 XML 文档(包括标签),而不仅仅是数据。对我来说,标签是快速组织数据集的一个主要障碍。此外,.NET 解决方案(创建 CS 文件)是一种复杂、多步骤且非通用的方法。
因此,我决定编写自己的 XML 数据编辑器,并在过程中学习更多关于 XSD、XML 和 XPath 的知识。利用 XmlDataDocument
、XmlSchema
和 DataTable
类,我创建了一个通用的编辑器,该编辑器根据架构定义和表关系动态创建用于数据录入的专用控件。
结果的示例是:(图片太长,抱歉!)
...这是从架构派生的(我的 XML 架构编辑器的截图)
示例文件
下载包含一个示例 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);
还有一些关于缩进和计算外部组框大小的简单计算,我将不详细讨论。
控件创建
组框内的控件是根据从架构收集的信息中的一些简化规则创建的。这个区域可以大大扩展,因为我不想实现所有基本类型。规则如下:
- 如果元素包含枚举,则使用枚举数据创建一个
ComboBox
。 - 如果元素包含
minInclusive
和maxInclusive
分面,则创建一个NumericUpDown
控件。 - 如果元素是布尔类型,则创建一个
CheckBox
控件。 - 如果元素是十进制或正整数类型,则创建一个右对齐的
TextBox
控件。 - 其他所有都是
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 查询
我实现了 XPath 功能,以便能够尝试如何从 XML 数据中提取信息。我来自数据库背景,我想要一些东西可以展平层次结构,这样我就可以一次看到所有数据,而无需通过层级进行鼠标点击等操作。使用 XmlDataDocument
的 SelectNodes
方法
XmlNodeList nodeList=null;
try
{
nodeList=doc.SelectNodes(edXPath.Text);
DlgXPathResult dlg=new DlgXPathResult(nodeList);
dlg.ShowDialog(this);
}
我提取一个节点列表,如果成功,则将其传递给对话框。数据在 ListView
控件中显示,其标题从 XmlNode
中提取。显示节点数据有三个基本步骤:
- 获取标题。
- 用数据填充行。
- 清理空列。
获取头部信息
标题信息从节点列表的属性和元素中提取。此函数对元素进行递归操作,因此可能会生成不必要的列。此外,还会忽略重复的元素名称,如果您的架构在两个不同的区域使用相同的元素名称,而您又恰好查询这些元素,则可能导致覆盖问题。
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 日 - 添加了对更复杂架构的支持,修复了一些小错误