XML
XPATH
__doPostBack
Cookie
Børre Stenseth
Moduler>Websites>IFA

IFA

Hva
Bruk av javascript og Postback

Eksempelet er en enkel konkurranse om å forslå og stemme fram det beste slagordet for pastillen IFA.

Når vi lager en enkel vevside kan vi spesifisere hvilken funksjon som skal respondere på brukerhandlinger. Typisk kan vi bestemme at et museklikk på en knapp skal behandles av tjeneren i en bestemt funksjon.

Image1

I dette tilfellet har vi en litt mer komplisert situasjon. Vi vet ikke på forhånd hvor mange knapper vi kommer til å ha på siden. Det avhenger av hvor mange slagord som er foreslått. Vi må dessuten lage siden "on-the-fly".

Det er mange måter å angripe dette på. Her er valgt å lage et javascript som sørger for at vi får sendt med en identifikasjon av knappen det er trykket på til tjeneren. Vi kan derfor ha en felles mottakksfunksjon, som er Page_Load, og så finne ut hva som har skjedd.

Javascriptet er slik:

    function __doPostBack(eventTarget, eventArgument) {
    var theForm = document.forms['form1'];
    if (!theForm) {
        theForm = document.form1;
    }
        if (!theForm.onsubmit || (theForm.onsubmit() != false)) {
            theForm.__EVENTTARGET.value = eventTarget;
            theForm.__EVENTARGUMENT.value = eventArgument;
            theForm.submit();
        }
    }
    
    function register(form)
    {
        var author=form.newauthor.value;
        var slogan=form.newslogan.value;
        if((author.length==0)||(slogan.length==0))
            alert("Du må fylle inn både slagord og forfatter");
        else
            __doPostBack('newslogan','')
    }

Skriptet forutsetter at følgende 2 "skjulte" inputfelter finnes i formen på siden:

<input code="hidden" name="__EVENTTARGET" id="__EVENTTARGET" value="" />
<input code="hidden" name="__EVENTARGUMENT" id="__EVENTARGUMENT" value="" />

Navngivingen er tilpasset den standarden .Net selv genererer når vi kopler opp en kontroll som f.eks. en listboks. Grunnen til av vi har brukt standarden er at den er kjent på tjenersiden. Det som skjer er altså at vi kaller __doPostBack med to parametre fra de knappene som skal formidle en stemme til et av slagordene, slik:

...
<input code=""button"" id=""vote2"" value=""Stem på denne"" 
               runat=""server"" onclick=""__doPostBack('vote2','1')"" />
...

Funksjonen sørger for å sette verdier i de skjulte feltene og vi er sikre på at vi kan plukke opp disse på tjeneren. Funksjonen register kalles når brukeren ønsker å registrere et nytt slagord, og tar bare en minimalistisk inputkontroll før den samme mekanismen brukers til å sende tilsvarende data som identifiserer denne begivenhetene.

På tjenersiden plukker vi opp en postback slik:

protected void Page_Load(object sender, EventArgs e)
{
   // we need to load the XML-file anyway
   String filename= String.Format(@"{0}\App_Data\Slogans.xml",
                    Path.GetDirectoryName(Request.PhysicalPath));
   // establish datastrucure
   DataAccess DataSource = new DataAccess(filename);
   
   if (IsPostBack)
   {
        // pick up cookies
        HttpCookieCollection cks = Request.Cookies; 
        
        // what has happened ? a vote or a new slogan
        // we read what we need from postEventArgumentID 
        // and postEventSourceID
        NameValueCollection formvar = Request.Form;
        String ButtonId = formvar[postEventSourceID];
        String args = formvar[postEventArgumentID];
        if (ButtonId.StartsWith("vote"))
        {
            String SloganId = ButtonId.Replace("vote", "");
            // vote only once on each
            HttpCookie ck= cks["hasvoted"];
            if ((ck == null)||(ck.Value!=SloganId))
            {
                DataSource.AddVote(SloganId, 1);
                HttpCookie cook=new HttpCookie("hasvoted");
                cook.Value=SloganId;
                Response.SetCookie(cook); 
            }
       }
        else if (ButtonId.StartsWith("new"))
        {
            String NewSlogan = formvar["newslogan"];
            String Author = formvar["newauthor"];
            // submit only once 
            HttpCookie ck = cks["hasmade"];
            if ((ck == null) || (ck.Value != NewSlogan))
            {
                DataSource.InsertSlogan(Author, NewSlogan);
                HttpCookie cook = new HttpCookie("hasmade");
                cook.Value = NewSlogan;
                Response.SetCookie(cook);
            }               
        }
    }
    // display all slogans within placeholder       
    ControlCollection list = PlaceHolder2.Controls;
    HtmlGenericControl hc = new HtmlGenericControl("div");
    hc.InnerHtml = DataSource.MakeSloganListSorted();
    list.Add(hc);
    DataSource.Store();
}

DataLagring

Slagordene lagres i en enkel XML-fil som ser slik ut:

<?xml version="1.0" encoding="utf-8"?>
<slogans nextid="15">
  <slogan id="1" votes="7">
    <content>IFA-esker
i alle damevesker
    </content>
    <author>Lille Per</author>
    <date>2007-02-03</date>
  </slogan>
  <slogan id="2" votes="6">
    <content>Alle i FIFA
spiser IFA</content>
    <author>børre</author>
    <date>2007-2-11</date>
  </slogan>
</slogans>

