XMLHTTP Callback  

Et irritationsmoment for brugere af webapplikationer er ventetiden ved postbacks. Der findes en lang række situationer, hvor det kræver et postback midtvejs i udfyldelse af en formular, f.eks. når man vælger et land i en dropdown boks hvor regioner/stater så skal fremgå i en efterfølgende dropdown boks. Løsningen på det problem hedder XMLHTTP, hvor javascript på html-siden sender et request via XMLHTTP, får et svar retur og f.eks. udfylder regions dropdown boksen.

I ASP.NET 2.0 er der direkte support for XMLHTTP callbacks. For at udnytte dette kræver det lidt javascript på klienten samt at aspx-siden implementerer System.Web.UI.ICallbackEventHandler - lad os se på et eksempel skrevet til VS 2005:

Et eksempel

Ofte bliver man på en webside bedt om at taste en adresse ind og herunder postnummer og by. Lad os hjælpe brugeren lidt og spare ham/hende for at skulle taste bynavnet ind:

Denne aspx side indeholder 3 tekstfelter: txtAddress, txtPostalCode og txtCity. På txtCity er propertien ReadOnly sat til True. Når der indtastes en værdi i txtPostalCode og feltet herefter forlades, udfyldes txtCity på baggrund af returværdien fra et XMLHTTP Callback.

Server side

På serversiden skal klassen Default.aspx.vb implementere ICallbackEventHandler og dennes enlige metode: RaiseCallbackEvent. Der er kun én RaiseCallbackEvent metode pr. aspx-side, så hvis siden skal håndtere forskellige XMLHTTP Callbacks skal vi sørge for at input strengen (eventArgument) indeholder informationer om, hvilken metode vi ønsker udført på serveren. I eksemplet herunder indikeres hvilken metode der er tale om ved at eventArgument-strengen starter med "PostalCode:".

Ikke alle browsere understøtter denne type af callback. Der er kommet to nye properties SupportsCallback og SupportsXmlHttp på klassen HttpCapabilitiesBase, som man kan bruge til at undersøge, hvorvidt den aktuelle browser understøtter callback.

I Page_Load kigger vi på om XMLHTTP er understøttet af browseren og sætter i så fald et clientside event op, så der ved ændring i txtPostalCode tekstfeltet kaldes ResolveCity javascript metoden. På en web-page kan man få fat i en ClientScriptManager og via den generere en streng ved hjælp af GetCallbackEventReference. Denne streng kommer til at se ud i retning af WebForm_DoCallback('__Page',Command,CallBackHandler,context,onError,true) og den skal vi bruge om et øjeblik, når vi ser på klient koden.

Partial Class _Default
  Inherits System.Web.UI.Page
  Implements System.Web.UI.ICallbackEventHandler

  Protected _callbackstr As String
  Protected _eventArgument As String

  Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load

    If (Request.Browser.SupportsXmlHttp) Then
      txtPostalCode.Attributes.Add("OnChange", "ResolveCity();")
      _callbackstr = Page.ClientScript.GetCallbackEventReference(Me, "Command", "CallBackHandler", "context", "OnError", True)
    Else
      ' Tillad brugeren at indtaste by selv
      txtCity.ReadOnly = False
    End If

  End Sub

  Private Sub RaiseCallbackevent(ByVal eventArgument As String) Implements ICallbackEventHandler.RaiseCallbackEvent
    _eventArgument = eventArgument
  End Sub

  Private Function GetCallbackResult() As String Implements ICallbackEventHandler.GetCallbackResult
    Dim unknown As String = ""
    If _eventArgument.StartsWith("PostalCode:") Then
      Return PostalCodeUtil.GetCity(_eventArgument.Substring("PostalCode:".Length), unknown)
    Else
      Return unknown
    End If
  End Function

End Class
public partial  class _Default : System.Web.UI.Page, System.Web.UI.ICallbackEventHandler
{
  protected string _callbackstr; 
  protected string _eventArgument = "";

  protected void Page_Load(object sender, System.EventArgs e)
  {
    if (Request.Browser.SupportsXmlHttp)
    {
      txtPostalCode.Attributes.Add("OnChange", "ResolveCity();");
      _callbackstr = Page.ClientScript.GetCallbackEventReference(this, "Command", "CallBackHandler", "context", "OnError", true);
    }
    else
    {
      // Tillad brugeren at indtaste by selv
      txtCity.ReadOnly = false;
    }
  }

