Problems extending the XMLElement(Visual Basic)
Problems extending the XMLElement(Visual Basic)
Problems extending the XMLElement(Visual Basic)
Problems extending the XMLElement(Visual Basic)
Problems extending the XMLElement(Visual Basic)
Problems extending the XMLElement(Visual Basic) Problems extending the XMLElement(Visual Basic) Problems extending the XMLElement(Visual Basic) Problems extending the XMLElement(Visual Basic) Problems extending the XMLElement(Visual Basic) Problems extending the XMLElement(Visual Basic) Problems extending the XMLElement(Visual Basic) Problems extending the XMLElement(Visual Basic)
Problems extending the XMLElement(Visual Basic) Problems extending the XMLElement(Visual Basic)
Problems extending the XMLElement(Visual Basic)
Go Back  Xtreme Visual Basic Talk > > > Problems extending the XMLElement(Visual Basic)


Reply
 
Thread Tools Display Modes
  #1  
Old 10-11-2010, 01:41 PM
HarbingTarbl HarbingTarbl is offline
Newcomer
 
Join Date: Oct 2010
Posts: 2
Default Problems extending the XMLElement(Visual Basic)


I have to create an XML database for a class I'm taking, but I'm running into some problems.

I'm trying to pack my data directly into the DOM using some classes I created and derived from the XMLDocument class, I'm trying to do it this way so I can easily manipulate the entries while the document is in memory and save them back to a file without having to mess around too deeply with the XMLDocument class.

I'm running into some problems however when I try to typecast the elements I get from calling
Code:
XMLDocument.DocumentElement.Item
I assume that this is happening due to my derived class not being pushed into the DOM when a file is loaded, because it works just fine if I enter the data manually in code.
Code:
Option Explicit On
Option Strict On

Imports System.Xml
Imports System.IO


