TX Text Control .NET Server

XML Documents

Topic

An XML file such as an address list holds structured data and no formatting. ServerTextControl loads it together with its document type definition (DTD), which says which elements are allowed where, and a CSS stylesheet, which says how each element looks. Your code then reads and changes the elements by name, and every change is checked against the DTD. Read on for the files involved, the rules for loading and saving and the classes that give access to the elements.

XML as a data source for a mail merge is a different subject, covered in Data Sources. Events of ServerTextControl in general are listed in Events.

Requirements

XML documents need the Classic engine. The XML filter is part of the Classic package TXTextControl.TextControl.ASP.SDK, which runs on Windows only; the Core package TXTextControl.TextControl.Core.SDK doesn't contain it, just as it doesn't contain the filter for DOC files. Create Your First Application compares the two packages. The examples are console programs that use the Classic package.

The XML, DTD and CSS Files

The examples on this page use an address list. Save the three files below in a Documents folder of your project and set their Copy to Output Directory property to Copy if newer.

addresses.xml names its stylesheet and its DTD in its first lines:

<?xml version="1.0"?>
<?XML:stylesheet type="text/css" href="addresses.css"?>
<!DOCTYPE Addresses SYSTEM "addresses.dtd">
<Addresses>
  <DocumentHeadline>Addresses</DocumentHeadline>
  <AddressTable>
    <Address>
      <Name>Jack Miller</Name>
      <Street>Baker Street</Street>
      <City>New York</City>
    </Address>
    <Address>
      <Name>Tom Baker</Name>
      <Street>Miller Street</Street>
      <City>Los Angeles</City>
    </Address>
    <Address>
      <Name>Bill Taylor</Name>
      <Street>Old Street</Street>
      <City>Washington</City>
    </Address>
  </AddressTable>
</Addresses>

addresses.dtd allows one headline and one address table with at least one address. Each address has a name, a street, a city and an optional telephone number:

