Showing posts with label asp.net ajax. Show all posts
Showing posts with label asp.net ajax. Show all posts

Thursday, October 22, 2009

How to trigger some javascript (client-interaction) the very first time the page is requested in an ESRI .NET web ADF based web application?


Since the esri .net web adf is based on asp.net ajax we can leverage the client application and page lifecycle to trigger activities in different phases of the lifecycle. 
The below illustrates what happens during first page load, partial page postback, and browser close/moving away:
 A. During first initial page request: (1st time the page is loaded or refreshed)
APP_init
APP_load
APP_page_Load 

Notes: The init event of the Application instance is raised only one time for the life of the page in the browser. It is not raised for subsequent asynchronous postbacks. During an initial request, no PageRequestManager events are raised.
B. During Asynchronous postback (aka partial page postback, ajax request using asp.net ajax)
PRM_init_request
PRM_begin_request
WRM_invoking_request
WRM_completedRequest
PRM_page_loading
PRM_page_loaded
APP_load
PRM_end_request 

C. During browser close or browsing away from the page:
PRM_init_request
APP_unload

Where
APP = Sys.Application object
PRM = Sys.WebForms.PageRequestmanager
WRM = Sys.Net.WebRequestManager


1)       1) Create a file and place it  as follows: YourWebApp/javascript/ocgis.js
//contents of file
if (typeof myns== 'undefined') { myns = {}; }
myns.didOnce = false
myns.initAppEventHandler = function(sender) {
                myns.loadAppEventHandler();
}

myns.loadAppEventHandler = function(sender) {
    var prm = Sys.WebForms.PageRequestManager.getInstance();
                if (!ocgis.didOnce) {
                    alert("TODO: initiate things and show panel using javascript....");
                    myns.didOnce = true;
                }
} 

2)       In the default.aspx page, Insert the below right before </body> , i.e. end of body tag
<script language="javascript" type="text/javascript" src="javascript/myns.js" ></script>
<script
language="javascript" type="text/javascript">
                Sys.Application.add_init(myns.initAppEventHandler);
                Sys.Application.add_load(myns.loadAppEventHandler);
</script>  


The diagram below does a great job of showing the events from the 3 main objects in a nice sequence diagram:

Friday, December 26, 2008

Passing Callbacks as asp.net ajax DataItems or ScriptBlocks without piggybacking on an ESRI web adf control

In the previous post I talked about passing callbackresults collection from web adf controls as DataItems on the ScriptManager back to the client-side code for processing (updating client-side rendered controls). What if we didn't want to piggyback on a web adf control and append to its callbacks, what if we just want to pass a javascript as callbacks without piggybacking?
This is possible using a JSON serializer, and which is what i talk about in this post. 
Usually this is what we do: 
1) take a map control or task control  and add callbacks to it, e.g.        Map1.Callbacks.AddRange(TaskResults1.Callbacks)
          OR
     Map1.Callbacks.Add(CallbackResult.CreateJavaScript("alert('hi');")
2) Then we serialize it to json: Map1.Callbacks.ToString()
3) Then we register it as DataItems on the ScriptManager control         myScriptMgr.RegisterDataItem(Page, Map1.Callbacks.ToString())
4) The asp.net ajax framework sends that dataItem string to the client, and we       have a js handler that takes all DataItems from the Page ("__Page") and
      processes it, usually by passing the dataItem string to the js function,       ESRI.ADF.System.processCallbackResult(dataItemString)
Now if we skip step(1),  and add callbacks to an empty callbackresultcollection, e.g. new CallbackResultCollection.add(CallbackResult.CreateJavascript("alert('hi');")), it won't be serialized properly and will not work. 
There's two ways to pass javascript back to the client without a web adf component: 
1) We can make use of the backward compatibility for AGS9.2 callback framework in the ESRI.ADF.System.js file, method: processCallbackResult. But note that we are not using the callback framework, we are just passing a string that makes use of that old format, e.g.
pass the below as a dataitem or scriptblock:
"///:::///:::javascript::: alert('hi');"
2) We can pass the new format, which is a json formatted data, for example:
'[{"id":null,"type":"","action":"javascript","params":["alert(\u0027 --hi-- \u0027);"]}]'
Dim jsonCallback As String = "[{{ {0}id{0}:null,{0}type{0}:{0}{0},{0}action{0}:{0}javascript{0},{0}params{0}:[{0}{1}{0}]}}]"
ScriptManager1.RegisterDataItem(Page, String.Format(jsonCallback, ControlChars.Quote, "alert('hi);"))
''' <summary>
''' To create an ESRI ADF usable json formatted response that can be processed by
''' client-side js function:
''' ESRI.ADF.System.processCallbackResult(string),
'''
''' where an example of string is:
''' [{"id":null,"type":"","action":"javascript","params":["alert(\u0027 --hi-- \u0027);"]}]
'''
''' </summary>
''' <param name="paramValue">depends on the action type, e.g. js code snippet, html code snippet etc
'''</param>
''' <param name="action">valid values:
''' javascript, content, innercontent, set, invoke, image, include
'''</param>
''' <returns>json formatted callback result to be handled by ESRI.ADF.System.processCallbackResult(result)</returns>
''' <remarks></remarks>
Public Function createJsonCallbackResult(ByVal paramValue As String, ByVal action As String) As String
 Dim params() As String = {paramValue}
 Dim hash As Hashtable = New Hashtable()
 hash.Add("id", Nothing)
 hash.Add("type", "")
 hash.Add("action", action)
 hash.Add("params", params)

 Dim jsonConverter As System.Web.Script.Serialization.JavaScriptSerializer = New System.Web.Script.Serialization.JavaScriptSerializer()
 Dim jsonResponseStr As String = jsonConverter.Serialize(hash)
 'Dim jsonCallback As String = "[{{ {0}id{0}:null,{0}type{0}:{0}{0},{0}action{0}:{0}javascript{0},{0}params{0}:[{0}{1}{0}]}}]"
 Return jsonResponseStr
End Function

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: