1

I'm building a class library in C# which uses the XmlTextWriter class to build an XML which is then exported as a HTML document.

However when I save the file with a .html extension using the XmlTextWriter object as content, the resulting file only contains the text "System.Xml.XmlTextWriter"

This comes up in the method defined below, specifically in the final line:-

public void SaveAsHTML(string filepath)
        {
            XmlTextWriter html;
            html = new XmlTextWriter(@"D:/HTMLWriter/XML/HTMLSaveAsConfig.xml", System.Text.Encoding.UTF8);
            html.WriteStartDocument();
            html.WriteStartElement("html");
            html.WriteRaw(Convert.ToString(Head));
            html.WriteRaw(Convert.ToString(Body));
            html.WriteEndElement();
            html.WriteEndDocument();
            html.Flush();
            html.Close();
            System.IO.File.WriteAllText(filepath, html.ToString());
        }

For context, the variables Head and Body are also XmlTextWriter objects containing what will become the and elements of the html file respectively.

I've tried using Convert.ToString(), which causes the same issue.

I'm tempted to try overriding the ToString() method for my class as a fix, potentially using the XmlSerializer class. However I was wondering if there's a less noisy way of returning the Xml object as a string?

Ben Roberts
  • 77
  • 1
  • 1
  • 9
  • Are you trying to write the output to `"D:/HTMLWriter/XML/HTMLSaveAsConfig.xml"` or `filepath`? – Phil Brubaker May 11 '20 at 00:24
  • @PhilBrubaker the output should be written to filepath. HTMLSaveAsConfig.xml will be deleted when the object is deconstructed. – Ben Roberts May 11 '20 at 17:05
  • Thanks Ben - I figured as much. Please see my [Answer](https://stackoverflow.com/a/61722567/6610379) below, it may get you closer to a good place with this problem. – Phil Brubaker May 11 '20 at 17:18

3 Answers3

1

The following function will extract the string from the System.Xml.XmlTextWriter objects you've created as Head and Body.

private string XmlTextWriterToString(XmlTextWriter writer)
{
    // Ensure underlying stream is flushed.
    writer.Flush();
    // Reset position to beginning of stream.
    writer.BaseStream.Position = 0;
    using (var reader = new StreamReader(writer.BaseStream))
    {
        // Read and return content of stream as a single string
        var result = reader.ReadToEnd();
        return result;
    }
}

Some caveats here are that the underlying System.IO.Stream object associated with the System.Xml.XmlTextWriter must support both 'read' and 'seek' operations (i.e., both Stream.CanRead and System.CanSeek properties must return true, respectively).

I've made the following edits to your original code:

  1. Replaced the Convert.ToString() calls with calls to this new function.
  2. Made an assumption that you're intending to write to the file specified by the filepath parameter to your SaveAsHTML() function, and not the hard-coded path.
  3. Wrapped the creation (and use, and disposal) of the System.Xml.XmlTextWriter in a using block (if you're not familiar, see What are the uses of “using” in C#?).

Following is your code with those changes.

public void SaveAsHTML(string filepath)
{
    using (var html = new XmlTextWriter(filepath, System.Text.Encoding.UTF8))
    {
        html.WriteStartDocument();
        html.WriteStartElement("html");
        html.WriteRaw(XmlTextWriterToString(Head));
        html.WriteRaw(XmlTextWriterToString(Body));
        html.WriteEndElement();
        html.WriteEndDocument();
        html.Flush();
        html.Close();
    }
}

Another thing of which to be mindful is that, not knowing for sure from the code provided how they're being managed, the lifetimes of Head and Body are subject to the same exception-based resource leak potential that html was before wrapping it in the using block.

A final thought: the page for System.Xml.XmlTextWriter notes the following: Starting with the .NET Framework 2.0, we recommend that you create XmlWriter instances by using the XmlWriter.Create method and the XmlWriterSettings class to take advantage of new functionality.

Phil Brubaker
  • 1,257
  • 3
  • 11
  • 14
  • Thanks Phil. With a bit of tweaking, this should work for me. My original plan was to override the ToString() method with something like this, but given that the operation is being made on an XmlTextWriter rather than the object derived from my class, I'm making it a protected method within the class. Got a few creases to iron out, but once it's complete I'll update this thread with the result. – Ben Roberts May 11 '20 at 19:58
0

The last line writes the value of XmlTextWriter.ToString(), which does not return the text representation of the XML you wrote. Try leaving off the last line, it looks like your XmlTextWriter is already writing to a file.

Paramecium13
  • 345
  • 1
  • 7
0

@PhilBrubaker's solution seems to be on the right track. There are still a few bugs in my code that I'm working towards getting a fix for, but the good news is that the casting seems to be working now.

protected string XmlToString(XmlWriter xmlBody)
        {
            XmlTextWriter textXmlBody = (XmlTextWriter)xmlBody;
            textxmlBody.BaseStream.Position = 0;
            using (var reader = new StreamReader(textXmlBody.BaseStream))
            {
                var result = reader.ReadToEnd();
                reader.Dispose();
                return result;
            }
        }

I've changed the input parameter type from XmlWriter and cast it explicitly to XmlTextWriter in the method, this is so that the method also works when the Create() method is used instead of an initialisation as recommended for .NET 2.0. It's not 100% reliable at the moment as XmlWriter doesn't always cast correctly to XmlTextWriter (depending on the features), but that's out of the scope for this thread and I'm investigating that separately.

Thanks for your help!

On a side note, the using block is something I haven't come across before, but it's provided so many solutions across the board for me. So thanks for that too!

Ben Roberts
  • 77
  • 1
  • 1
  • 9
  • Glad to be able to help! – Phil Brubaker May 12 '20 at 08:39
  • Note that the [`using`](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/using-statement) block you're using here for your [`StreamReader`](https://learn.microsoft.com/en-us/dotnet/api/system.io.streamreader) manages the call to [`reader.Dispose`()](https://learn.microsoft.com/en-us/dotnet/api/system.io.textreader.dispose) for you, even if an exception is thrown. You might call [`reader.Close`()](https://learn.microsoft.com/en-us/dotnet/api/system.io.streamreader.close) here, though, as a matter of semantics. – Phil Brubaker May 12 '20 at 08:50