AJAX PageMethods, JSON Serialization, and a little bit of LINQ databinding

I’ve put together a small demo on using PageMethods to bring data from the server to the client. I’m a big believer in PageMethods, as the definitely offer greater performance than an UpdatePanel, especially when used correctly.

In the spirit of March Madness, I have put together a very small sample Men’s Basketball Tournament research site. Nothing fancy, just want to demonstrate a few concepts. Also, I wouldn’t use any of the actual research as meaningful. (Although I am currently 27-5 on my bracket sheet)

The first thing I did was create a simple XML document that held the data to be used by this application:

<NCAAMensBBTournament>

  <Region regionName="Midwest">

    <Team Code="UW">

      <SchoolName>Wisconsin</SchoolName>

      <TeamName>Badgers</TeamName>

      <Seed>3</Seed>

      <RPI>8</RPI>

      <ScoutingReport>This team plays solid fundamentals and is unflappable under pressure. Should make Elite 8, Final Four is a possibility.</ScoutingReport>

    </Team>

    <Team Code="CSUF">

      <SchoolName>Cal State-Fullerton</SchoolName>

      <TeamName>Titans</TeamName>

      <Seed>14</Seed>

      <RPI>70</RPI>

      <ScoutingReport>Not a chance - they get destroyed by the ferocious BADGERS!</ScoutingReport>

    </Team>

  </Region>

</NCAAMensBBTournament>

Next, I created a couple of Data Transfer Object s (DTO) in C#. Both are views of the same data, but one is going to be used to populate a dropdown, and one is going to be used to populate the main area. The purpose of this is to minimize the traffic between client and server, and do our best to avoid that potential bottleneck, In addition, it is less data to be serialized by the server and de-serialized by the client. Note that I marked the classes with the [Serializable] attribute. This will allow the class to be serialized to JSON when passing form the client to the browser:

[Serializable]

public class TeamOption

{

    private string _Code;

    private string _SchoolName;

    public string Code

    {

        get { return _Code; }

        set { _Code = value; }

    }

    public string SchoolName

    {

        get { return _SchoolName; }

        set { _SchoolName = value; }

    }

   

    public TeamOption()

    {

    }

    public TeamOption(string Code, string TeamName)

    {

        _Code = Code;

        _SchoolName = TeamName;

    }

    #endregion

/// <summary>

/// Data Transfer Object for Team

/// </summary>

/// <remarks>The point of this object is strictly to hold data, and not perform any agaions against the data.</remarks>

[Serializable]

public class Team

{

    #region private members

    private string _Code;

    private string _TeamName;

    private string _Seed;

    private string _RPI;

    private string _ScoutingReport;

    private string _SchoolName;

   

    public string Code

    {

        get { return _Code; }

        set { _Code = value; }

    }

    public string TeamName

    {

        get { return _TeamName; }

        set { _TeamName = value; }

    }

    public string Seed

    {

        get { return _Seed; }

        set { _Seed = value; }

    }

    public string RPI

    {

        get { return _RPI; }

        set { _RPI = value; }

    }

    public string ScoutingReport

    {

        get { return _ScoutingReport; }

        set { _ScoutingReport = value; }

    }

    public string SchoolName

    {

        get { return _SchoolName; }

        set { _SchoolName = value; }

    }

     

    public Team()

    {

    }

    public Team(string Code, string SchoolName, string TeamName, string Seed, string RPI)

    {

        _Code = Code;

        _SchoolName = SchoolName;

        _TeamName = TeamName;

        _Seed = Seed;

        _RPI = RPI;

    }

    public Team(XElement TeamElement)

    {

    // TODO: add error handling in case elements aren't present

    this.Code = TeamElement.Attribute("Code").Value;

    this.RPI = TeamElement.Element("RPI").Value;

    this.ScoutingReport = TeamElement.Element("ScoutingReport").Value;

    this.Seed = TeamElement.Element("Seed").Value;

    this.TeamName = TeamElement.Element("TeamName").Value;

    }

OK, so the first dropdown (“Select a Region”) should be populated as the page loads – so let’s do that. We’re going to use LINQ to get the data from the XML file and bind the dropdown. Note how you can bind directly to the IEnumerable<XAttribute> type. This can also work for other types, such as XElement.

protected void Page_Load(object sender, EventArgs e)

