XML
Databinding
XSLT
AJAX
Google
Børre Stenseth
Moduler>AJAX>AJAX i .NET>Du verden

Land i verden

Hva
screen
AJAX i .NET, land i verden

I denne modulen skal vi lage en vevside som lar oss finne fram kart og 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

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

Dette er en AJAX-løsning av det du finner som en normal vevside på modulen: Verden

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, flateinnholdet og et kartutsnitt.

http://donau.hiof.no/borres/dn/verden3http://donau.hiof.no/borres/dn/verden3

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: [3] .

<?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

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>
     <!-- http://www.ia.hiof.no/~borres/ 
    <script type="text/javascript"
            src="http://maps.google.com/maps?file=api&amp;v=2&amp;key=...my googlekey...">
    </script>
    -->
    <!-- http://donau.hiof.no/borres/-->
    <script type="text/javascript"
            src="http://maps.google.com/maps?file=api&amp;v=2&amp;key=...my other googlekey...">
    </script>
     
     <script src="maps.js" type="text/javascript"> </script>
</head>
<body onload="showmap()" onunload="GUnload()">
<h1>Du store verden</h1>
    <form id="form1" runat="server">
    <asp:ScriptManager ID="ScriptManager1" runat="server">
    </asp:ScriptManager>
    
   <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>
        <!-- DataFile below may be changed to 
             geonames direct (http://ws.geonames.org/countryInfo?)
             or local copy -->
        <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">
    <asp:UpdatePanel ID="UpdatePanelAll"
             runat="server">
      <ContentTemplate>
      
      <!-- textual information -->
       <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>
        <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>
        <!-- map data -->
        <div id="map_canvas"  
         style="width: 400px; height: 400px;border:solid; margin-top:10px">
         <!-- just to provoke a rendering of the map-->
         <img src="dummy.gif" alt="-" onload="showmap()" />
        </div>
        
        <!-- hidden data for the javascript to use -->
        <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>
     </ContentTemplate>
     
     <Triggers>
       <asp:AsyncPostBackTrigger ControlID="ListBox2" 
            EventName="SelectedIndexChanged" />
     </Triggers>
    </asp:UpdatePanel>
    
    </td>
    </tr>
    </table>
    </form>
</body>
</html>

Fila er ganske rett fram, bortsett fra en ting: Jeg må på en eller annen måte få Javascriptet som bruker google-bibliotekene til å generere kartet når oppdateringen av UpdatePanelAll er gjort. Jeg har valgt å løse dette ved:

  • å oppdatere fire skjulte felt (bBoxWest,bBoxNorth,bBoxEast,bBoxSouth) med de nødvendige kartreferansene.
  • provosere fram en kjøring av Javascriptet (showMap) når det kunstige bilde dummy.gif er lastet. Siden img-taggen tar et onload-event er dette mulig. dummy.gif er altså et lite bilde med noen white pixler.

Merk at vi har koplet til to Javascript-kilder i toppen av fila: maps.js og Googles bibliotek. Det første skriptet er det jeg har skrevet for å få riktig kartutsnitt, se nedenfor. Det siste med nødvendig ID for aktuelle domener.

default.aspx.cs

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".

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");
            // or you can get it directly from geonames
            // doc.Load("http://ws.geonames.org/countryInfo?");
            // or you could make a local copy to speed things up
            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;
                
                // set the coordinates som the javascript 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 from HTML-elements
  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());
  }        
}
Referanser
  1. GeonamesGeoNameswww.geonames.org/14-03-2010
  1. Google Maps APIGooglecode.google.com/apis/maps/index.html14-03-2010
  1. MSDN ForumsMicrosoftforums.microsoft.com/MSDN/14-03-2010
  1. GeonamesGeoNameswww.geonames.org/14-03-2010
  1. Google Maps APIGooglecode.google.com/apis/maps/index.html14-03-2010
  1. MSDN ForumsMicrosoftforums.microsoft.com/MSDN/14-03-2010
  • Eksempelet:
    https://svn.hiof.no/svn/psource/Csharpsites/verden3
Vedlikehold

B. Stenseth, april 2008

(Velkommen) Moduler>AJAX>AJAX i .NET>Du verden (Noen viner)