Module Module1
    Class InvoiceDataXML
        Inherits XmlElement

        Public Sub New(ByVal Prefix As String, ByVal InvoiceNumber As String, ByVal URI As String, ByVal Document As XmlDocument)
            'Is this the correct way to create default elements?
            'Are the elements correctly packed/filled when Load is called?
            'That fact that Load is completely invisible makes it somewhat difficult to understand how it works
            MyBase.New(Prefix, "Invoice", URI, Document)
            Dim Child As XmlElement
            Dim Attribute As XmlAttribute
            'Invoice Number
            Attribute = Document.CreateAttribute("Number")
            Attribute.Value = InvoiceNumber
            Me.Attributes.Append(Attribute)
            'Item Number
            Child = Document.CreateElement("ItemNumber")
            Attribute = Document.CreateAttribute("value")
            Child.Attributes.Append(Attribute)
            AppendChild(Child)
            'Last Name
            Child = Document.CreateElement("LastName")
            Attribute = Document.CreateAttribute("value")
            Child.Attributes.Append(Attribute)
            AppendChild(Child)
            'First Name
            Child = Document.CreateElement("FirstName")
            Attribute = Document.CreateAttribute("value")
            Child.Attributes.Append(Attribute)
            AppendChild(Child)
            'Total
            Child = Document.CreateElement("Total")
            Attribute = Document.CreateAttribute("value")
            Child.Attributes.Append(Attribute)
            AppendChild(Child)
        End Sub
        Public ReadOnly Property InvoiceNumber As String
            Get
                Return Me.GetAttribute("Number")
            End Get
        End Property

        Public Property ItemNumber As String
            Get
                Return Me.Item("ItemNumber").GetAttribute("value")
            End Get
            Set(ByVal value As String)
                Me.Item("ItemNumber").SetAttribute("value", value)
            End Set
        End Property

        Public Property LastName As String
            Get
                Return Me.Item("LastName").GetAttribute("value")
            End Get
            Set(ByVal value As String)
                Me.Item("LastName").SetAttribute("value", value)
            End Set
        End Property

        Public Property FirstName As String
            Get
                Return Me.Item("FirstName").GetAttribute("value")
            End Get
            Set(ByVal value As String)
                Me.Item("FirstName").SetAttribute("value", value)
            End Set
        End Property

        Public Property Total As String
            Get
                Return Me.Item("Total").GetAttribute("value")
            End Get
            Set(ByVal value As String)
                Me.Item("Total").SetAttribute("value", value)
            End Set
        End Property

    End Class

    Class InvoiceDocument
        Inherits XmlDocument

        Public Function CreateInvoice(ByVal InvoiceNumber As String) As InvoiceDataXML
            Dim InvoiceData As InvoiceDataXML = New InvoiceDataXML(Nothing, InvoiceNumber, Nothing, Me)
            'Is it alright for prefix and the namespace URI to be blank?
            Return InvoiceData
        End Function

        Public Overrides Function CreateElement(ByVal prefix As String, ByVal localName As String, ByVal namespaceURI As String) As System.Xml.XmlElement
            If (localName = "Invoice") Then
                'Return CreateInvoice() I'm wanting to read from the file, and then return the correct invoice number for it to create,
                'but the only way I know of to get an attribute from an XMLElement, is to call CreateElement and then read the attribute from there,
                'which kind of defeats the purpose of my CreateInvoice function
            End If
            Return MyBase.CreateElement(prefix, localName, namespaceURI)
        End Function

        Public ReadOnly Property Invoice(ByVal InvoiceNumber As String) As XmlElement
            Get
                Return CType(Me.DocumentElement.SelectSingleNode("Invoice[@Number=" & InvoiceNumber & "]"), XmlElement)
            End Get
        End Property
    End Class

    Class XMLConverter
        Public Document As InvoiceDocument
        Public File As StreamReader

        Public Sub New(ByVal File As String)
            Document = New InvoiceDocument
            If (File.Substring(File.LastIndexOf(CChar("."))) = ".xml") Then
                Document.Load(File)
            Else
                Document.AppendChild(Document.CreateElement("Invoices"))
                Me.File = New StreamReader(File, System.Text.Encoding.ASCII)
                Convert()
            End If
        End Sub

        Private Sub Convert()
                Dim Line, Array(5) As String
                Dim Node As InvoiceDataXML
                Do
                    Line = File.ReadLine()
                    Array = Line.Split(CChar(","))
                    Node = Document.CreateInvoice(Array(0))
                    Node.ItemNumber = Array(1)
                    Node.LastName = Array(2)
                    Node.FirstName = Array(3)
                    Node.Total = Array(4)
                    Document.DocumentElement.AppendChild(Node)
                Loop Until File.EndOfStream
        End Sub

        Public Sub Save(ByVal FileName As String)
            Dim File As New FileStream(FileName, FileMode.Create, FileAccess.Write)
            Document.Save(File)
            File.Close()
        End Sub

        Public Sub Add(ByRef Invoice As InvoiceDataXML)
            Document.DocumentElement.AppendChild(Invoice)
        End Sub
    End Class

    Sub Main()
        Dim Converter As New XMLConverter("File.xml")
        Dim Test As InvoiceDataXML = Converter.Document.CreateInvoice("5")
        Test.Total = "$50.23"
        Test.ItemNumber = "355-34"
        Test.LastName = "John"
        Test.FirstName = "Smith"
        'Here is what I am wanting to do, I want to be able to load an XML file using my InvoiceDocument class, and then be able to manipulate the data inside the class
        'using the methods and properties defined inside my InvoiceDataXML class.
        'The problem I am running into, is that I can't seem to find a way to read the data from a file correctly to ensure that it is placed into the InvoiceDocument's DOM
        'as a InvoiceDataXML rather than a XMLElement object.
        Converter.Add(Test)
        Converter.Save("File2.xml")
    End Sub
End Module
Other than writing my own XMLReader I have no idea how to accomplish this, does anyone have an idea how I could go around doing this(other than writing my own XMLReader).
Reply With Quote
  #2  