    {

        XDocument teamDoc = GetTeamData();

       

        // LINQ Query to get list of regions

        IEnumerable<XAttribute> regionData = from Region in teamDoc.Descendants("Region").Attributes("regionName") select Region; // LINQ query!

       

        // set dropdownlist datasource to result of LINQ Query and Databind

        RegionList.DataSource = regionData;

        RegionList.DataValueField = "value";

        RegionList.DataTextField = "value";

        RegionList.DataBind();

    }

/// <summary>

/// Static method so that it can also be used by PageMethods

/// </summary>

/// <returns>XDocument containing all of the data in the XML file.</returns>

private static XDocument GetTeamData()

{

    // later on, consider moiving this to a service and get data using WCF

    string path = HttpContext.Current.Server.MapPath(@"App_Data/TeamData.xml");

    // Load XML Document

    XDocument teamDoc = XDocument.Load(path);

    return teamDoc;

}

Now, we can begin coding our server-side PageMethods and the associated client-side JavaScript methods. Let’s begin by creating the PageMethod to accept a string parameter, Region, and return the data necessary to populate the teams dropdown with all the teams playing in that region. Again, we’re using LINQ to populate this data from the XML file. Note that we’re not specifically serializing anything, more on that below.

[WebMethod]

public static List<TeamOption> GetRegionalTeams(string SelectedRegion)

{

    XDocument teamDoc = GetTeamData();

   

    IEnumerable<XElement> teams = from team in teamDoc.Descendants("Team")

           where team.Parent.Attribute("regionName").Value == SelectedRegion

           select team; // get values using LINQ

    List<TeamOption> teamList = new List<TeamOption>();

    foreach(XElement team in teams) // iterate over results of LINQ query

    {

        teamList.Add(new TeamOption(team.Attribute("Code").Value, team.Element("SchoolName").Value));

    }

    return teamList;

}

Now that we’ve coded the PageMethod, let’s code the client-side event to respond to the selection of a region by calling into the server using AJAX. The first key is this, added to the HTML for the dropdown,:

onchange="GetTeams(this);

Now, when the value of the select changes, this will be fired:

function GetTeams(regionDropDown)

{

    var region = regionDropDown.options[regionDropDown.selectedIndex].value;

    var teams = PageMethods.GetRegionalTeams(region, RegionCallback);

}

Note that when I code the PageMethods call, I add a second parameter. This parameter is the callback method to which control will return once server-side processing is complete. You can (and should for production use) add 2 more parameters if you choose - a callback for errors, and a callback for timeouts. So, here’s the cool thing about this. The AJAX framework will automatically serialize my DTO objects to JSON, so that when I use them in the callback, I can refer directly to the properties:

function RegionCallback(result)

{

    // result is what is returned from the successful PageMethod call. It is the serialized JSON representation of our DTO.

    var teamDropDown = $get('TeamList');

    teamDropDown.options.length = 0; // clear out old list

    for(var i=0; i<result.length; ++i)

    {

        // Note that we can iterate through the DTO List object as an array, and that we have acces to the properties of the DTO

        teamDropDown.options[teamDropDown.options.length] = new Option(result[i].SchoolName, result[i].Code);

    }

}

 

This populates the drop down with teams for each region, but what happens when we select a team? Well, we’ll need another PageMethod pattern, this time returning the full team object so we can populate the main content area.

The C# WebMethod:

[WebMethod]

public static Team GetTeam(string TeamCode)

{

    XDocument teamDoc = GetTeamData();

    IEnumerable<XElement> teamElements = from team in teamDoc.Descendants("Team") where team.Attribute("Code").Value == TeamCode select team; // Using LINQ again...

    XElement teamElement = teamElements.First();

    Team selectedTeam = Adapt(teamElement);

    return selectedTeam;

}

JavaScript code:

function GetTeam(teamCode)

{

    PageMethods.GetTeam(teamCode, TeamCallback);

}

      

function TeamCallback(result)

{

    var teamName = $get('TeamName');

    var seed = $get('Seed');

    var rpi = $get('RPI');

    var scoutingReport = $get('ScoutingReport');

    teamName.innerText = result.TeamName;

    seed.innerText = result.Seed;

    rpi.innerText = result.RPI;

    scoutingReport.innerText = result.ScoutingReport;

}

And don’t forget this:

onchange="GetTeam(this.options[this.selectedIndex].value);"

 And that’s about it! Download the demo code from the link below, and you should be able to test this behavior out for yourself. Have fun!

Final thoughts: One very important thing to note is that if you are using PageMethods, you still need to add the ScriptManager object to your ASP.NET page. In addition, you must change the EnablePageMethods property to true, since it is false by default.

Also, you should note that for the drop down that is populated by the server during the page_load event, we used an ASP.NET control, but the second drop down is an HTML SELECT element. You can save processing cycles by removing the ASP.NET UserControl overhead where possible. We don’t need that second control to be ASP.NET – it’s never going back to the server for processing. AJAX will populate the HTML control just fine.

Download the demo code

 

======================================================
This posting is provided "AS IS" with no warranties, and confers no rights.
======================================================