Friday, December 26, 2008

Async/Partial Postbacks from client-side javascript and passing arguments to update web ADF controls and others.

A mechanism for making partial postbacks from client-side scripts and passing arguments Pros: 1) easily to call server-side page/code-behind methods using javascript and pass arguments. 2) easily add new methods to be called with minimal modification to page, code etc. 3) extensible: pass in as many args as necessary, change # of args 4) Update web adf controls, custom controls that implement ICallbackEventHandler & IPostbackEventHandler 5) Update UpdatePanels 6) Update other controls via javascript Cons: 1) Not the most elegant 2) Not a great framework 3) Not usable if Page implements ICallbackEventHandler 4) ...???...
First step is to cause a partial postback (aka sync postback) from a button (doesn't have to be inside an UpdatePanel) which will update web ADF controls (or updatePanels) and/or execute javascript response from server. The js response can also be used to update other client-side controls. Code that goes in the aspx page to create the hidden web controls. Here's the declarative code for the hidden button and a textbox for accepting arguments:


Server-side Code to register the web controls which will cause Async Postback:
Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
Dim sMgr1 As ScriptManager = ScriptManager.GetCurrent(Page)
' Register the controls that will issue postbacks asynchronously.  Note that controls
' registered in this way must implement either INamingContainer, IPostBackDataHandler, or
' IPostBackEventHandler.
' These controls do not need to be contained in an UpdatePanel, and if they are, then
' there is no need to register them this way, they are already async enabled.

If (sMgr1 IsNot Nothing) Then
sMgr1.RegisterAsyncPostBackControl(div1_btn1)
sMgr1.RegisterAsyncPostBackControl(hiddenButton1)
End If
End Sub
Because partial postbacks go through the entire apsx page lifecycle, regular asp.net web control properties are updated during the 'process postback data' phase for a webcontrol (more: webcontrol execution lifecyle). In this case the TextBox webcontrol, 'evtArgsTxtBox.Text' property is updated during partial postback and the value set by the client-side js is available on the server. Alternatively if this approach doesn't work, you can also retrieve post values from the QueryString, Page.Request.Params['evtArgsTxtBox'] Inside the server-side method, we can update esri web adf controls very easily, we can also update any other control inside an asp.net ajax UpdatePanel. Once the web adf controls are updated, their changed client-side state is collected as Callbacks. The callbacks are passed to the client-side js function so that the esri web adf js framework can update the web adf controls. In this case we are serializing the web adf control callbacks as json and passing them to the client as asp.net ajax DataItems, which will then be evaluated by the client-side function ESRI.ADF.System.processCallbackResult('string'). For asp.net controls inside an UpdatePanel, we do not need to pass any js to the client, those controls are rendered and the asp.net ajax framework handles the update automatically. I use RegisterDataItem() instead of the ClientScriptBlock() for the ScriptManager control because using the latter causes script bloating inside the head element of the page. Observe that with firebug. Server-side code to handle the async/partial postback from asp:button, 'hiddenButton1' click event:
Protected Sub hiddenButton1_Load(ByVal sender As Object, ByVal e As System.EventArgs)
'always  executed
logger.Debug("HiddenButton1.Load event ...")
End Sub

''' * The purpose of this function is to execute server-side code/methods
'''   which are invoked from clientside js.
''' * It also receives arguments passed from the client.
''' * This method can only work with partial page postback and can only
'''   update server web controls of type:
'''   1) web adf controls since they impelemt IPostbackEventHandler and ICallbackEventHandler
'''   2) controls inside UpdatePanels
'''
''' It can also pass javascript back to the client for execution, and so other
''' controls can be updated via js code.
'''
''' The hidden asp.net textbox contol, 'evtArgsTxtBox' contains the argument for this method.
''' That textbox.value is populated by client-side js.
''' The format for the value is:
'''  @executionKeyword:@arg1
'''
''' Note that the value for @arg1 can be in any format,
''' e.g.
''' when calling DisplayLayers(), @arg1 format is "layer1;layer2;layer3"
''' when calling FindAddress(),
'''    @arg1 format is "123 Main St"
'''    or for finding intersections "Main St & Pole St"
Protected Sub hiddenButton1_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles hiddenButton1.Click