Old 10-11-2010, 04:39 PM
AtmaWeapon's Avatar
AtmaWeaponProblems extending the XMLElement(Visual Basic) AtmaWeapon is offline
Fabulous Florist

Forum Leader
* Guru *
 
Join Date: Feb 2004
Location: Austin, TX
Posts: 9,500
Default

I think you're just a little inexperienced with this API for accessing XML and made some bad choices. Honestly I rarely bother with creating class hierarchies for XML stuff; usually working with it is straightforward enough that it's appropriate to abstract the file, not the individual elements of the XML. It's possible that you're making some mistakes in the derivation, but it'd take a lot of effort to try and identify it. Let's talk about how I tend to do XML and if someone else wants to figure out exactly where yours went wrong. I'd have given it a run to see what the problem is, but I don't have a file that would match "File.xml" so I'm not certain I can reproduce your scenario.

The XML your code would create looks pretty non-standard, but your comments lead me to believe you're just testing the waters so far so it's understandable. For the sake of completeness, this is what an invoice would look like:
Code:
<Invoice Number="123">
    <ItemNumber value="123" />
    <LastName value="asdf" />
    <FirstName value="asdf" />
    <Total value="123" />
</Invoice>
There's no "right" way to format an XML file, but there are certain practices that can make it easier to parse and understand the file. The "value" attributes above are redundant; all elements are allowed to have one bit of content in addition to attributes; it could have been written like this:
Code:
<Invoice number="123">
    <ItemNumber>123</ItemNumber
    ...
This is what I'm going to run with. It's not wrong to use attributes on an element, but if there's only one attribute it's often sufficient to let the element's content be the value.

First, let's talk about how you use the XmlDocument class to edit an XML document; your code goes about it in kind of a roundabout manner. XmlDocument assumes you know a good bit about XML structure so the names can get confusing. An "element" is any XML node, but it also includes stuff you might not consider to be an element like whitespace and closing tags. "Nodes" are a concept special to this API; if you look at the documentation you'll see that everything in the DOM is a node of some sort.

When I write code that has to use an XML document as a kind of poor man's database, I treat the XML file like a database. This means having a clear separation between the XML and the data items. This lets me drop to plain text if the notion possesses me and eases the transition to a real database if the scenario requires. Some of it's kind of boring, but after we get past the prerequisites you'll see the XML part.

I'll keep the prerequisite short and sweet. We'll make a class to represent an invoice, using the shortest syntax possible and wild guesses for data types:
Code:
Public Class Invoice
    Public Property ItemNumber As String
    Public Property FirstName As String
    Public Property LastName As String
    Public Property Number As Integer
    Public Property Total As Double
End Class
The important part here is this class knows nothing about XML or how it should be represented in XML. This is important, because it means if the XML changes we don't have to bother this class or anything that uses it. OOP nerds would recognize this as separation of concerns.

Now, we need the class that appends an invoice to an XML file. Here's the skeleton to give you an idea; I'll fill it in later:
Code:
Public Class InvoiceFile

    Public Sub AppendInvoice(ByVal invoice As Invoice)

    End Sub

End Class
We'll do AppendInvoice() first because it's what you asked for. If I write it at a high level, this is what I want it to do:
Code:
' Load the document
' Get an XML element that represents the invoice
' Append the element to the document's "Invoice" node
' Save the document.
Loading the document and saving it is pretty straightforward. I suggest writing helper methods for the "get an XML element" step.