  void System.Web.UI.ICallbackEventHandler.RaiseCallbackEvent(string eventArgument)
  {
    _eventArgument = eventArgument;
  }

  string System.Web.UI.ICallbackEventHandler.GetCallbackResult()
  {
    string unknown = "";
    if (_eventArgument.StartsWith("PostalCode:"))
    {
      return PostalCodeUtil.GetCity(_eventArgument.Substring("PostalCode:".Length), unknown);
    }
    else
    {
      return unknown;
    }
  }
}

Understøttelse for Firefox

ASP.NET 2.0 går automatisk ud fra at Firefox ikke understøtter XMLHTTP. Dette resulterer i at Request.Browser.SupportsXmlHttp altid returnerer false i Firefox, og dermed sættes XMLHTTP Callback ud af spillet. Det forholder sig rent faktisk sådan at der ER understøttelse for XMLHTTP i Firefox. Men for at få det til at virke, er det nødvendigt at tilføje følgende til sin web.config:

<browserCaps>
  <!-- GECKO Based Browsers (Netscape 6+, Mozilla/Firebird, ...) //-->
  <case match="^Mozilla/5\.0 \([^)]*\) (Gecko/[-\d]+)? (?'type'[^/\d]*)([\d]*)/(?'version'(?'major'\d+)(?'minor'\.\d+)(?'letters'\w*)).*">
    browser=Gecko
    type=${type}
    frames=true
    tables=true
    cookies=true
    javascript=true
    javaapplets=true
    ecmascriptversion=1.5
    w3cdomversion=1.0
    css1=true
    css2=true
    xml=true
    tagwriter=System.Web.UI.HtmlTextWriter
    supportsXmlHttp=true
    supportsCallback=true
    <case match="rv:(?'version'(?'major'\d+)(?'minor'\.\d+)(?'letters'\w*))">
      version=${version}
      majorversion=${major}
      minorversion=${minor}
      <case match="^b" with="${letters}">
        beta=true
      </case>
    </case>
  </case>
</browserCaps>

browserCaps giver mulighed for at angive hvilke egenskaber en specifik type browser har. Når ASP.NET identificerer en browser type, ser den om der er angivet en definition af denne browsers egenskaber i web.config. Findes denne type browsers definition ikke her, benyttes en default configuration.

Client side

På klient siden undgår vi ikke helt at gribe til javascript ;-), men det er dog begrænset, hvad der skal til i dette eksempel. Funktionen ResolveCity opretter en kommando ved at aflæse indholdet af txtPostalCode og udfører så herefter XMLHTTP Callbacket gennem den kode der blev genereret ved hjælp af GetCallbackEventReference-metoden og gemt i variablen _callbackstr i Form_Load. I denne funktion er det også sat op, at det er CallBackHandler der kaldes, hvis XMLHTTP kaldet går godt og OnError hvis der opstår en Exception på serveren.

Der kan kun være én CallBackHandler metode i klienten, så har man flere forskellige XMLHTTP callbacks på samme side så benyttes context.CommandName til at afgøre hvilken af metoderne der er tale om.

  <%@ Page Language="VB" AutoEventWireup="false" ... %>

  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" ... >

  <html xmlns="http://www.w3.org/1999/xhtml" >
  <head runat="server">
      <title>Callback Demo</title>
  </head>
  <body>

  <script>
    function ResolveCity()
    {
      var Command = "PostalCode:" + document.forms[0].elements['txtPostalCode'].value;
      var context = new Object();
      context.CommandName = "ResolveCity";
      <%= _callbackstr %>
    }

    function CallBackHandler(result, context) 
    {
      if (context.CommandName == "ResolveCity" ) 
      {
        document.forms[0].elements['txtCity'].value = result;
      } 
    }
    
    function OnError(message, context) 
    {
      alert("Exception :\n" + message);
    }
  </script>

    <form id="form1" runat="server">

    ... 

    </form>
  </body>
  </html>

Download eksemplet i en zip fil: CallbackDemo.zip. Zipfilen indeholder både en C# og en VB udgave.