Me.layerNamesEvtArg = Nothing

Dim eventArgument As String = evtArgsTxtBox.Text
logger.Debug("Handling Control Event, hiddenButton1.Click, EvtArg received: " & eventArgument)
evtArgsTxtBox.Text = ""

Dim scriptMgr As ScriptManager = ScriptManager.GetCurrent(Page)
If (scriptMgr IsNot Nothing And Not scriptMgr.IsInAsyncPostBack) Then
'DataItems & ScriptBlocks can only be registered during Partial Postbacks.
Return
End If

Dim jsCodeResetTxtBox As String = "$get('" + evtArgsTxtBox.ClientID + "').value = '';"
Dim evtArgsCallback As CallbackResult = CallbackResult.CreateJavaScript(jsCodeResetTxtBox)

If (String.IsNullOrEmpty(eventArgument)) Then
logger.Warn("eventArgument is null or empty")
Dim jsCodeToExec As String = "alert('eventArg was not set:" + eventArgument + "')"

'Doesn't work
Dim cbResp As String = CallbackResult.CreateJavaScript(jsCodeToExec).ToString()
'This works
cbResp = "[" & createJsonCallbackResult(jsCodeToExec, "javascript") _
& "," & createJsonCallbackResult(jsCodeResetTxtBox, "javascript") & "]"

ScriptManager1.RegisterDataItem(Page, cbResp)
Return
End If

eventArgument = eventArgument.Trim()