When you're building an XML element, you have to keep some things in mind. First, nothing gets created without calling XmlDocument.CreateXXX(), where XXX is the name of some element type. So if you want to add an attribute to an element, you call CreateAttribute(), set its Value property, then append the result to the element's Attributes collection. If you want to add a child element, you call CreateElement() to create that element and call AppendChild() to add it. Adding a non-attribute text value to an element is kind of strange. The Value property isn't valid for elements. You can use the InnerText property, but the more consistent way is to call CreateTextElement with the text you want and use AppendChild() to add that child. Here's how I turn an invoice into an XML element to demonstrate these concepts:
Code:
Private Function InvoiceToXml(ByVal document As XmlDocument, ByVal invoice As Invoice) As XmlElement
    Dim invoiceRoot As XmlElement = document.CreateElement("Invoice")

    ' Invoice number is an attribute
    Dim numberAttribute As XmlAttribute = document.CreateAttribute("number")
    numberAttribute.Value = invoice.Number.ToString()
    invoiceRoot.Attributes.Append(numberAttribute)

    ' Everything else is a plain old element
    Dim itemNumberElement As XmlNode = document.CreateElement("ItemNumber")
    itemNumberElement.AppendChild(document.CreateTextNode(invoice.ItemNumber))
    invoiceRoot.AppendChild(itemNumberElement)

    Dim lastNameElement As XmlElement = document.CreateElement("LastName")
    lastNameElement.AppendChild(document.CreateTextNode(invoice.LastName))
    invoiceRoot.AppendChild(lastNameElement)

    Dim firstNameElement As XmlElement = document.CreateElement("FirstName")
    firstNameElement.AppendChild(document.CreateTextNode(invoice.FirstName))
    invoiceRoot.AppendChild(firstNameElement)

    Dim totalElement As XmlElement = document.CreateElement("Total")
    totalElement.AppendChild(document.CreateTextNode(invoice.Total.ToString()))
    invoiceRoot.AppendChild(totalElement)

    Return invoiceRoot
End Function
Now to append the invoice, we need to open the document and find the "Invoices" element that I'm assuming is the root. We can't just append the new invoice to the document root; that might make an invalid file. Here's what the full method looks like using that InvoiceToXml():
Code:
Public Sub AppendInvoice(ByVal invoice As Invoice)
    ' Some example stuff you'll just have to replace
    Dim document As New XmlDocument()
    If System.IO.File.Exists("example.xml") Then
        document.Load("example.xml")
    Else
        ' Create initial document
        document.AppendChild(document.CreateElement("Invoices"))
    End If

    ' Get the root Invoices element
    Dim invoicesElement As XmlNode = document.SelectSingleNode("Invoices")

    ' Add invoice
    Dim invoiceElement As XmlElement = InvoiceToXml(document, invoice)
    invoicesElement.AppendChild(invoiceElement)

    ' Save
    document.Save("example.xml")
End Sub
It's simple once you can translate an invoice to an XML node.

I've attached the file with the full implementation of all of the classes for you to look at. Ask any questions if you're confused. Obviously you might want to add some features to that class, but it works fine for the example.
Attached Files
File Type: vb XmlExample.vb (2.5 KB, 1 views)
__________________
.NET Resources
My FAQ threads | Tutor's Corner | Code Library
I would bet money 2/3 of .NET questions are already answered in one of these three places.
Reply With Quote
  #3  
Old 10-11-2010, 05:41 PM
HarbingTarbl HarbingTarbl is offline
Newcomer
 
Join Date: Oct 2010
Posts: 2
Default

I understand your code examples, as well as the process of separating the actual data in the Invoice class from it's representation in the XML File.

Perhaps what I am trying is just a bad design decision, but I had hoped to remove that separation from the actual data and it's representation.

Rather than having to keep a collection of Invoices, the XMLDocument's nodes would serve as the collection.

I had thought this would be a better design decision due to my annoyance with keeping data updated between a list view I have that displays the invoice data in a table. I originally planned on doing this using Linq with XML and then binding the data to my listview but I found that to be way beyond my abilities, so instead I thought I could start with learning how to use the XML classes supplied with .NET.

Would it just be better to do it by separating the actual invoice data from the XML and keeping it in a collection inside my main class?

And if that is the case, would I be better off just using a XMLReader/Writer rather than a full blown XMLDocument?
Reply With Quote
  #4  
Old 10-12-2010, 11:22 AM
AtmaWeapon's Avatar
AtmaWeaponProblems extending the XMLElement(Visual Basic) AtmaWeapon is offline
Fabulous Florist

Forum Leader
* Guru *
 
