XML
LINQ
Børre Stenseth
Moduler>LINQ>XML

LINQ og XML

Hva
LINQ og XML

LINQ er en interessant utvidelse av C# (versjon 3), LINQ/C# Learning Guide [1] , 101 Samples [2] og er integrert i Visual Studio. I korthet betyr dette at vi har fått språk elementer der vi kan formulere SQL-lignende setninger som en del av selve C#-språket.

LINQ er brukbart til å avsøke flere typer strukturer. Eksempler er databaser, XML og objektlister. Vi skal se på noen enkle eksempler i denne modulen.

Eksempel 1

Vi skal lese inn en XML-fil, bygge en liste av objekter og så skal vi plukke til ut av lista ved hjelp av LINQ.

Dataene tar vi fra en liste av vinbeskrivelser som beskrevet i modulen Noen datasett.

sample1

Vi etablerer et XmlDocument wineDoc fra dataene og vil legge alle vinene inn i en liste: List<wine> WineList. Hver vin er beskrevet med følgende klasse:

class wine : Object
{
    String wname;
    String wcountry;
    String wdice;
    String wdescription;
    String wtype;
    public wine(String name,String country,String dice,
               String description, String type)
    {
        wname = name;
        wcountry = country;
        wdice = dice;
        wdescription = description;
        wtype = type;
    }
    public String name { get { return wname; } }
    public String country { get { return wcountry; } }
    public String dice { get { return wdice; } }
    public String type { get { return wtype; } }
    public String description { get { return wdescription; } }
    public override string ToString()
    {
        return wname;
    }
}

Selve overføringen fra XML til objektliste gjøre ved hjelp av enkel xpath og vanlig DOM-programmering:

private void Form1_Load(object sender, EventArgs e)
{
    // fill up the winelist
    wineDoc = new XmlDocument();
    try
    {
        wineDoc.Load(WineSource);
        XmlNodeList wines = wineDoc.SelectNodes("//wine");
        WineList = new List<wine>(wines.Count);
        foreach (XmlElement wine in wines)
            WineList.Add(new wine(
                wine.GetAttribute("name"),
                wine.GetElementsByTagName("country")[0].InnerText,
                wine.GetElementsByTagName("dice")[0].InnerText,
                wine.GetElementsByTagName("description")[0].InnerText,
                wine.GetElementsByTagName("type")[0].InnerText
                ));
        
        labelMessage.Text = 
            Convert.ToString(wines.Count) +" viner lagt inn";
    }
    catch (Exception ex)
    {
        labelMessage.Text = ex.Message;
    }
}

Når vi nå har etablert objektlista kan vi formulere søk i lista. Eksempelet demonstrerer to slike søk ved hjelp av LINQ.

Portugisiske viner

Oppgaven vi skal løse er å velge ut alle vinder med property country lik "Portugal". Det vi gjør er altså å lage et sett, en liste med stringer. Når vi har utført selve søket, løper vi gjennom det vi fant og legger det i listeboksen.

private void buttonPortugal_Click(object sender, 
    EventArgs e)
{
    listBox1.Items.Clear();
    // Collect and show names of Portugese wines
    var wines =
        from w in WineList
        where w.country == "Portugal" 
        select new{N=w.name};
    foreach (var wine in wines)
        listBox1.Items.Add(wine.N);
}

De beste portugisiske vinene

Det er to forskjeller fra oppgaven ovenfor. For det første har vi et kombinert utvalgskriterium (både Portugal og sekser på terningen). For det andre velger vi ut hele objektet, vi konstruerer ikke en string-liste. Etter søket sitter vi altså igjen med en liste (med referanser til) vinobjekter. Så setter vi disse objektene inn i listeboksen. Dette betyr at når vi klikker i lista, vil vi få tilgang til selve objektet , og kan om ønskelig hente fra andre egenskaper ved vinen enn navnet. Hvert vinobjekt blir representert ved navnet i lista fordi vi har redefinert objectmetoden ToString() for vinobjekter til å levere vinens navn.

private void buttonBest_Click(object sender, 
    EventArgs e)
{
    listBox1.Items.Clear();
    // Collect and show names of Portugese wines 
    // with dice 6
    var wines =
        from w in WineList
        where w.country == "Portugal" && w.dice == "6"
        select w;
    foreach (var wine in wines)
        listBox1.Items.Add(wine);
}

