XML
Databinding
XSLT
Børre Stenseth
Moduler>Websites>Verden

Land i verden

Hva
screen1
Oppslag om land i verden

I denne modulen skal vi lage en enkel vevside som lar oss finne fram noe informasjon om land i verden. Utgangspunktet er data fra geonames [1] De XML-dataene vi skal bruke er referert på modulen Noen datasett, og har adresse:
http://www.it.hiof.no/~borres/commondata/geonames/land.xml.
XML-fila ser slik ut: land.xml

Eksempel 1

Vi skal lage en enkel vevside med en liste som inneholder alle land, og når vi klikker på et land skal vi få fram hovedstade, innbyggertallet og flateinnholdet.

http://donau.hiof.no/borres/dn/verden1http://donau.hiof.no/borres/dn/verden1

Med DOM-programmering er det ganske greitt å skrive kode som:

  • Fyller listeboksen.
  • Finner fram de relevante dataene gitt valgt land

Vi skal imidlertid forsøke å sette opp innholdet i listeboksen ved hjelp av databinding. Utfordringen blir å binde en XML-datakilde mot en listeboks. Vi vet at anatomien i et select-element på en vevside er slik at vi kan opgi både en verdi og en tekst som vises:

...
<option value="3041565">Andorra</option>
<option value="1149361">Afghanistan</option>
<option value="3576396">Antigua and Barbuda</option>
...

I .Net-terminologi heter de to egenskapene ved linjene i en listeboks henholdsvis: DataValueField og DataTextField. Vi bruker som vist ovenfor geonameID som DataValueField (fordi det er entydig) og countryName som DataTextField. geonameId og countryName er elementer i XML-fila som er barn av elementet country. Problemet blir altså hvordan skal vi sette oppdatabindingen slik at vi får bundet innholdet i disse to elementene til de respektive feltene.

Når vi binder XML-data i .Net kan vi oppgi attributter som om de var egenskaper ved en node, men vi har ingen direkte måte å angi elementbarn. En kunne tenke seg at et xpath-uttrykk kunne gjøre jobben, men det får vi ikke oppgi xpath-identifikasjon som verdi for verken DataValueField eller DataTextField. Løsningen er å transformere XML-fila slik at vi får en struktur der de relevante verdiene er plassert som attributter, altså å "flate ut" fila. Det kan vi gjøre og det er lagt opp til denne løsningen i beskrivelsen av en listeboks. De aktuelle elementene i aspx - fila som beskriver databindingen er slik:

<asp:ListBox 
    ID="ListBox2" 
    runat="server" 
    DataSourceID="XmlDataSource2" 
    Width="150px" 
    Height="287px"  
    DataTextField="countryName"  
    DataValueField="geonameId" 
    onselectedindexchanged="ListBox2_SelectedIndexChanged"
    AutoPostBack="True">
</asp:ListBox>

<asp:XmlDataSource 
    ID="XmlDataSource2" 
    runat="server" 
    DataFile=
    "http://www.it.hiof.no/~borres/commondata/geonames/land.xml"
    XPath="//country" 
    TransformFile="flatten.xslt">
</asp:XmlDataSource>

ListBox2 kopler direkte til XMLDataSource2 og inneholder direkte henvisninger til countryName og geonameId, som altså skal kunne plukkes direkte datakilden.

Datakilden, XMLDataSouce2, har for det første en uri til kilden, DataFile. Den har videre et xpath-uttrykk som plukker ut country-elementer. Og endelig har den altså en TransformFile (flatten.xslt) som skal sørge for at de country-elementene vi velger ut skal ha relevante datafelter som attributter. Utfordringen blir å skrive fila flatten.xslt.

Det er greitt nok å lage en transformasjon for vårt eksempel. Vi kan f.eks. gjøre slik:

<?xml version="1.0" encoding="ISO-8859-1"?>
<xsl:stylesheet version="1.0" 
                        xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:output method="xml" 
                     version="1.0" 
                     encoding="UTF-8" 
                     indent="yes"/>