Join Date: Feb 2004
Location: Austin, TX
Posts: 9,500
Default

I'll make my argument, then we'll talk about your alternatives.

It is healthy to separate the data from its representation in any non-trivial application. It's not uncommon for a GUI application to have three distinct versions of the data: a database-friendly version, a logic-friendly version, and a UI-friendly version. It's nice to have this because UI and data store decisions change much more frequently than the logic that uses the data. (Obviously business rules change frequently, but the notion that an employee has a name, ID, salary, etc. is rarely changed after the design phase.)

If you decide to cut out these representations, a change can be much more painful. Suppose you only want to store your data in a ListView, so you make a custom ListViewItem that represents the data with convenience properties. At the logic layer, this seems to work because it's sort of transparent. At the data access layer you still have to do some work but we'll ignore that because it's there in my solution as well. The problem comes when you decide that the ListView control doesn't suit you and you'd rather use a DataGrid or some third-party ListView with more features. This is going to hurt. You have to throw away your current ListViewItem class and rewrite it as whatever object the new control expects. In addition, you have to change all of the business layer logic that depended on that object and all of the data layer logic that depended on it. Even though you might have the same properties and methods on the new object, you'll have changed types which means you have to change the type in all code that touches it.

With the separation I'm suggesting, it's not as painful to make a UI/data layer change. If I change the UI object, the only code I need to change is in the UI and whatever converts the business object to the UI object. The data layer doesn't have to change at all because nothing in the business object changed. It works vice versa; if the data layer changes, I only need to change the code that interfaces between the business and data layers; the UI need not know anything has changed.

You see this separation in the common GUI pattern Model-View-Controller, but modern software engineering has branched it out into several different patterns that use similar separations. In WPF, you can actually almost cut one of the separations out: the UI can sort of directly work with the business layer objects without the need for a UI-layer representation. Of course, when you get to the realities of it you find that there's simply a very general and adaptable UI representation you can use for any data. And a variation of MVC for WPF called Model-View-ViewModel makes a strong case for still having that UI-specific object.

I've blown 3k characters on describing why layered architecture can be convenient. Now the standard software engineering concern applies: almost all techniques for dependency management increase abstraction and thus complexity. If you are certain that your UI will never change in a way that breaks your code *or* you are fine with the effort associated with having the UI coupled to the business layer, it's not a sin to keep them coupled. But you forfeit your right to complain if it causes you trouble later

Now, let's talk how to keep the document as the actual data collection. I think I can make it work, but I'm not sure it's worth all of the effort. It's hard to explain with extended code samples over forums, but I'll try an approach. The attached zip file has several files in it; each file represents a concept I'm talking about and sometimes builds on the last.

Step1.vb
This demonstrates we can use inheritance for extensibility to support the notion of an XML document that is also a collection of data. Person is a specialized XmlElement that will always have the name "Person" and a text value that represents the person's full name. I added a convenience property FullName to make it easier to get at the name. PersonCollection is an XmlDocument with specialized behavior; its root node is always "People" and it has a specialized AddPerson() method that lets you add a new person without having to directly create a Person object. The application adds a couple of people and saves the file to demonstrate everything works.

Step2.vb
This is the most naive way to write a GetPeople() method that returns the child person elements. It calls ChildNodes() to get all the children of the root element, then converts them to Person objects. Except that cast fails. Why?

The in-memory representation of the Xml DOM is not just a collection of XmlNode items; it's likely a tree of some internal data types that makes it easier to support XPath queries. Methods like ChildNodes() read this data structure and produce XmlNode-derived objects that represent them. Since ChildNodes() wasn't programmed to know about Person objects, it doesn't return them.

(In case it's not obvious, Step2 isn't functional. I coded until I got painted into a corner.)

Step3.vb
You might decide to address this by overriding ChildNodes(). This has some problems; it must return XmlNodeList, which only has a protected constructor and is a read-only collection, so we're going to have to derive our own to use it. This requires us to implement our own backing store, indexer function, enumerator, and Count implementation. I cheated and just piggybacked List(Of T).

