TX Text Control .NET for WPF

XML Processing

Topic

Structured XML data, such as an address list or a product catalog, can be edited in the WPF TextControl like a document. TX Text Control reads the XML file with the two files it refers to: a document type definition (DTD), the rules for which elements may appear where, and a CSS stylesheet, which formats the elements. Users edit the formatted result while every change is checked against the DTD, and code-behind reads, replaces, adds and removes elements by name.

The Enterprise edition includes XML editing; see Editions. Code on this page belongs in MainWindow.xaml.cs with the TextControl named textControl1. To use XML as the data of a mail merge instead, read JSON and XML.

MainWindow.xaml.cs needs these using directives:

using System.Text;
using System.Windows;
using TXTextControl;

The Three Files of an XML Document

The sample is an address list, address.xml. Its second and third lines name the stylesheet and the DTD:

<?xml version="1.0"?>
<?XML:stylesheet type="text/css" href="address.css"?>
<!DOCTYPE address SYSTEM "address.dtd">
<address>
  <address_record>
    <name>Paul</name>
    <street>Baker Street</street>
    <city>London</city>
    <email>paul@hotmail.com</email>
  </address_record>
  <address_record>
    <name>George</name>
    <email>george@hotmail.com</email>
  </address_record>
</address>

address.dtd requires at least one record and, in each record, a name and an email address, with an optional street and city between them:

<!ELEMENT address ( address_record+ )>
<!ELEMENT address_record ( name, street?, city?, email )>
<!ELEMENT name (#PCDATA )>
<!ELEMENT street (#PCDATA )>
<!ELEMENT city (#PCDATA )>
<!ELEMENT email (#PCDATA )>

The DTD doesn't have to be a file of its own. It can also be written inline, in square brackets inside the DOCTYPE declaration of the XML file, so that one file carries the data and its rules:

<?xml version="1.0"?>
<?XML:stylesheet type="text/css" href="address.css"?>
<!DOCTYPE address [
  <!ELEMENT address ( address_record+ )>
  <!ELEMENT address_record ( name, street?, city?, email )>
  <!ELEMENT name (#PCDATA )>
  <!ELEMENT street (#PCDATA )>
  <!ELEMENT city (#PCDATA )>
  <!ELEMENT email (#PCDATA )>
]>
<address>
  ...
</address>

address.css turns the list into a table:

address { display: table }
address_record { display: table-row }
name, street, city, email { display: table-cell; border: 1px; font-size: 11pt }

This is what the WPF TextControl shows after loading the three files:

The XML address list in a WPF TextControl, formatted as a table with one row per address by the CSS file.

Only the stylesheet decides the layout. These rules show each name as a blue heading and the other elements as a list:

name { display: block; font-size: 14pt; color: blue }
street, city, email { display: list-item; font-size: 11pt }

The same XML address list in a WPF TextControl with another CSS file: blue names followed by bulleted street, city and email lines.

Load an XML File and Handle Errors

Pass StreamType.XMLFormat to Load. Two events report problems with the file; attach them in MainWindow.xaml:

<WPF:TextControl x:Name="textControl1"
                XmlInvalid="textControl1_XmlInvalid"
                XmlNotWellFormed="textControl1_XmlNotWellFormed" />

The code-behind loads the file, shows whether the document is validated, and reports errors:

private void OpenXml_Click(object sender, RoutedEventArgs e)
{
    textControl1.Load("address.xml", StreamType.XMLFormat);

    statusText.Text = textControl1.XmlEditMode == XmlEditMode.Validate
        ? "Valid XML: changes are checked against the DTD"
        : "Not validated";
}

private void textControl1_XmlInvalid(object sender, XmlErrorEventArgs e)
{
    MessageBox.Show($"The document doesn't match its DTD: {e.Reason} (line {e.LineNumber}, position {e.LinePosition})");
}

private void textControl1_XmlNotWellFormed(object sender, XmlErrorEventArgs e)
{
    MessageBox.Show($"The file isn't well-formed XML: {e.Reason} (line {e.LineNumber})");
}
The fileEventResult
Matches its DTDNoneLoaded with the CSS styles; XmlEditMode is Validate
Breaks a rule of the DTDXmlInvalidLoaded as a normal document, without validation
Contains invalid XML syntaxXmlNotWellFormedNot loaded

XmlErrorEventArgs describes the error with Reason, LineNumber, LinePosition, FilePosition and URL. Where a value is 0 or an empty string, the information isn't available. The statusText of the handler is a TextBlock in the window. Load(StreamType.XMLFormat) without a file name shows the Open dialog box filtered to XML files.

What Changes in the XML Editor Mode

While XmlEditMode is Validate, the editor protects the structure of the document:

  • Text can be typed and deleted only in elements that contain text. A selection stays inside one element and expands to its whole text when the user drags across elements. TAB and SHIFT+TAB select the next and the previous element.
  • Formatting commands change the style of the current element, because XML carries no formatting. In code, FormattingStylesDialog opens the style dialog, and InlineStyles and ParagraphStyles change the styles directly.
  • Text pasted from another document loses its formatting.
  • Selection.Load and Selection.Save can't be used, because an XML document can't be part of another document.

Validate can't be set in code; loading a valid file switches it on. Setting XmlEditMode to NoValidate ends validation. The TextControl then works as a normal editor, and the document can't be saved as XML any more, because it may no longer be valid.

Work With Elements in Code

GetXmlElements takes an element name and returns an XmlElementCollection of XmlElement objects. When an element has sub-elements, its text consists of their texts, one per line, separated by \n. The handler below lists the names, rewrites the second record and inserts a third one:

private void EditElements_Click(object sender, RoutedEventArgs e)
{
    StringBuilder names = new StringBuilder();

    foreach (XmlElement name in textControl1.GetXmlElements("name"))
    {
        names.AppendLine(name.Text);
    }

    statusText.Text = names.ToString();

    XmlElementCollection records = textControl1.GetXmlElements("address_record");

    // One line per sub-element: name and email
    records.GetItem(2).Text = "George\ngeorge@textcontrol.com";

    // -1 ignores the "before" position; 2 inserts after the second record
    XmlElement newRecord = new XmlElement("address_record", "Ringo\nAbbey Road\nLondon\nringo@hotmail.com");

    if (!records.Add(newRecord, -1, 2))
    {
        MessageBox.Show("The DTD doesn't allow this record here.");
    }
}

The code works as follows:

  • GetItem returns the element with a one-based index or null; without a parameter, the element at the input position.
  • XmlElement.Text reads and writes the text, one line per sub-element.
  • The XmlElement constructor takes the name and, optionally, the text of the new element.
  • XmlElementCollection.Add places the element before or after a position in the collection, where 0 stands for the element at the input position and -1 ignores the parameter. If the DTD doesn't allow the element there, Add returns false and XmlInvalid occurs.

Remove deletes the element with a one-based index, or with 0 the one at the input position. For sub-elements, AddChild, GetChildItem and RemoveChild are available; the DTD determines where a new child goes. Any change that would break the DTD fails, such as removing the only remaining record.

Save the XML File and Its Styles

The XML file receives the elements and their text only. Formatting belongs in the stylesheet, which is saved in a separate call:

private void SaveXml_Click(object sender, RoutedEventArgs e)
{
    textControl1.Save("address.xml", StreamType.XMLFormat);
    textControl1.Save("address.css", StreamType.CascadingStylesheet);
}

The DTD is never changed or saved, and TX Text Control can't create an XML document from scratch; it edits XML files that come with a DTD.

Next Steps