<xsl:template match="/">
<geonames>
    <xsl:apply-templates select="//country"/>
</geonames>
</xsl:template>
<xsl:template match="country">
    <xsl:element name="country">
        <xsl:attribute name="geonameId">
            <xsl:value-of select="geonameId"/>
        </xsl:attribute>
        <xsl:attribute name="countryName">
            <xsl:value-of select="countryName"/>
        </xsl:attribute>
    </xsl:element>
</xsl:template>
</xsl:stylesheet>

Resultatet blir slik: ownflattenout.xml

Men det går an å skrive en generisk fil som gjør denne jobben når vi trenger den. Kilden er Microsoft Forum, MSDN, [2] :

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0"
                xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:output method="xml"
              version="1.0"
              encoding="UTF-8"
              indent="yes"/>
  
  <xsl:template match="@* | node()">
    <xsl:copy>
      <xsl:apply-templates select="@* | node()" />
    </xsl:copy>
  </xsl:template>
  <xsl:template match="*[*[not(*)]]">
    <xsl:copy>
      <xsl:apply-templates select="@*" />
      <xsl:apply-templates select="*" 
                           mode="element-to-attribute"/>
    </xsl:copy>
  </xsl:template>
  <xsl:template match="*" mode="element-to-attribute">
    <xsl:attribute name="{name()}" 
                   namespace="{namespace-uri()}">
      <xsl:value-of select="." />
    </xsl:attribute>
  </xsl:template>
</xsl:stylesheet>

Resultatet blir slik: flattenout.xml

Default.aspx - fila i sin helhet er slik:

<%@ Page Language="C#" AutoEventWireup="true"  CodeFile="Default.aspx.cs" 
Inherits="_Default" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" 
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title>Du store verden</title>
</head>
<body>
<h1>Du store verden</h1>
    <form id="form1" runat="server">
    <table cellpadding="10px" cellspacing="0px" border="0px">
    <tr>
    <td valign="top">
        <asp:ListBox 
            ID="ListBox2" 
            runat="server" 
            DataSourceID="XmlDataSource2" 
            Width="150px" 
            Height="287px"  
            DataTextField="countryName"  
            DataValueField="geonameId" 
            onselectedindexchanged="ListBox2_SelectedIndexChanged"
            AutoPostBack="True">
        </asp:ListBox>
        
        <asp:XmlDataSource 
            ID="XmlDataSource2" 
            runat="server" 
            DataFile=
            "http://www.ia.hiof.no/~borres/commondata/geonames/land.xml"
            XPath="//country" 
            TransformFile="flatten.xslt">
        </asp:XmlDataSource>
    </td>
    <!-- ***************** -->
    <td valign="top">
    <h2>
        <asp:Literal ID="LiteralCountry" runat="server"></asp:Literal>
    </h2>
    <div>
        <asp:Label ID="LabelHovedstad" 
                   runat="server" 
                   Text="Hovedstad:" 
                   Width="100px" 
                   Visible="false"/>
        <span style="margin-left:10px;font-weight:bold">
        <asp:Literal ID="LiteralHovedstad" runat="server" Text="">
        </asp:Literal>
        </span>
    </div>
    <div>
        <asp:Label ID="LabelBefolkning" 
                   runat="server" 
                   Text="Innbyggere:" 
                   Width="100px" 
                   Visible="false"/>
        <span style="margin-left:10px;font-weight:bold">
        <asp:Literal ID="LiteralBefolkning" runat="server" Text="">
        </asp:Literal>
        </span>
    </div>
    <div>
        <asp:Label ID="LabelAreal" 
                   runat="server" 
                   Text="Areal (km2):" 
                   Width="100px" 
                   Visible="False"/>
        <span style="margin-left:10px;font-weight:bold">
        <asp:Literal ID="LiteralAreal" runat="server" Text="">
        </asp:Literal>
        </span>
    </div>
    </td>
    </tr>
    </table>
    </form>
</body>
</html>

Logikken for å plukke fram de relevante dataene for et land er rimelig grei DOM-koding i Default.aspx.cs fila. Merk at her jobber vi mot den opprinnelige XML-file, ikke den som er "flatet ut".

protected XmlDocument getDoc()
{
    XmlDocument doc = new XmlDocument();
    try
    {
        doc.Load("http://www.ia.hiof.no/~borres/commondata/geonames/land.xml");
        return doc;
    }
    catch (Exception ex)
    {
        return null;
    }
}
protected void ListBox2_SelectedIndexChanged(object sender, EventArgs e)
{
    XmlDocument doc = getDoc();
    if (doc != null)
    {
        LiteralCountry.Text = (String)ListBox2.SelectedItem.Text;
        String geocode = (String)ListBox2.SelectedItem.Value;
         // pick up country element
        String myXP=String.Format("//country[geonameId='{0}']",geocode);
        XmlElement country=(XmlElement)(doc.SelectSingleNode(myXP));
        if (country != null)
        {
            // get the values we want and display them
            LabelHovedstad.Visible = true;
            LabelBefolkning.Visible = true;
            LabelAreal.Visible = true;
            LiteralHovedstad.Text = 
                ((XmlElement)(country.SelectSingleNode("capital"))).InnerText;
            LiteralBefolkning.Text = 
                ((XmlElement)(country.SelectSingleNode("population"))).InnerText;
            LiteralAreal.Text=
                ((XmlElement)(country.SelectSingleNode("areaInSqKm"))).InnerText;
        }
    }
}

Eksempel 2

screen2

Vi ta utgangspunkt i eksempel 1 og skal legge til kartinformasjon, dvs. vi skal vise fram et kart over det landet vi velger.

Vi bruker GoogleMaps googlemaps [3] . Merk at for å bruke Google Maps må du skaffe deg en ID som gjelder for et domene.

De endringene vi trenger å gjøre er følgende:

  • Default.aspx. Vi må endrer aspx-fila slik at vi får plass til å vise kartet og vi må plassere de fire dataelementene som beskriver kartutsnittet. Vi må dessuten legge inn et kall på et JavaScript som viser selve kartet, og en referanse til Google Maps kart bibliotek.
  • Default.aspx.cs. Vi må endre cs-fila slik at vi i tillegg til hovedstad, innbyggere og areal, plukker ut de fire koordinatverdiene som beskriver landets utstrekning, hjørenene. Disse fire verdiene plasseres på aspx-sida slik at de kan nås ved JavaScripting.
  • JavaScript. Vi må skrive en funksjon i JavaScript som tar de aktuelle karthjørnene og legger ut selve kartet.
http://donau.hiof.no/borres/dn/verden2http://donau.hiof.no/borres/dn/verden2

default.aspx

Vi sette av et div-element til selve kartet:

<div id="map_canvas" 
     style="width: 400px; height: 400px;border:solid; margin-top:10px">
</div>

Hjørnene på kartutsnittet plasserer vi slik:

<div style="display:none">
    <asp:Label ID="bBoxWest" runat="server" Text="0"/>
    <asp:Label ID="bBoxNorth" runat="server" Text="0"/>
    <asp:Label ID="bBoxEast" runat="server" Text="0"/>
    <asp:Label ID="bBoxSouth" runat="server" Text="0"/>
</div>

asp.Label-elementer manifestrere seg som span-elementer på den endelige HTML-siden. Merk at koordinatene ligger inne i et div-element som ikke er synlig på HTML-siden.

I headeren på fila legger vi inn følgende:


<script code="text/javascript"
        src="http://maps.google.com/maps?file=api&amp;v=2&amp;key=...">
</script>
<script code="text/javascript" 
        src="maps.js">
</script>


