tags:

views:

80

answers:

4

Hi,

I have a sample xml file that looks like this:

<Books>
   <Category Genre="Fiction" BookName="book_name" BookPrice="book_price_in_$" />
   <Category Genre="Fiction" BookName="book_name" BookPrice="book_price_in_$" />
   <Category Genre="NonFiction" BookName="book_name" BookPrice="book_price_in_$" />
   <Category Genre="Children" BookName="book_name" BookPrice="book_price_in_$" />
</Books>  

I need to collect all book names and book prices and pass to some other method. Right now, i get all book names and book prices seperately into two different List<string> using the following command:

List<string>BookNameList = root.Elements("Category").Select(x => (string)x.Attribute("BookName")).ToList();
List<string>BookPriceList = root.Elements("Category").Select(x => (string)x.Attribute("BookPrice")).ToList();

I create a text file and send this back to the calling function (stroing these results in a text file is a requirement, the text file has two fields bookname and bookprice).

To write to text file is use following code:

for(int i = 0; i < BookNameList.Count; i++)
{
   //write BookNameList[i] to file
   // Write BookPriceList[i] to file
}

I somehow dont feel good about this approach. suppose due to any reason both lists of not same size. Right now i do not take that into account and i feel using foreach is much more efficient (I maybe wrong). Is it possible to read both the entries into a datastructure (having two attributes name and price) from LINQ? then i can easily iterate over the list of that datastructure with foreach.

I am using C# for programming.

Thanks,

[Edit]: Thanks everyone for the super quick responses, i choose the first answer which I saw.

+10  A: 

Selecting:

var books = root.Elements("Category").Select(x => new {
    Name = (string)x.Attribute("BookName"), 
    Price = (string)x.Attribute("BookPrice")
}).ToList();

Looping:

foreach (var book in books)
{
    // do something with
    // book.Name
    // book.Price
}
GWB
**I need to collect all book names and book prices and pass to some other method.** If I'm reading that right, your solution would require passing anonymous objects outside the scope they were created in (technically possible, but kind of an evil thing to do).
R0MANARMY
In that case, you should define a data structure and use it. Do you have the ability to edit the other function or is is set in stone? What is the signature of that function?
GWB
@GWB, thanks for the solution, this was what i was looking for, U r awesome!!!!!!!!!!!!!!!!!
dsilva
+1  A: 

You can do this with a single query and a foreach loop.

var namesAndPrices = from category in root.Elements("Category")
                     select new
                     {
                        Name = category.Attribute("BookName").Value,
                        Price = category.Attribute("BookPrice").Value
                     };

foreach (var nameAndPrice in namesAndPrices)
{
    // TODO: Output to disk
}
Jeff Yates
+1  A: 

To build on Jeff's solution, if you need to pass this collection into another function as an argument you can abuse the KeyValuePair data structure a little bit and do something along the lines of:

var namesAndPrices = from category in root.Elements("Category")
                     select new KeyValuePair<string, string>(
                        Name = category.Attribute("BookName").Value,
                        Price = category.Attribute("BookPrice").Value
                     );

// looping that happens in another function
// Key = Name
// Value = Price
foreach (var nameAndPrice in namesAndPrices)
{
    // TODO: Output to disk
}
R0MANARMY
Wouldn't the properties of a KeyValuePair be Key and Value, not Name and Price?
GalacticCowboy
@GalacticCowboy: Yes, they would be, I just wanted to point out that Key would hold the Name, and Value would hold the Price.
R0MANARMY
+2  A: 

I think you could make it more tidy by some very simple means.

A somewhat simplified example follows.

First define the type Book:

public class Book
{
   public Book(string name, string price)
   {
      Name = name;
      Price = price;
   }

   public string Name { get; set; }
   public string Price { get; set; } // could be decimal if we want a proper type.
}

Then project your XML data into a sequence of Books, like so:

var books = from category in root.Elements("Category")
            select new Book((string) x.Attribute("BookName"), (string) x.Attribute("BookPrice"));

If you want better efficiency I would advice using a XmlReader and writing to the file on every encountered Category, but it's quite involved compared to your approach. It depends on your requirements really, I don't think you have to worry about it too much unless speed is essential or the dataset is huge.

The streamed approach would look something like this:

using (var outputFile = OpenOutput())
using (XmlReader xml = OpenInput())
{
   try
   {
       while (xml.ReadToFollowing("Category"))
       { 
           if (xml.IsStartElement())
           {
               string name = xml.GetAttribute("BookName");
              string price = xml.GetAttribute("BookPrice");

              outputFile.WriteLine(string.Format("{0} {1}", name, price));
          }
      }
   }
   catch (XmlException xe)
   {
        // Parse error encountered. Would be possible to recover by checking
        // ReadState and continue, this would obviously require some 
        // restructuring of the code.
        // Catching parse errors is recommended because they could contain
        // sensitive information about the host environment that we don't
        // want to bubble up.
        throw new XmlException("Uh-oh");
   }       
}

Bear in mind that if your nodes have XML namespaces you must register those with the XmlReader through a NameTable or it won't recognize the nodes.

Skurmedel
+1 for recommending creating an actual type, but your example won't work as-is because Book doesn't have a (string, string) constructor defined.
GWB
select new Book() { Name = x.Attribute("BookName").Value, Price = x.Attribute("BookPrice").Value }; // or something like that...
GalacticCowboy
@GWB: It came awfully similar to yours lol. Yeah, I omitted the constructor for density, but it's true, it wouldn't so it looks a bit weird when I call it later down. Added a constructor. @GalacticCowboys: That would work as well.
Skurmedel