|
|||||||||||||||||||||
|
|||||||||||||||||||||
|
Announcements
Want a new Job?
Chapters
Services
Feature Zones
|
Contents
IntroductionMy previous utility [^] helped you to create XSD files. After completing this, I hunted around for a utility that would let me manage the data of an associated XML document. Specifically, I wanted two features:
I did not find anything that came close to meeting these two criteria. All of the editors I found were oriented around editing the entire XML document, tags and all, not just the data. For me, the tags are a major encumberance to quickly putting together a data set. Furthermore, the .NET solution, which is to create a CS file, is a complicated, multi-step and non-generic approach. Thus, I decided to write my own XML data editor and, in the process, learn more about XSD, XML, and XPath. Utilizing the An example of the result is: (Sorry about the long picture!)
...which is derived from the schema (screenshot from my XML Schema Editor):
Example FilesThe download includes an example XSD and XML file. Load the sampleSchema2.XSD file first using the File/Load XSD menu item. Then load the sale5.xml file using the File/Load XML menu item. Some Background Basics: Importing the Schema and XML DocumentsGiven: private XmlSchema schema=null;
private XmlDataDocument doc=null;
The XSD file is loaded: // 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);
The XML file is loaded: XmlTextReader tr=new XmlTextReader(dlgOpen.FileName);
doc.Load(tr);
tr.Close();
Dynamic GUI GenerationOnce the schema has been loaded into the Get Root TablesThe GUI generation begins by inspecting all root (not parent) tables and determining the 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;
}
}
}
There are two very simple helper functions to acquire the appropriate objects, illustrated here because of the different mechanism required to obtain them: 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;
}
Create the GroupBox: A Collection of RecordsThe 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);
...
Once the group box is placed, the following steps are performed on all columns in the // For each column in the table...
foreach (DataColumn col in dt.Columns)
{...
1. The placement of the column name and the appropriate control for non-hidden columns: // 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. The placement of the column name and read-only text control for hidden fields. This could be omitted, but I left it in so that I could inspect the hidden features of the 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. If any child relationships are discovered, the same function is invoked recursively: // 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);
There's also some straightforward math regarding indenting and calculating the size of the outer groupbox, which I'm not going to discuss. Control CreationThe controls within the group box are created based on some simplistic rules from information gathered from the schema. This area could be greatly expanded because I simply didn't want to implement all the basic types. The rules are as follows:
Currently, I ignore other facets such as length and pattern. Record NavigationRecord navigation requires that we manually select child records that are indexed by the parent record ID. The BasicsThese rows are stored in the 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();
}
The 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;
}
Moving AroundEach handler -- first, previous, next and last -- has basically the same form. I'll demonstrate the "next" handler. Each button in any given navigation bar is "tagged" with the 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);
}
}
For this reason, a little bit of manipulation is necessary to acquire the desired record. If the table is a root table, then there is a 1:1 correlation between our internal position and the rows in the 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);
}
This is done with rather brute force and could be optimized to look forward or backward from its current position. Oh, well. Next version... 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;
}
}
}
Updating ChildrenNow, when we change to another record in a parent table, all the children need to be updated to display the set of rows that relate to the parent record. This is accomplished recursively. The matching rows of all children is acquired and the child is set to the first record if it exists. Then the same is done for its children. 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);
}
}
Adding a RecordAdding a record is rather nasty. Records are added to the end of the collection, which helps a bit. Any relational fields to the parent must be set to the parent ID and all child relationships must have a new record created, as well... Recursively, of course. 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);
}
}
...and for each child table... 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);
}
}
Deleting a RecordInterestingly, deleting a record is very straightforward. This is thanks to cascading deletes, which the 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);
...
...followed by a bunch of code to display a valid record and reset the children. Data BindingData binding makes life incredibly easy with regards to associating a control with a column in a ctrl.DataBindings.Add("Text", dt, dc.ColumnName);
This binds the "Text" field of the control to the specified data table and column. Isn't reflection great? The BindingContext[dt].Position=dt.Rows.Count;
XPath Queries
I implemented XPath capabilities so that I could play with how to extract information from the XML data. Coming from a database background, I wanted something that would flatten the hierarchy so that I could see all the data at once, without needing to mouse-click, etc. through layers of hierarchy. Using the XmlNodeList nodeList=null;
try
{
nodeList=doc.SelectNodes(edXPath.Text);
DlgXPathResult dlg=new DlgXPathResult(nodeList);
dlg.ShowDialog(this);
}
I extract a node list and, if it's successful, pass it on to a dialog box. The data is displayed in a
Getting Header InformationThe header information is extracted from both the attributes and the elements of the node list. This function operates recursively on elements and therefore can generate unwanted columns. Also, duplicate element names are ignored, which can lead to overwrite problems if your schema uses the same element name in two different areas and you happen to query on those elements. 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;
}
}
Fill the Rows with DataThis function inspects each child node for attribute values and text values, and recurses into child elements. Some logic is used to prevent empty rows. 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;
}
}
ConclusionAfter writing all this, I finally feel like I have a set of tools that I can use to generate schemas and manipulate the XML data. While not addressing all the issues of schemas, facets, etc. (the "choice" schema element looks particularly nasty), I feel that I've created a good set of tools for accomplishing 90% for which I need schemas and XML files. If there's any particular feature you feel is a "must have," let me know and I'll try to encorporate it. HistoryOct 15, 2003 - Added support for more complex schemas, fixed some minor bugs | ||||||||||||||||||||