Rick Strahl's Weblog
Rick Strahl's FoxPro and Web Connection Weblog
White Papers | Products | Message Board | News |

Partial Control Rendering in AJAX Callbacks


September 17, 2008 •

Web Connection 5.0 introduced quite a bit of AJAX functionality in the form of a few high level controls that can be dropped onto a page. The control I use most frequently is the wwMethodCallback control which makes extremely short work of calling back to the server and activating a method in the current form. The steps for this very easy. Let's quickly review  how JSON style callbacks work.

 

Start by dropping a wwWebMethodCallback Control onto the page:

 

<ww:wwWebMethodCallback ID="Proxy" runat="server">

</ww:wwWebMethodCallback>

 

To use this mechanism you can effectively call a server method from the client by implementing a method on the current page (or alternately as a method on any object you specify with Page being the default).

 

The method on the page object is just a plain function that takes an input parameter and returns a value. Parameters are passed from the client, serialized into JSON, sent to the server for processing and then result from the method call is serialized into JSON and passed back to the client. A server method is just a simple method like this:

 

************************************************************************

*  GetEntryTotals

****************************************

***  Function: Callback method that recalcs hourly totals and rates

************************************************************************

FUNCTION GetEntryTotals(lnCustPk,lcDateIn,lcTimeIn,lcDateOut,lcTimeIn)

 

ltTimeIn = CTOT(lcDateIn + " " + lcTimeIn)

ltTimeOut = CTOT(lcDateOut + " " + lcTimeout)

IF EMPTY(ltTimeIn) OR EMPTY(ltTimeout)

      RETURN NULL

ENDIF

 

loCustomer = CREATEOBJECT("busCustomer")

loCustomer.Load(lnCustPk)

 

loTotals = CREATEOBJECT("EMPTY")

ADDPROPERTY(TotalHours, ltTimeOut - ltTimeIn / 3600)

ADDPROPERTY(TotalAmount,  loCustomer.oData.BillRate * loTotals.TotalHours)

 

RETURN loTotals

ENDFUNC

*   GetEntryTotals

 

You can then call this code from the client like with a simple function:

 

function updateEntryTotals() {

    Proxy.callMethod("UpdateEntryTotals",

                     [custPk,

                      $w("txtDateIn").value,

                      $w("txtTimeIn").value,

                      $w("txtDateOut").value,

                      $w("txtTimeOut").value

                     ],

                     function(result) {

                         $w("txtTotalHours").value = result.totalhours.formatNumber("n2");

                         $w("txtTotalAmount").value = result.totalamount.formatNumber("n2");

                     }, onPageError);

}

 

Basically this code pulls values out of some fields on the page to send to the server, and the server processes those input values and returns an object with two properties back to the client. The client receives a callback function with the result object as a parameter which it then uses to update page content. It’s a very simple mechanism that makes it very, very easy to quickly create AJAX callbacks to the server.

 

BTW, the $w() calls are merely shortcuts to document.getElementById() which is function that's part of the wwScriptLibrary.js which is automatically loaded when a wwWebCallbackHandler control is on the page so functionality from the library is always available.

 

The example above demonstrates how to return DATA to the client – you return a value as an object and then use this ‘data’ to apply directly against individual elements on the page.

Returning Partial Rendered Page Content

AJAX callbacks are very powerful and in addition to returning ‘data’ to the client it’s also possible to return HTML. Even better with a very simple little trick you can actually return just about any part of the page from your callback.

 

So instead of passing updated data back to say update or add a grid column on the client – which would involve HTML code and duplication – you could render the grid on the server and send only the HTML for that grid back to the client.

 

This sort of thing is actually very easy because all Web Connection controls include a Render() method that generates the HTML output for that control. It’s quite possible to call .Render() on any control on the page when in the middle of a Page callback. And it doesn’t have to be single controls either – you can render a Panel for example, and all child controls of that panel will also be rendered and returned as HTML.

 

The following is kind of a contrived example, but it’s simple and demonstrates the point effectively. Imagine that you have a DataGrid and an ErrorDisplay control on page and you want to update both controls as part of a callback. Here’s the HTML markup:

 

<ww:wwWebPanel runat="server" id="panUpdatableContent"   OverrideNamingContainer="True">       


    <ww:wwWebErrorDisplay ID="ErrorDisplay" runat="server" />                   

 

    <ww:wwWebDataGrid ID="gdEntries" runat="server"

              AutoGenerateColumns="false"

              PageSize="7" AllowPaging="true"                        

              cssClass="blackborder"         

              Width="800px"

              CellPadding="4"

              DataKeyNames="Pk"

    >        

        <Columns>

            <ww:wwWebDataGridColumn Expression="TTOD(TimeIn)" HeaderText="Date"></ww:wwWebDataGridColumn>

            <ww:wwWebDataGridColumn Expression="'<b>' + Title + '</b><br/>' + TextAbstract(Descript,120)" HeaderText="Description"></ww:wwWebDataGridColumn>

            <ww:wwWebDataGridColumn Expression="TotalHours" Format="9,999.99" Style="text-align:left"></ww:wwWebDataGridColumn>

        </Columns>                       

    </ww:wwWebDataGrid>       

 