Dataene administreres slik, klassen DataAccess:

XmlDocument doc;
String FileName;
String LoadErrorMessage = "";
public DataAccess(String filename)
{
    //load XML-file
    FileName = filename;
    try
    {
        doc = new XmlDocument();
        doc.Load(FileName);
    }
    catch (Exception ex)
    {
        LoadErrorMessage = ex.Message;
        doc = null;
    }
}
public void InsertSlogan(String author,String slogan)
{
    if (doc == null)
        return;
    // find next id
    XmlElement root = doc.DocumentElement;
    String nextId = root.GetAttribute("nextid");
    XmlElement newSlogan=doc.CreateElement("slogan");
        newSlogan.SetAttribute("id", nextId);
    XmlElement newauthor = doc.CreateElement("author");
        newauthor.AppendChild(doc.CreateTextNode(author));
    XmlElement content = doc.CreateElement("content");
        content.AppendChild(doc.CreateTextNode(slogan));
    XmlElement date = doc.CreateElement("date");
        DateTime dt=DateTime.Now;
        date.AppendChild(
            doc.CreateTextNode(
            String.Format("{0}-{1}-{2}",dt.Year,dt.Month,dt.Day)));
    newSlogan.AppendChild(content);
    newSlogan.AppendChild(newauthor);
    newSlogan.AppendChild(date);
    newSlogan.SetAttribute("votes", "1");
    root.AppendChild(newSlogan);
    root.SetAttribute("nextid", 
        Convert.ToString(Convert.ToUInt16(nextId) + 1));
}
public void AddVote(String sloganId,int d)
{
    if (doc == null)
        return;
    String xpath = String.Format("//slogan[@id='{0}']", sloganId);
    XmlNodeList list = doc.SelectNodes(xpath);
    if (list.Count == 1)
    {
        int votes=Convert.ToUInt16(
            ((XmlElement)list[0]).GetAttribute("votes"));
        ((XmlElement)list[0]).SetAttribute("votes", 
            Convert.ToString(votes + d));
    }
}
 public String MakeSloganListSorted()
{
    if (doc == null)
        return String.Format(errorstring, LoadErrorMessage);
    XPathNavigator navigator = doc.CreateNavigator();
    XPathExpression selectExpression = navigator.Compile("//slogan");
    selectExpression.AddSort("@votes", 
        XmlSortOrder.Descending, 
        XmlCaseOrder.None, 
        "", 
        XmlDataType.Text);
    XPathNodeIterator nodeIterator = navigator.Select(selectExpression);
   StringBuilder Result = new StringBuilder(2000);
    while( nodeIterator.MoveNext() )
    {
        XmlElement elt = (XmlElement)
            ((IHasXmlNode)nodeIterator.Current).GetNode();
        String Author = 
            elt.GetElementsByTagName("author")[0].InnerText;
        String Content = 
            elt.GetElementsByTagName("content")[0].InnerText;
        String Id = 
            elt.GetAttribute("id");
        String Votes = 
            elt.GetAttribute("votes");
        Result.Append(String.Format(SloganEntry, Id, 
            Content.Replace("\r\n","<br/>"), Votes, Author));
    }
    return Result.ToString();
}
// a simpler version
public String MakeSloganListUnsorted()
{
    if (doc == null)
        return String.Format(errorstring, LoadErrorMessage);
    XmlNodeList list = doc.GetElementsByTagName("slogan");
    StringBuilder Result = new StringBuilder(2000);
    foreach (XmlElement elt in list)
    {
        String Author = 
            elt.GetElementsByTagName("author")[0].InnerText;
        String Content = 
            elt.GetElementsByTagName("content")[0].InnerText;
        String Id = 
            elt.GetAttribute("id");
        String Votes = 
            elt.GetAttribute("votes");
        Result.Append(String.Format(SloganEntry, Id, 
            Content, Votes, Author));
    }
    return Result.ToString();
}
public void Store()
{
    if (doc == null)
        return;
    // write to FileName
    XmlWriterSettings settings = new XmlWriterSettings();
    settings.Indent = true;
    XmlWriter xr = XmlWriter.Create(FileName, settings);
    doc.WriteContentTo(xr);
    xr.Flush();
    xr.Close();
}

Stringen som brukes til å formattere hvert slagord på siden er slik:

String SloganEntry = @"
<div class=""oneslogan"">
<fieldset style=""width: 340px"">
    <legend>{2} stemmer</legend>
    <div style=""margin:20px;color:blue;font-size:24px"">{1}</div>
     <div><span style=""margin-right:10px"">{3}</span>
        <input type=""button"" id=""vote{0}"" value=""Stem p� denne"" 
           runat=""server"" onclick=""__doPostBack('vote{0}',{2})"" />
    </div>
</fieldset>
</div>
";

Løsningen slik den foreligger har en rekke svakheter:

  • Datalagringen burde kanskje vært gjort i en database ?
  • Vi burde hatt muligheter for å legge inn kommentarer til forslagene, i tillegg til å stemme på dem
  • Cookie handteringen er kanskje for streng
  • Vi frisker opp siden komplett hver gang. Alternativer ?
Referanser
Prosjekt:
https://svn.hiof.no/svn/psource/Csharpsites/demosite4

En tilsvarende, litt mer utbygd versjon, implementert med AJAX og Python/CGI er beskrevet på IFA

Vedlikehold

B.Stenseth, februar 2007

(Velkommen) Moduler>Websites>IFA (Templates)