Vi kopler til to script: Google Maps bibliotek og min egen scriptfil, se nedenfor. De tre ... erstatter av layoutmessige årsaker min Google nøkkel.

Endelig endrer vi fila slik at hver gang den lastes vises kartet fram på nytt, med de valgte dataene. Dette kan vi gjøre siden vår løsning er basert på en full "round-trip". showmap er min funksjon, mens GUnload er i Google-biblioteket.

<body onload="showmap()" onunload="GUnload()">

Fila i sin helhet

<%@ Page Language="C#" AutoEventWireup="true"  CodeFile="Default.aspx.cs" 
Inherits="_Default" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" 
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title>Du store verden</title>
     <!-- http://www.ia.hiof.no/~borres/ 
    <script type="text/javascript"
            src="http://maps.google.com/maps?file=api&amp;v=2&amp;key=..googlekey..">
    </script>
    -->
    <!-- http://donau.hiof.no/borres/ -->
    <script type="text/javascript"
            src="http://maps.google.com/maps?file=api&amp;v=2&amp;key=..googlekey2..">
    </script>
    
    <script type="text/javascript" 
            src="maps.js">
    </script>
</head>
<body onload="showmap()" onunload="GUnload()">
<h1>Du store verden</h1>
    <form id="form1" runat="server">
    <table cellpadding="0px" cellspacing="10px" border="0px">
    <tr>
    <td valign="top">
        <asp:ListBox 
            ID="ListBox2" 
            runat="server" 
            DataSourceID="XmlDataSource2" 
            Width="150px" 
            Height="500px"  
            DataTextField="countryName"  
            DataValueField="geonameId" 
            onselectedindexchanged="ListBox2_SelectedIndexChanged"
            AutoPostBack="True">
        </asp:ListBox>
        
        <asp:XmlDataSource 
            ID="XmlDataSource2" 
            runat="server" 
            DataFile=
            "http://www.ia.hiof.no/~borres/commondata/geonames/land.xml"
            XPath="//country" 
            TransformFile="flatten.xslt">
        </asp:XmlDataSource>
    </td>
    <!-- ***************** -->
    <td valign="top">
    <div style="font-size:16px;font-weight:bold">
        <asp:Literal ID="LiteralCountry" runat="server"></asp:Literal>
    </div>
    <div>
        <asp:Label ID="LabelHovedstad" 
                   runat="server" 
                   Text="Hovedstad:" 
                   Width="100px" 
                   Visible="false"/>
        <span style="margin-left:10px;font-weight:bold">
        <asp:Literal ID="LiteralHovedstad" runat="server" Text="">
        </asp:Literal>
        </span>
    </div>
    <div>
        <asp:Label ID="LabelBefolkning" 
                   runat="server" 
                   Text="Innbyggere:" 
                   Width="100px" 
                   Visible="false"/>
        <span style="margin-left:10px;font-weight:bold">
        <asp:Literal ID="LiteralBefolkning" runat="server" Text="">
        </asp:Literal>
        </span>
    </div>
    <div>
        <asp:Label ID="LabelAreal" 
                   runat="server" 
                   Text="Areal (km2):" 
                   Width="100px" 
                   Visible="False"/>
        <span style="margin-left:10px;font-weight:bold">
        <asp:Literal ID="LiteralAreal" runat="server" Text="">
        </asp:Literal>
        </span>
    </div>
    
    <div id="map_canvas" 
         style="width: 400px; height: 400px;border:solid; margin-top:10px">
    </div>
    
    </td>
    </tr>
    </table>
    <div style="display:none">
        <asp:Label ID="bBoxWest" runat="server" Text="0"/>
        <asp:Label ID="bBoxNorth" runat="server" Text="0"/>
        <asp:Label ID="bBoxEast" runat="server" Text="0"/>
        <asp:Label ID="bBoxSouth" runat="server" Text="0"/>
    </div>
    </form>