<!ELEMENT Addresses (DocumentHeadline, AddressTable)>
<!ELEMENT DocumentHeadline (#PCDATA)>
<!ELEMENT AddressTable (Address+)>
<!ELEMENT Address (Name, Street, City, Telephone?)>
<!ELEMENT Name (#PCDATA)>
<!ELEMENT Street (#PCDATA)>
<!ELEMENT City (#PCDATA)>
<!ELEMENT Telephone (#PCDATA)>

Only elements declared with #PCDATA contain text: DocumentHeadline, Name, Street, City and Telephone. Addresses, AddressTable and Address only give the document its structure.

addresses.css formats the elements:

DocumentHeadline { display: block; font-size: 16pt; color: blue }
Name { display: block; font-size: 12pt }
Street, City, Telephone { display: block; font-size: 11pt }

Load and Save an XML Document

ServerTextControl.Load with the StreamType value XMLFormat loads the XML file, checks it against its DTD and applies the styles of the CSS file. The following program loads the address list, reports errors and saves the document again:

using TXTextControl;

using (ServerTextControl tx = new ServerTextControl())
{
    tx.Create();

    // Report documents that break the DTD or aren't XML
    tx.XmlInvalid += (sender, e) =>
        Console.WriteLine($"Invalid: {e.Reason} (line {e.LineNumber}, position {e.LinePosition})");
    tx.XmlNotWellFormed += (sender, e) =>
        Console.WriteLine($"Not well-formed: {e.Reason} (line {e.LineNumber}, position {e.LinePosition})");

    tx.Load("Documents/addresses.xml", StreamType.XMLFormat);
    Console.WriteLine(tx.Text);

    // The text goes to the XML file, the styles to the CSS file
    tx.Save("Documents/addresses.xml", StreamType.XMLFormat);
    tx.Save("Documents/addresses.css", StreamType.CascadingStylesheet);
}

What happens on loading depends on the file:

  • Valid. The document matches its DTD and is loaded with the formatting of its CSS file.
  • Invalid. The document breaks a rule of the DTD. XmlInvalid occurs, and the text and the styles are still loaded.
  • Not well-formed. The file contains invalid XML syntax. XmlNotWellFormed occurs, and the document isn't loaded.

Saving has three rules:

  • Two calls of Save. Save with XMLFormat writes the text and the elements to the XML file, and a second call with CascadingStylesheet writes the styles to the CSS file. Without the second call, changes of the formatting are lost.
  • The DTD stays as it is. Changing and saving the DTD isn't supported, and neither is creating a new XML document from scratch: start from an existing XML file with its DTD.
  • No partial loading and saving. Selection.Load and Selection.Save can't be used with XML, because an XML document inserted into another document, or a part of one, would no longer be valid.

An XML document has no direct formatting: its formatting comes from the styles of the CSS file, which are in the InlineStyles and ParagraphStyles collections after loading. Change a style there and save the CSS file to keep the change.

Handle XML Errors

Both events pass an XmlErrorEventArgs object:

PropertyContains
ReasonThe reason for the error
LineNumberThe line of the XML document that contains the error, or 0 if it isn't known
LinePositionThe character position of the error within the line, or 0
FilePositionThe absolute position of the error in the file, or 0
URLThe URL of the document that contains the error, or an empty string

XmlInvalid doesn't only occur on loading. It also occurs when your code changes the document in a way the DTD doesn't allow, as described in Insert and Remove Elements.

Read the Elements of a Document

ServerTextControl.GetXmlElements returns an XmlElementCollection with all elements of one name, as XmlElement objects. There is one collection for each element name: one for all Address elements, another for all Name elements. The following program reads the address list in three ways:

using TXTextControl;

using (ServerTextControl tx = new ServerTextControl())
{
    tx.Create();
    tx.Load("Documents/addresses.xml", StreamType.XMLFormat);

    // All Name elements, in the order of the document
    foreach (XmlElement name in tx.GetXmlElements("Name"))
    {
        Console.WriteLine($"{name.ElementName}: {name.Text}");
    }

    // The same with an index, which starts at 1
    XmlElementCollection names = tx.GetXmlElements("Name");

    for (int i = 1; i <= names.Count; i++)
    {
        Console.WriteLine($"Name {i} of {names.Count}: {names.GetItem(i).Text}");
    }

    // An element with sub-elements returns their texts, separated by line feeds
    Console.WriteLine(tx.GetXmlElements("Address").GetItem(2).Text);
}

The last line prints the three sub-elements of the second address on three lines:

Tom Baker
Miller Street
Los Angeles

The code works as follows:

  • GetItem takes a one-based index and returns null if there is no element with it. Without a parameter, it returns the element at the current input position.
  • Count is the number of elements in the collection.
  • ElementName is the name of the element.
  • Text is the text of the element. For an element with sub-elements, it joins their texts with line feed characters (character 10, \n in C#).

Change the Text of Elements

Setting Text replaces the text of an element. For an element with sub-elements, separate the texts of the sub-elements with \n, in the order of the DTD:

using TXTextControl;

using (ServerTextControl tx = new ServerTextControl())
{
    tx.Create();
    tx.Load("Documents/addresses.xml", StreamType.XMLFormat);

    // Replace the second address: name, street and city
    tx.GetXmlElements("Address").GetItem(2).Text = "Paul Baskerville\nJackson Street\nLondon";

    // Change a single text element
    tx.GetXmlElements("DocumentHeadline").GetItem(1).Text = "Customer Addresses";

    tx.Save("Documents/addresses.xml", StreamType.XMLFormat);
}

XmlElement.Select selects the text of an element that belongs to the document and returns true if it could be selected. For an element with sub-elements, it selects the text of the first sub-element. The selected text is then available through tx.Selection, as for any other document.

Insert and Remove Elements

Elements are added and removed through the collection of their name, and each change is checked against the DTD. With addresses.dtd, only complete Address elements can be added to or removed from the address table, and a Telephone element can be added to an address that has none. A change the DTD doesn't allow isn't made: the method returns false, and XmlInvalid occurs. The DTD requires at least one address, so removing the last one fails, for example.

using TXTextControl;

using (ServerTextControl tx = new ServerTextControl())
{
    tx.Create();
    tx.XmlInvalid += (sender, e) => Console.WriteLine($"Not allowed: {e.Reason}");
    tx.Load("Documents/addresses.xml", StreamType.XMLFormat);

    XmlElementCollection addresses = tx.GetXmlElements("Address");

    // Insert a new address after the second one
    XmlElement newAddress = new XmlElement("Address", "Paul Baskerville\nJackson Street\nLondon");
    bool added = addresses.Add(newAddress, -1, 2);
    Console.WriteLine($"Address added: {added}, addresses: {tx.GetXmlElements("Address").Count}");

    // Give the first address a telephone number
    XmlElement firstAddress = tx.GetXmlElements("Address").GetItem(1);
    firstAddress.AddChild(new XmlElement("Telephone", "555 0100"));
    Console.WriteLine($"Telephone: {firstAddress.GetChildItem("Telephone").Text}");

    // Remove the telephone number and the address inserted above
    firstAddress.RemoveChild("Telephone");
    bool removed = tx.GetXmlElements("Address").Remove(3);
    Console.WriteLine($"Address removed: {removed}");

    tx.Save("Documents/addresses.xml", StreamType.XMLFormat);
}

The code works as follows:

  • The XmlElement constructor takes the name of the element and, optionally, its text. The text of an element with sub-elements is separated by \n, as for Text.
  • XmlElementCollection.Add takes the element and two one-based positions in the collection, before and after. Add(newAddress, -1, 2) places the new address after the second one: -1 means that a parameter is ignored, and 0 places the element before or after the element at the current input position.
  • AddChild adds a sub-element at the position the DTD gives it. Where an element can have several sub-elements of the same name, a second overload, AddChild(element, before, after), places it among them.
  • GetChildItem returns the first sub-element of a name, or, with a one-based index as the second parameter, a particular one. RemoveChild takes the same parameters and returns false if the DTD doesn't allow the removal.
  • XmlElementCollection.Remove removes the element with a one-based index; 0 removes the element at the current input position.

Two properties of XmlElement control what happens when a new element is added: with AutoText set to true, an element without text gets its name in square brackets as its text, such as [Telephone], and with AutoSelect set to true, the text of the new element is selected.

Next Steps