Koden er i sin helhet slik:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Xml;
namespace list1
{
    public partial class Form1 : Form
    {
        const string WineSource=
            "http://www.ia.hiof.no/~borres/commondata/vin/allwines.xml";
        XmlDocument wineDoc;
        List<wine> WineList;
        public Form1()
        {
            InitializeComponent();
        }

#region makelist
        private void Form1_Load(object sender, EventArgs e)
        {
            // fill up the winelist
            wineDoc = new XmlDocument();
            try
            {
                wineDoc.Load(WineSource);
                XmlNodeList wines = wineDoc.SelectNodes("//wine");
                WineList = new List<wine>(wines.Count);
                foreach (XmlElement wine in wines)
                    WineList.Add(new wine(
                        wine.GetAttribute("name"),
                        wine.GetElementsByTagName("country")[0].InnerText,
                        wine.GetElementsByTagName("dice")[0].InnerText,
                        wine.GetElementsByTagName("description")[0].InnerText,
                        wine.GetElementsByTagName("type")[0].InnerText
                        ));
                
                labelMessage.Text = 
                    Convert.ToString(wines.Count) +" viner lagt inn";
            }
            catch (Exception ex)
            {
                labelMessage.Text = ex.Message;
            }
        }
#endregion makelist
#region button1
        private void buttonPortugal_Click(object sender, 
            EventArgs e)
        {
            listBox1.Items.Clear();
            // Collect and show names of Portugese wines
            var wines =
                from w in WineList
                where w.country == "Portugal" 
                select new{N=w.name};
            foreach (var wine in wines)
                listBox1.Items.Add(wine.N);
        }
#endregion button1
#region button2
        private void buttonBest_Click(object sender, 
            EventArgs e)
        {
            listBox1.Items.Clear();
            // Collect and show names of Portugese wines 
            // with dice 6
            var wines =
                from w in WineList
                where w.country == "Portugal" && w.dice == "6"
                select w;
            foreach (var wine in wines)
                listBox1.Items.Add(wine);
        }
#endregion button2
#region listselect
        private void listBox1_SelectedIndexChanged(object sender, 
            EventArgs e)
        {
            try
            {
                // assume complete wine in list
                wine w = (wine)listBox1.SelectedItem;
                MessageBox.Show("type: "+w.type+"\r\n"+w.description);
            }
            catch (Exception ex)
            {
                // only winename in list
                MessageBox.Show((String)listBox1.SelectedItem);
            }
        }
    }
#endregion listselect
#region onewine
    class wine : Object
    {
        String wname;
        String wcountry;
        String wdice;
        String wdescription;
        String wtype;
        public wine(String name,String country,String dice,
                   String description, String type)
        {
            wname = name;
            wcountry = country;
            wdice = dice;
            wdescription = description;
            wtype = type;
        }
        public String name { get { return wname; } }
        public String country { get { return wcountry; } }
        public String dice { get { return wdice; } }
        public String type { get { return wtype; } }
        public String description { get { return wdescription; } }
        public override string ToString()
        {
            return wname;
        }
    }
#endregion onewine
}

Eksempel 2

Vi bruker de samme dataene og skal gjøre bare en enkel endring fra eksempel 1. I eksempel 1 etablerte vi objektlista ved hjelp av xpath of dom-programmering. Nå skal vi forsøke å etablere lista ved hjelp av LINQ.

Det eneste vi endrer er koden for å laste inn XML-strukturen:

private void Form1_Load(object sender, EventArgs e)
{
    // fill up the winelist
    XDocument xdoc = new XDocument();
    try
    {
        xdoc = XDocument.Load(WineSource);
        WineList = new List<wine>();
        var wines = from v in xdoc.Descendants("wine")
                    select new
                    {
                        name = (string)v.Attribute("name"),
                        country = (string)v.Element("country"),
                        dice = (string)v.Element("dice"),
                        description = (string)v.Element("description"),
                        type = (string)v.Element("type")
                    };
        foreach (var w in wines)
            WineList.Add(new wine(w.name, w.country, 
                w.dice, w.description, w.type));
        labelMessage.Text =
            Convert.ToString(WineList.Count) + " viner lagt inn";
    }
    catch (Exception ex)
    {
        labelMessage.Text = ex.Message;
    }
}

Merk at her bruker vi ikke et XmlDocument, men et XDodument. XDocument er et format som gjør XML-filer søkbare med LINQ.

Eksempel 3

Vi bruker de samme dataene. Nå skal vi gjøre en kraftig forenkling. Vi kutter ut hele objektlista og bruker XDocumentet direkte som søkestruktur.

Etableringen av strukturen blir nå slik:

private void Form1_Load(object sender, EventArgs e)
{
    // fill up the winelist
    xdoc = new XDocument();
    try
    {
        xdoc = XDocument.Load(WineSource);
        labelMessage.Text =
            Convert.ToString(xdoc.Descendants("wine").Count())
            + " viner lagt inn";
    }
    catch (Exception ex)
    {
        labelMessage.Text = ex.Message;
    }
}

De to søkerutinene blir slik

Portugisiske viner