</ww:wwWebPanel>

 

<ww:wwWebTextBox runat="server" ID="txtCount" Text="3" />

<input type="button" id="btnUpdateGrid" value="Update Grid" onclick="updateGrid();" />

 

Notice the panel that wraps the ErrorDisplay and DataGrid controls which allows capturing the output from both of them by rendering the panel on the server. This is only necessary if you want to render multiple controls in a group which if you want to update them partially are likely to be already contained in some kind of control container. Note that I also apply OverrideNamingContainer="True" which ensures that the panel doesn’t add naming container unique names to the child controls to make it easier to reference the controls in script code.

 

The client code that calls this function looks like this:

 

function updateGrid() {

    Proxy.callMethod("UpdateGrid", [$w("txtCount").value],

                     function(result) {

                         var el = $ww("panUpdatableContent").setHtml(result,true);

                     }, onPageError);

}

 

It basically picks up the the value of the txtCount control and sends it to the server in the method callback. It then receives the result (which will be the rendered content of the panel) and merges it back into the document.

 

The key to all of this is the server side code and how it renders the panel in the callback:

 

************************************************************************

*  UpdateGrid

****************************************

***  Function: Callback function that updates the grid

***    Assume:

***      Pass:

***    Return:

************************************************************************

FUNCTION UpdateGrid(lcCount)

 

loEntry = NewObject("ttEntry","timeTrakkerSwFox\ttEntry.prg")

 

IF loEntry.GetRecentEntriesForUser(VAL(lcCount),1,.f.,"TEntries") < 0

      this.ErrorDisplay.ShowError("Error: " + loEntry.cErrormsg) 

ENDIF

 

this.gdEntries.DataSource = "TEntries"

this.gdEntries.DataBind()

 

this.ErrorDisplay.ShowMessage("Grid Updated to " + lcCount + " items.")

 

lcOutput = this.panUpdatableContent.Render()

 

RETURN lcOutput

ENDFUNC

 

I just pass a parameter of the number of rows to display (a TOP query) so the number of rows returned varies based on the input I provide. This is the contrived part <s> - it’s a silly example, but it does demonstrate nicely that the grid view changes as new data is loaded. The code basically re-runs a query to retrieve only the requested data, rebinds the grid and writes a message into the ErrorDisplay control.

 

Then this code:

 

lcOutput = this.panUpdatableContent.Render()

 

is all it takes to re-render the panel including the datagrid and the error display.

 

Additionally – with one small change – you can also keep request state intact for things like the active page and sort order and so on that was rendered on the server. Because by default callback requests run through the page cycle up to the Load event, things like sorting and page number state has already been picked up by the page so when the grid is re-rendered that information is also reset.

 

For this to happen though one small change is required:

<ww:wwWebMethodCallback ID="Proxy" runat='server'

    ScriptLocation="/wconnect/scripts/wwscriptlibrary.js"
    PostBackMode="Post" >

    </ww:wwWebMethodCallback>

 

By default the PostBackMode only posts back the parameters you provide explictly in method calls. However, if you use Post or PostNoViewState the form data of all the current fields on the page are also posted to the server. When this happens the page effectively runs through a full load cycle meaning that POST data is loaded into fields nad things like page numbers and sort orders on the grid for example (which are stored in ViewState) are also reset.

 

Partial rendering is a powerful tool to allow you to use server rendered output on the client for AJAX updates without having to recreate HTML from scratch in JavaScript code.

Some Caveats of Partial rendering

Partial rendering can work great in many scenarios especially if you have complex layouts that are created server side. But it’s important to keep a few limitations of this approach in mind.

 

Basically partial rendering works only if the controls in question are not updated explicitly on the client. For example, imagine that I have a ListBox on the page and I add one or more values to it on the client, then requiest the server to do partial rendering of a panel that includes the listbox – those values added on the client aren’t going to be included because the server has no idea that the values in the list have changed unless you explicitly post them to the server.

 

The basic rule to partial rendering is that anything changed on partially updated content should only be changed on the server. This means either full post backs or using AJAX callbacks that end up re-rendering the control. In the list box example this means if you add an item to the list box, you’d have to send the server a message that the list has changed by sending the updated item and maybe adding it to the database, so the next time the list is rendered on the server the new value is available.

 

It depends entirely on your application whether this limitation is an issue or not.

 

Another issue to watch out for is the size of the partial content. If your content is too large partial rendering can be slow which is both annoying to the user and taxes your network bandwidth. It’s tempting to do updates of a large chunk of a page, but it’s often much more efficient to updates in smaller atomic units that update quickly.

 

With these caveats in mind partial rendering can be a huge time saver because it essentially allows you to create rendering content once and then reuse it for AJAX updates.  I’ve used this approach in a number of applications with great results.

 

Check out partial rendering if you haven’t before – Web Connection makes this process very easy and it’s one of the quickest ways to get productive with AJAX.

Posted in:

Feedback for this Weblog Entry