</body>
</html>

Default.aspx.cs

Her er det bare noen linjer kode som skal endres fra eksempel 1. Metdoen som plukker opp valg i listeboksen trenger å gjøre følgende:

bBoxEast.Text=
    ((XmlElement)(country.SelectSingleNode("bBoxEast"))).InnerText;
bBoxNorth.Text =
    ((XmlElement)(country.SelectSingleNode("bBoxNorth"))).InnerText;
bBoxSouth.Text =
    ((XmlElement)(country.SelectSingleNode("bBoxSouth"))).InnerText;
bBoxWest.Text =
    ((XmlElement)(country.SelectSingleNode("bBoxWest"))).InnerText;

Fila i sin helhet

using System;
using System.Configuration;
using System.Data;
using System.Linq;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.HtmlControls;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Xml.Linq;
using System.Xml;
public partial class _Default : System.Web.UI.Page 
{
    protected void Page_Load(object sender, EventArgs e)
    {
    }
    #region select
    protected XmlDocument getDoc()
    {
        XmlDocument doc = new XmlDocument();
        try
        {
            doc.Load("http://www.ia.hiof.no/~borres/commondata/geonames/land.xml");
            return doc;
        }
        catch (Exception ex)
        {
            return null;
        }
    }
    protected void ListBox2_SelectedIndexChanged(object sender, EventArgs e)
    {
        XmlDocument doc = getDoc();
        if (doc != null)
        {
            LiteralCountry.Text = (String)ListBox2.SelectedItem.Text;
            String geocode = (String)ListBox2.SelectedItem.Value;
             // pick up country element
            String myXP=String.Format("//country[geonameId='{0}']",geocode);
            XmlElement country=(XmlElement)(doc.SelectSingleNode(myXP));
            if (country != null)
            {
                // get the values we want and display them
                LabelHovedstad.Visible = true;
                LabelBefolkning.Visible = true;
                LabelAreal.Visible = true;
                LiteralHovedstad.Text = 
                    ((XmlElement)(country.SelectSingleNode("capital"))).InnerText;
                LiteralBefolkning.Text = 
                    ((XmlElement)(country.SelectSingleNode("population"))).InnerText;
                LiteralAreal.Text=
                    ((XmlElement)(country.SelectSingleNode("areaInSqKm"))).InnerText;
                // send back the coordinates som the javascrip can find them
                bBoxEast.Text=
                    ((XmlElement)(country.SelectSingleNode("bBoxEast"))).InnerText;
                bBoxNorth.Text =
                    ((XmlElement)(country.SelectSingleNode("bBoxNorth"))).InnerText;
                bBoxSouth.Text =
                    ((XmlElement)(country.SelectSingleNode("bBoxSouth"))).InnerText;
                bBoxWest.Text =
                    ((XmlElement)(country.SelectSingleNode("bBoxWest"))).InnerText;
            }
        }
    }
    #endregion select
}

JavaScriptet

JavaScriptet, maps.js, består av en funksjon:

function showmap()
{
  // unpack data
  var W=document.getElementById('bBoxWest').innerHTML;
  var N=document.getElementById('bBoxNorth').innerHTML;      
  var E=document.getElementById('bBoxEast').innerHTML;
  var S=document.getElementById('bBoxSouth').innerHTML;
  // drop out if no data set
  if((W==0) && (E==0))
    return;
  
  // make a rectangle covering the area
  var mbounds=new GLatLngBounds(new GLatLng(S,W),new GLatLng(N,E));
  if (GBrowserIsCompatible()) 
  {
    // make map in the  div with ID="map_canvas"
    var map = new GMap2(document.getElementById("map_canvas"));
    // which zoomlevel to cover the area
    var Zoomlevel=map.getBoundsZoomLevel(mbounds);
    // find center in the area
    var center=mbounds.getCenter();
    // set up map
    map.setCenter(center, Zoomlevel);
    // add controls (if we want them)
    map.addControl(new GSmallMapControl());
    map.addControl(new GMapTypeControl());
  }        
}