private void buttonPortugal_Click(object sender, EventArgs e)
{
    listBox1.Items.Clear();
    // Collect and show names of Portugese wines
    var wines =
        from w in xdoc.Descendants("wine")
        where (string)w.Element("country") == "Portugal"
        select new { N = (string)w.Attribute("name") };
    foreach (var w in wines)
        listBox1.Items.Add(w.N);
}

Vi ser at vi søker direkte i XDocument xdoc.

De beste portugisiske vinene

Her har vi et lite problem. Vi ønsker at når brukeren klikker i lista skal han få opp flere egenskaper ved vinen enn selve navnet. Vi løser dette med å lagre (referanse til) et wine-objekt i lista. Vi lage altså slike vinlister bare ved behov.

private void buttonBest_Click(object sender, EventArgs e)
{
    listBox1.Items.Clear();
    // Collect and show names of Portugese wines 
    // with dice 6
    var wines =
        from w in xdoc.Descendants("wine")
        where (string)w.Element("country") == "Portugal"
        && (string)w.Element("dice") == "6"
        select w;
    foreach (var w in wines)
        listBox1.Items.Add(new wine(w));
    foreach (var w in wines)
        listBox1.Items.Add(w);
}

Listeklikket blir da som tidligere:

private void listBox1_SelectedIndexChanged(object sender, EventArgs e)
    {
        try
        {
            // assume complete wine in list
            XElement w = ((wine)listBox1.SelectedItem).myElement;
            MessageBox.Show("type: " + w.Element("type").Value +
                            "\r\n" +
                            "description: " + w.Element("description").Value);
        }
        catch (Exception ex)
        {
            // only winename in list
            MessageBox.Show((String)listBox1.SelectedItem);
        }
    }
}

Koden er i sin helhet slik:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Xml.Linq;
namespace linq2
{
    public partial class Form1 : Form
    {
        const string WineSource =
            "c:\\web\\commondata\\vin\\allwines.xml";
        XDocument xdoc;
        public Form1()
        {
            InitializeComponent();
        }
        #region load
        private void Form1_Load(object sender, EventArgs e)
        {
            // fill up the winelist
            xdoc = new XDocument();
            try
            {
                xdoc = XDocument.Load(WineSource);
                labelMessage.Text =
                    Convert.ToString(xdoc.Descendants("wine").Count())
                    + " viner lagt inn";
            }
            catch (Exception ex)
            {
                labelMessage.Text = ex.Message;
            }
        }
        #endregion load
        #region button1
        private void buttonPortugal_Click(object sender, EventArgs e)
        {
            listBox1.Items.Clear();
            // Collect and show names of Portugese wines
            var wines =
                from w in xdoc.Descendants("wine")
                where (string)w.Element("country") == "Portugal"
                select new { N = (string)w.Attribute("name") };
            foreach (var w in wines)
                listBox1.Items.Add(w.N);
        }
        #endregion button1
        #region button2
        private void buttonBest_Click(object sender, EventArgs e)
        {
            listBox1.Items.Clear();
            // Collect and show names of Portugese wines 
            // with dice 6
            var wines =
                from w in xdoc.Descendants("wine")
                where (string)w.Element("country") == "Portugal"
                && (string)w.Element("dice") == "6"
                select w;
            foreach (var w in wines)
                listBox1.Items.Add(new wine(w));

            foreach (var w in wines)
                listBox1.Items.Add(w);
        }
        #endregion button2
        #region select
        private void listBox1_SelectedIndexChanged(object sender, EventArgs e)
        {
            try
            {
                // assume complete wine in list
                XElement w = ((wine)listBox1.SelectedItem).myElement;
                MessageBox.Show("type: " + w.Element("type").Value +
                                "\r\n" +
                                "description: " + w.Element("description").Value);
            }
            catch (Exception ex)
            {
                // only winename in list
                MessageBox.Show((String)listBox1.SelectedItem);
            }
        }
    }
#endregion select
#region wine
    class wine : Object
    {
        XElement theWine;
        public wine(XElement w)
        { theWine = w; }
        public XElement myElement { get { return theWine; } }
        public override string ToString()
        { return (String)theWine.Attribute("name"); }
    }
#endregion wine
}
Referanser
  1. LINQ/C# Learning GuideSheets, Brent2007TheServerSidewww.theserverside.net/tt/articles/showarticle.tss?id=CsharpLINQLearningGuide200714-03-2010
  1. 101 Samples2009Microsoftmsdn.microsoft.com/en-us/vcsharp/aa336746.aspx14-03-2010
  • Eksempel1:
    https://svn.hiof.no/svn/psource/Csharpspikes/linq1
  • Eksempel2:
    https://svn.hiof.no/svn/psource/Csharpspikes/linq2
  • Eksempel3:
    https://svn.hiof.no/svn/psource/Csharpspikes/linq3
Vedlikehold

B.Stenseth, jan 2008

(Velkommen) Moduler>LINQ>XML (Database)