That's still not quite enough. Since the base ChildNodes returns objects that aren't Person objects, we need a way to get back the Person objects we've added. So it's best to just keep the list of people around as a private member.

That's not enough though; when we load the file, the document doesn't know that it's supposed to populate the PersonNodeList as it reads in people, so I had to add a step to the end of the Load() override to support this.

Step4.vb
Now we have a problem; we want to make it so that if we change a person in the collection the XML can be updated as well. This is sort of complicated.

Since when we call Load() the nodes we get as Person objects aren't the same nodes used to create the in-memory DOM, updating our Person objects has no real effect on the file. If we want to update a particular person, we have to make sure that we find and update the appropriate nodes in-memory. The easiest way to do this is to completely clear the DOM and rebuild it based on the current collection of Person objects. This file has a Save() override that does this.

That's as far as I'm going to go. There's some other use cases I'd like to walk through, but I think the point is made. Since you can't really put your items into the real DOM, trying to derive specialized node types is one-way. When you get stuff back out of the DOM, it will always be in terms of the .NET types. To get around this, you have to re-implement a lot of logic in such a way that it reads the results from the base class calls and massages them back to your types. If you want update-in-place, you have to either clobber the existing DOM or do extra work to find the correct node to update and synch it with the appropriate object. Depending on how intense your usage of the DOM features are, you might end up overriding 5-10 methods and putting a serious cramp on the efficiency of the class due to all of the extra mapping of data types.

It took me twice as long to figure out how to get around the pitfalls in this approach as it did for me to write my "map between business object and XML objects" approach. This wasn't just inexperience; each time I had to overload something and force it to output a different data type I had to do a small amount of research to decide the best approach. I think this is a heck of a lot more work for very little real benefit when compared to a more separated architecture.

I don't believe XmlReader/Writer would necessarily be a better choice for persisting the XML. I've typed enough for one thread, but if you're curious I can discuss the costs and benefits of each approach.
Attached Files
File Type: zip XmlInheritance.zip (3.2 KB, 1 views)
__________________
.NET Resources
My FAQ threads | Tutor's Corner | Code Library
I would bet money 2/3 of .NET questions are already answered in one of these three places.
Reply With Quote
Reply


Currently Active Users Viewing This Thread: 1 (0 members and 1 guests)
 
Thread Tools
Display Modes

Posting Rules
You may not post new threads
You may not post replies
You may not post attachments
You may not edit your posts

BB code is On
Smilies are On
[IMG] code is Off
HTML code is Off

Forum Jump

Advertisement:





Free Publications
The ASP.NET 2.0 Anthology
101 Essential Tips, Tricks & Hacks - Free 156 Page Preview. Learn the most practical features and best approaches for ASP.NET.
subscribe
Programmers Heaven C# School Book -Free 338 Page eBook
The Programmers Heaven C# School book covers the .NET framework and the C# language.
subscribe
Build Your Own ASP.NET 3.5 Web Site Using C# & VB, 3rd Edition - Free 219 Page Preview!
This comprehensive step-by-step guide will help get your database-driven ASP.NET web site up and running in no time..
subscribe
Problems extending the XMLElement(Visual Basic)
Problems extending the XMLElement(Visual Basic)
Problems extending the XMLElement(Visual Basic) Problems extending the XMLElement(Visual Basic)
Problems extending the XMLElement(Visual Basic)
Problems extending the XMLElement(Visual Basic)
Problems extending the XMLElement(Visual Basic) Problems extending the XMLElement(Visual Basic) Problems extending the XMLElement(Visual Basic) Problems extending the XMLElement(Visual Basic) Problems extending the XMLElement(Visual Basic) Problems extending the XMLElement(Visual Basic) Problems extending the XMLElement(Visual Basic)
Problems extending the XMLElement(Visual Basic)
Problems extending the XMLElement(Visual Basic)
 
Problems extending the XMLElement(Visual Basic)
Problems extending the XMLElement(Visual Basic)
 
-->