Eksempel 3

Denne løsningen er bygd over samme lest som de to foregående. Forskjellen er at websiden er "selvforsynt" i den forstand at vi ikke trenger å hente data om et land fra serveren. Vi bygger opp en Array med land-data som en Javascript variabel. Det betyr at når vi laster siden første gang laster vi DOM-treet med landbeskrivelser, ekstraherer data og lager en slik struktur:

<script type="text/javascript">
//<![CDATA[
var alleLand=[
"3041565,Andorra,Andorra la Vella,70549,468.0,1.421389,42.656387,1.78172,42.436104,AD",
"1149361,Afghanistan,Kabul,29928987,647500.0,60.504166,38.472115,74.915741,29.406105,AF",
...
"878675,Zimbabwe,Harare,12746990,390580.0,25.236664,-15.616112,33.073051,-22.414764,ZW"];
//]]>
</script>

Koden som lager dette er plassert i Default.aspx.cs og kalles når siden lastes første (og eneste) gang. Alternativt kunne vi laget en array av JSON-objekter.

    protected void Page_Load(object sender, EventArgs e)
    {
        // page is already loaded and datasource generated
        XmlDocument doc=new XmlDocument();
        try
        {
            doc = XmlDataSource2.GetXmlDocument();
        }
        catch (Exception ex)
        {
            Response.Redirect("error.html",true);
        }
        if(doc==null)
            Response.Redirect("error.html", true);
        String allCountries = "";
        // get all data and wrap it as JS-array
        XmlNodeList countries = doc.GetElementsByTagName("country");
        foreach (XmlElement c in countries)
        {
            // geokode,navn,hovestad,befolkning,areal,vest,nord,oest,syd,isocode
            // remember the xml is flattened, so use attributes
            allCountries += String.Format(@"""{0},{1},{2},{3},{4},{5},{6},{7},{8},{9}"",",
                c.GetAttribute("geonameId"),
                c.GetAttribute("countryName"),
                c.GetAttribute("capital"),
                c.GetAttribute("population"),
                c.GetAttribute("areaInSqKm"),
                c.GetAttribute("bBoxWest"),
                c.GetAttribute("bBoxNorth"),
                c.GetAttribute("bBoxEast"),
                c.GetAttribute("bBoxSouth"),
                c.GetAttribute("countryCode")
               );
        }
        String varString = String.Format(@"var alleLand=[{0}];",
            allCountries.Substring(0, allCountries.Length - 1));
        
        // put this javascriptcode in container in header:Literalmapdata
        Literalmapdata.Text = String.Format(@"
<script type=""text/javascript"">
 //<![CDATA[
{0}
//]]>
</script>
", varString);
    }

Land beskrivelsen er utvidet litt med flagg, referanse til wikipedia og referanse til et nettsted som spiller nasjonalsanger. Default.aspx er modifisert tilsvarende.

http://donau.hiof.no/borres/dn/verden4http://donau.hiof.no/borres/dn/verden4

Javascriptet er nå slik:

_mapsLocal.js
Referanser
  1. Geonameswww.geonames.org/14-03-2010
  1. Microsoft Forum generisk XSLT-filforums.microsoft.com/MSDN/ShowPost.aspx?PostID=681086&SiteID=114-03-2010
  1. MGoogle Maps, APIcode.google.com/apis/maps/index.html14-03-2010
  • Eksempel 1:
    https://svn.hiof.no/svn/psource/Csharpsites/verden1
  • Eksempel 2:
    https://svn.hiof.no/svn/psource/Csharpsites/verden2
  • Eksempel 3:
    https://svn.hiof.no/svn/psource/Csharpsites/verden4
Vedlikehold

B. Stenseth, oppdatert mars 2011

(Velkommen) Moduler>Websites>Verden (Fotballresultater)