LINQ to XML - How to use XDocument the right way

2019-04-27 02:32发布

问题:

Now I will start off by saying this is indeed an assigment. I have however nearly finished it untill I ran into the Linq to XML syntax.

I have 2 classes: Track and CD now as part of the assigment I create a cd and then added some tracks to it. After searching for lots of tutorials that explained perfectly how to go from xml to objects I just cannot seem to get this working (objects to xml).

I currently have:

//My list of cds
List<CD> cds = new List<CD>();
//Make a new CD and add some tracks to it
CD c1 = new CD("Awake","Dream Theater");
Track t1 = new Track("6:00", "Dream Theater", new TimeSpan(00, 05, 31));
Track t2 = new Track("Caught in a Web", "Dream Theater", new TimeSpan(00, 05, 28));
Track t3 = new Track("Innocence Faded", "Dream Theater", new TimeSpan(00, 05, 34));
c1.addTrack(t1);
c1.addTrack(t2);
c1.addTrack(t3);
cds.Add(c1);

//Make another cd and add it
CD c2 = new CD("Second cd","TestArtist");
Track t4 = new Track("TrackForSecond","TestArtist",new TimeSpan(00,13,37));
c2.addTrack(t4);
cds.add(c2);

Now this is what gets me the objects I need to put into XML. The to XML part is:

XDocument xmlOutput = new XDocument (
     new XDeclaration("1.0","utf-8","yes"),
     (from cl in cds orderby cl.getArtist()
        select new XElement("cd",  /*From new to the end of this is the error*/
            (
               from c in cds
                   select new XAttribute("artist",c.getArtist())
            ),
            (
               from c in cds
                   select new XAttribute("name", c.getTitle())
            ),
            new XElement("tracks",
               (
                   from t in c1.getTracks()
                       select new XElement("track",
                           new XElement("artist",t1.getArtist()),    
                           new XElement("title",t1.getTitle()),
                           new XElement("length",t1.getLength())
                       )          
               )                    
            )
        )
    )                   
);
Console.WriteLine(xmlOutput);

This works great (gets me the result I need!) for just 1 cd. When I decide to add another cd it shows:

An unhandled exception of type 'System.InvalidOperationException' occurred in System.Xml.Linq.dll
Duplicate Attribute (cd)

Which is pointing at the XDocument. Aside from this not working it feels pretty stupid (from c in cds x2) but whatever I try I cannot seem to stop this syntax from hating me:

(
from c in cds
   select new XAttribute("artist",c.getArtist()),    
   select new XAttribute("name", c.getTitle()) //No not happening!
),

Would be very happy with any help you can provide!

回答1:

First, I suggest you to use properties and C# style naming for methods. Here is how your classes could be refactored:

public class CD
{
    private readonly List<Track> _tracks = new List<Track>();

    public CD(string artist, string title)
    {
        Artist = artist;
        Title = title;
    }

    public string Artist { get; private set; }
    public string Title { get; private set; }

    public  IEnumerable<Track> Tracks
    {
        get { return _tracks; }
    } 

    public void AddTrack(Track track)
    {
        _tracks.Add(track);
    }

    public CD WithTrack(string title, TimeSpan length)
    {
        AddTrack(new Track(Artist, title, length));
        return this;
    }
}

This is Value Object class - private setters does not allow to change property values outside of this class. And here is class for track:

public class Track
{
    public Track(string artist, string title, TimeSpan length)
    {
        Artist = artist;
        Title = title;
        Length = length;
    }

    public string Artist { get; set; }
    public string Title { get; private set; }
    public TimeSpan Length { get; private set; }
}

Now you can use Fluent API to create collection of CDs:

List<CD> cds = new List<CD>
    {
        new CD("Awake", "Dream Theater")
            .WithTrack("6:00", new TimeSpan(00, 05, 31))
            .WithTrack("Caught in a Web", new TimeSpan(00, 05, 28))
            .WithTrack("Innocence Faded", new TimeSpan(00, 05, 34)),
        new CD("Second cd", "TestArtist")
            .WithTrack("TrackForSecond", new TimeSpan(00, 13, 37))
    };

And here is XML creation:

var xDoc = new XDocument(
    new XDeclaration("1.0", "utf-8", "yes"),
    new XElement("cds",
          from cd in cds
          orderby cd.Artist
          select new XElement("cd",
               new XAttribute("artist", cd.Artist),
               new XAttribute("name", cd.Title),
               from t in cd.Tracks
               select new XElement("track",
                     new XElement("artist", t.Artist),
                     new XElement("title", t.Title),
                     new XElement("length", t.Length)));

You had several problems here - missing root node, and enumerating over all CDs on each iteration.



回答2:

There are a few problems with your XDocument construction.

  1. There must be exactly one root element in an XDocument. Your statements are constructing a root element for each CD.
  2. You have strange nested loops in your LINQ. First you're ordering the CDs by artist, and then you're iterating again over the entire CD collection when producing the artist and name attributes. You want to produce these attributes from the "current" CD.
  3. You're using "c1" and "t1" in your LINQ instead of the iteration variables "cl" and "t".

Try this (forgive me for turning your getters/setters into properties:

var xmlOutput = new XDocument(
    new XDeclaration("1.0", "utf-8", "yes"),
    new XElement(
        "cds",
        from cd in cds
        orderby cd.Artist.ToUpperInvariant()
        select new XElement(
            "cd",
            new XAttribute("title", cd.Title),
            new XAttribute("artist", cd.Artist),
            new XElement(
                "tracks",
                from t in cd.Tracks
                select new XElement(
                    "track",
                    new XAttribute("artist", t.Artist),
                    new XAttribute("title", t.Title),
                    new XAttribute("length", t.Length))))));


回答3:

select new XElement("cd",  /*From new to the end of this is the error*/
        (
           from c in cds
               select new XAttribute("artist",c.getArtist())
        ),

This will create an element named cd (which is good), but then attempt to add one artist attribute for every CD in the collection, which is almost certainly not what you want, and is the cause of the problem.

That is, this code attempts to make xml like this:

<cd
    artist="Dream Theater"
    artist="TestArtist"
// the later stuff

which you probably know is illegal xml.

The idea you are missing is that here:

 (from cl in cds orderby cl.getArtist()

you are using LINQ to do the loop for you - withint the scope of this from, c1 is one particular CD from the collection. So you don't need to do from c in cds within this, because you've already got the CD object you need:

    select new XElement("cd",  /*From new to the end of this is the error*/
        select new XAttribute("artist",c1.getArtist()),
        select new XAttribute("name", c1.getTitle()),
        new XElement("tracks",
           (
               from t in c1.getTracks()
                   select new XElement("track",
                       new XElement("artist",t1.getArtist()),    
                       new XElement("title",t1.getTitle()),
                       new XElement("length",t1.getLength())
                   )          
           )                    
        )
    )
)  

You already have the right idea in selecting over c1.getTracks(); apply the same idea in creating the attributes.