If eventArgument.Contains("ClearHighlight") Then
ClearHighlight()
Map1.CallbackResults.Add(evtArgsCallback)
Map1.CallbackResults.Add(CallbackResult.CreateJavaScript("clearClientsideMapGraphics();"))
scriptMgr.RegisterDataItem(Page, Map1.CallbackResults.ToString)
ElseIf eventArgument.Contains("SuggestPossibleMatches") Then
Dim jsCode As String = "///:::///:::javascript::: alert(""some message"");"
Dim jsCodeCallback As String = String.Format("ESRI.ADF.System.processCallbackResult('{0}');", _
    jsCode.Replace("\", "\\"))
ScriptManager.RegisterClientScriptBlock(Page, sender.GetType, _
                "SuggestPossibleMatches_Click", jsCodeCallback, True)

ElseIf eventArgument.Contains("FindAddress") Then
Dim jsCode As String = FindAddressPartialPostback(eventArgument)
Map1.CallbackResults.Add(evtArgsCallback)
Map1.CallbackResults.Add(CallbackResult.CreateJavaScript("log('executed FindAddress() on server');"))

'-----------Client ScriptBlock approach-----------:
'Dim mapCallbackResult As String = String.Format("ESRI.ADF.System.processCallbackResult('{0}');", _
'    Map1.CallbackResults.ToString().Replace("\", "\\"))
'Dim sumCallbackResult As String = mapCallbackResult & ";" & jsCode
'    ScriptManager.RegisterClientScriptBlock(Page, sender.GetType, "hiddenButton1_Click", sumCallbackResult, True)

'-----------DataItem approach------------:
If (Not eventArgument.Contains("&")) Then
    'only if it is an address search, update FindAddress Panel
    Map1.CallbackResults.Add(CallbackResult.CreateJavaScript(jsCode))
End If
scriptMgr.RegisterDataItem(Page, Map1.CallbackResults.ToString)

ElseIf eventArgument.Contains("FindStreet") Then
Dim jsCode As String = FindByStreetName(eventArgument)
Map1.CallbackResults.Add(evtArgsCallback)
Map1.CallbackResults.Add(CallbackResult.CreateJavaScript("log('executed FindByStreetName() on server');"))
scriptMgr.RegisterDataItem(Page, Map1.CallbackResults.ToString)
ElseIf eventArgument.Contains("FindFolioNumber") Then
FindParcelByFolioNumber(eventArgument)
Map1.CallbackResults.Add(evtArgsCallback)
scriptMgr.RegisterDataItem(Page, Map1.CallbackResults.ToString)
ElseIf eventArgument.Contains("DisplayLayers") Then
Dim jsCode As String = DisplayLayers(eventArgument)
Map1.CallbackResults.Add(evtArgsCallback)
scriptMgr.RegisterDataItem(Page, Map1.CallbackResults.ToString)
ElseIf eventArgument.Contains("SaveMapToFile") Then
Dim jsCode As String = SaveMapToFile(eventArgument)
If (jsCode IsNot Nothing) Then
    Map1.CallbackResults.Add(evtArgsCallback)
    Map1.CallbackResults.Add(CallbackResult.CreateJavaScript("log('executed SaveMapToFile() on server');"))
    Map1.CallbackResults.Add(CallbackResult.CreateJavaScript(jsCode))
    scriptMgr.RegisterDataItem(Page, Map1.CallbackResults.ToString)
End If

End If

End Sub
Client-side code to handle the DataItems passed from asp.net ajax framework. Note that this js code must be executed after the asp.net ajax js libraries & esri web adf have been loaded on client and all page elements have been loaded. Therefore, this can be declared at the bottom of the aspx page.
Sys.Application.add_init(initAppEventHandler);

function initAppEventHandler(sender) {
var prm = Sys.WebForms.PageRequestManager.getInstance();
if (!prm.get_isInAsyncPostBack()) {   
   //to handle dataItems that are sent back during a PartialPostBack
   prm.add_pageLoading(partialPostBackDataItemHandler);   
}
}

function partialPostBackDataItemHandler(sender, args) {
if (_dataItems['__Page'] != null) {
   ESRI.ADF.System.processCallbackResult(_dataItems['__Page']);
}

if (_dataItems['_SOME_CONTROL_NAME'] != null) {
   //do something, like replace contents of a server-side web control
   //$get('_SOME_CONTROL_NAME').innerHTML = dataItems['_SOME_CONTROL_NAME'];
}

//... and so on. handle other dataItems passed from server.
}
There's one more thing we need to add to be able to use these functions defined in our MyPage.aspx from client-side javascript code. Due to a bug in firefox and sometimes security limitations on asp.net ajax library, javascript code may not be able to simulate an user-event, e.g. click. Which means. $get('hiddenButton1').click() may not work in firefox or other browsers.
To fix this we need to use the below code:
/**
Why create this method?
someButton.click() - doesn't work as desired on firefox, we wanted it to
fire a partial page postback using asp.net ajax ScriptManager, but it
caused the whole page to reload. It works fine in IE7 though.

Mozilla/Firefox incorrectly generates the default action for mouse and
printable character keyboard events, when they are dispatched on form
elements.
Ref:
http://forums.asp.net/p/1343661/2728613.aspx
http://www.howtocreate.co.uk/tutorials/javascript/domevents (browse down to 'Manually firing events'
google search: manaully fire event, fireforx fire an event
*/
function fireClickEvent(elemName) {
var el = $get(elemName);
if (el) {
   if (el.fireEvent) { //IE
       el.click();
       /*
       if (isIE())
           el.click();
       else
           el.fireEvent("onclick");
       */
   } else { //firefox
       var clickEvt = window.document.createEvent("MouseEvent");
       //initEvent(evtName, bubbles?, cancellable?)
       clickEvt.initEvent("click", true, true);
       el.dispatchEvent(clickEvt);
   }
} else {
   alert("Unable to send Partial Postback to server"
     +"\nUnable to find DOM element with ID '"+elemName+"'");
}
}

Now, finally the fun part, using what we just built. Let's say we wanted to invoke the FindAddress() method on our MyPage.aspx page and pass in the arguments. On the server, the FindAddress() method would display the results in the TaskResults control, zoom in to the result on the map and highlight the result. The TaskResults, and Map web adf controls can be updated on the server and their changes will be reflected on the server as long as we pass their CallbackResult collections to the client and invoke the js method, processCallbackResult(...). Client-side javascript code to invoke MyPage.aspx.cs.FindAddress() server-side method:
//write code to get the value for street, city etc.
var message = "FindAddress:"+address;
$get('evtArgsTxtBox').value = message;
fireClickEvent('hiddenButton1);
Server-side FindAddress() method:
'''@param eventArgument = "FindAddress: 123 Main St" or
'''   "FindAddress: Main St & Pole St"
Private Function FindAddress(ByVal eventArgument As String) As String

   Dim responseSearch As String = ""
   Dim responseMap As String = ""
   Dim sResults As String = ""

   Dim argArray() As String = eventArgument.Split(":")
   Dim searchtype As String = argArray(0)
   Dim searchvalue As String = argArray(1)
   Dim searchArray() As String = Split(searchvalue, ";")
   Dim datasetLabel As String = ""
   datasetLabel = "Address: " & searchvalue

   Dim gisresource As ESRI.ArcGIS.ADF.Web.DataSources.IGISResource = _
           GeocodeResourceManager1.GetResource(0)
   If Not GeocodeResourceManager1.Initialized Then
       GeocodeResourceManager1.Initialize()
   End If
   If Not gisresource.Initialized Then
       gisresource.Initialize()
   End If
   Dim igf As ESRI.ArcGIS.ADF.Web.DataSources.IGeocodeFunctionality = gisresource.CreateFunctionality(GetType(ESRI.ArcGIS.ADF.Web.DataSources.IGeocodeFunctionality), Nothing)

   Dim addressfields As System.Collections.Generic.List(Of ESRI.ArcGIS.ADF.Web.Geocode.Field) = igf.GetAddressFields()

   Dim avc As New System.Collections.Generic.List(Of ESRI.ArcGIS.ADF.Web.Geocode.AddressValue)
   Dim av1 As New ESRI.ArcGIS.ADF.Web.Geocode.AddressValue("STREET", searchArray(0))
   ' how to find intersection? Example:
   ' Dim av3 As New ESRI.ArcGIS.ADF.Web.Geocode.AddressValue("STREET", "pepperfish bay & harbor view way")
   avc.Add(av1)
   If searchArray.Length > 1 Then
       Dim av2 As New ESRI.ArcGIS.ADF.Web.Geocode.AddressValue("ZONE", searchArray(1))
       avc.Add(av2)
   End If

   Dim dt As System.Data.DataTable
   Dim recordcount As Integer
   igf.MinCandidateScore = 30
   igf.MinMatchScore = 60
   igf.ShowAllCandidates = True
   dt = igf.FindAddressCandidates(avc, False, True)

   Dim showTaskResultsFP As CallbackResult = _
     CallbackResult.CreateJavaScript( _
     " var __trpElem = $find('TaskResultsPanel');" & _
     " if (__trpElem && __trpElem.show) __trpElem.show();")

   If (dt IsNot Nothing And dt.Rows IsNot Nothing And dt.Rows.Count > 0) Then
       dt.TableName = datasetLabel
       recordcount = dt.Rows.Count

       'TODO optionally remove duplicates if a perfect match

       sResults = CreateLocationsTable(dt)
       Dim taskResultCBR As CallbackResultCollection = _
ADFUtils.GetInstance().AddToTaskResults(Map1, dt, "TaskResults1", Nothing, Nothing, Nothing, datasetLabel)
       Map1.CallbackResults.AddRange(taskResultCBR)
       Map1.CallbackResults.Add(showTaskResultsFP)

       If dt.Rows.Count = 1 Then
           Map1.Extent = GetFirstFeatureExtent(dt)
       Else
           'Map1.RefreshResource("Graphics")
       End If

   Else
       dt = New DataTable(datasetLabel)
       Dim dc1 As DataColumn = New DataColumn("Search")
       dt.Columns.Add(dc1)
       Dim dr1 As DataRow = dt.NewRow()
       dr1(dc1) = "returned no results"
       dt.Rows.Add(dr1)

       Dim str As SimpleTaskResult = New SimpleTaskResult("Find Intersection (0) : None", "No results found")
       TaskResults1.DisplayResults(Nothing, Nothing, Nothing, str)
       Dim taskResultCBR As CallbackResultCollection = TaskResults1.CallbackResults
       Map1.CallbackResults.AddRange(taskResultCBR)
       Map1.CallbackResults.Add(showTaskResultsFP)
       sResults = "" & datasetLabel & "
No matching locations found"

   End If

   responseSearch = "showResultsAddress(""" & sResults & """);"
   Return responseSearch

End Function
Using this pattern, we can easily add new methods, call those methods from javascript and modify existing methods. References and more info:

No comments:

Post a Comment