Monday, December 29, 2008

ArcGIS Server and Image Server sample deployment archcitecture

This post is about a system diagram that explains a sample deployment using ArcGIS Server, ArcSDE and Image Server at 9.2. The concepts at 9.3 are similar but there's more integration enhancements. Legend:
  • SOC - Server Object Container, ArcGISSOC.exe process
  • SOM - Server Object Manager, ArcGISSOM.exe process
  • MXD - Map Document authored with ArcMap 
  • Image Server - a windows service to manage Image Service Providers, service name: 'ESRI Image Server'
  • ISP - Image Service Provider, workhorse of the Image Server, comparable to the SOC. Runs as a separate process (windows service with a name similar to 'ESRI Image ServiceProvider: 3383'). Each ISP runs on a different TCP/IP port. 
  • ISCDef - Image Service Compiled definition, a binary file which is the compiled image service definition components
  • ISDef - Image Service definition, an xml configuration file (comparable to MXD for vector data)
  • ArcSDE - A layer on top of a Relational Database System to add spatial capabilities to it
  • GeoDB - Geodatabase. Can be an ArcSDE geodb, file based geodb etc
  • ArcGIS Server Cache - image files of the cached Map Service corresponding to Map Document (MXD)
  • Raster Data - any type of imagery file/data

Friday, December 26, 2008

Creating a JavaScript Custom Timer

TODO

A study of ESRI Java web adf lifecycle & events - how they fit it in the servlet and JSF lifecycle and events

ESRI has a Java web Application Development Framework (ADF) for ArcGIS Server, similar to Oracle's web ADF and other web frameworks. The ESRI web ADF is an API for building mapping web applications and consists of a set of JSF server controls, e.g. TOC control, Map control, OverviewMap control, Navigation control, PrintTask control etc. 
A very high-level workflow is:
  1. use a full-fledged WYSIWYG desktop editor for creating and symbolizing maps and data, aka ArcMap
  2. publish that map as a web mapping service (MapService) using ArcGIS Server
  3. consume that MapService in a web application created using the ESRI web ADF (either in .net or java)
  4. make changes to the map if necessary and restart MapService. 
  5. The data changes are automatically picked up, e.g. data added/deleted/updated
It is a complicated and sophisticated framework built on top of the JSF framework. 
The JSF components of the ESRI java web ADF are also Ajax enabled.
It exposes its own task framework for building tasks/wizards/workflows on using the various JSF controls it exposes.
Sun outlines a few approaches for creating Ajax-enabled components in JSF. Link,
At ArcGIS Server 9.2, the JSF components are ajax-enabled using the PhaseListener approach. This approach is nicely explained in this pdf slideshow and the below diagram:
The ADF uses a PhaseListener (PostBackPhaseListener.java) to handle all Ajax requests. It us also responsible for rendering xml fragments that contain the update state for each ajax-enabled web adf control. The xml fragments are transformed to html via an xslt using some pre-existing xsl files and the client javascript updates the UI elements on the browser. 
Similar to the doc and case study outlined by Sun, the PhaseListener is triggered by keywords in the HttpRequest QueryString (Request Parameters). The keyword being "doAJAX=true" in this case.
Each ADF JSF component can have 1 or more AjaxRenderers associated with it. The purpose of the AjaxRenderer is to detect changes in the JSF component's state, and render xml fragments of any changes to the state. The way to detect change is flexible, they serialize the variables of the JSF component and compare it with a new serialized state, or custom code can be written.  
Anyways, I've digressed, the ajax implementation of the JSF controls is another topic for another post. 
Today I'd like to talk about something that's very important when developing other phaseListeners, servlets in a web adf application - when do the ADF lifecycle & events get kicked off in relation to the standard jsf phases and servlet lifecycles? 
Web ADF lifecycle:
1) very first request to the Java EE container for the web app:
new WebApplication()
webcontext.Initialization - webcontext instance1
webcontext.Passivation - webcontext instance1
2) 2nd ajax request
webcontext.activation - webcontext instance1
webcontext.passivation - webcontext instance1
3) 1st request from a new client
webcontext.initialization - webcontext instance2
webcontext.passivation - webcontext instance2
4)  2nd ajax request from the 2nd client
webcontext.activation - webcontext instance2
webcontext.passivation - webcontext instance2
5) final request from 2nd client
webcontext.activation  - webcontext instance2
webcontext.passivation - webcontext instance2
webcontext.destruction - webcontext instance2
6) final request from 1st client
webcontext.activation - webcontext instance1
webcontext.passivation - webcontext instance1
webcontext.destruction - webcontext instance1
7) Web Application is removed or Java EE container is shut down
WebApplication.destroy()
There are 6 JSF phases and the ADF phases exist in between those.
Servlet lifecycle:
ContextInitialized - creates an ADF WebApplication class instance (1 per web application, exists in application scope)
ADFServletContextListener
ContextDestroyed - destroys an ADF WebApplication class instance (1 per web application, exists in application scope)
JSF lifecycle:
RESTORE_VIEW:
beforePhase: ADF webContext.activate()  - there is no predefined phase for this, it can happen as soon as possible
APPLY_REQUEST_VALUES:
PostBackPhaseListener is kicked off if an ajax request is made- calls all AjaxRenderers to gather xml fragments of changed JSF web control states. 
RENDER_RESPONSE:
afterPhase: ADF webContext.passivate() - this event can happen before render_response or before response_complete (since response_complete may happen before render_response), basically this is the last event of the web adf lifecycle for an http request (once the webcontext has been initialized).
RESPONSE_COMPLETE event:
ADF webContext.passivate
The below is a java class that will show you when the various lifecycle/events of the ESRI java web ADF are being executed
package com.ncedis.adf.web.common;

import java.util.Iterator;
import java.util.Map;

import org.apache.log4j.Logger;

import com.esri.adf.web.data.WebContext;
import com.esri.adf.web.data.WebContextInitialize;
import com.esri.adf.web.data.WebContextObserver;
import com.esri.adf.web.data.WebLifecycle;

public class WebADFLifecycleNotifier implements WebContextObserver, WebContextInitialize, WebLifecycle
{
 private static final long serialVersionUID = -5663664360161434104L;
 private static final Logger logger = Logger.getLogger(WebADFLifecycleNotifier.class);
 
 private boolean isStartup = true;
 private WebContext webContext;

 public void update(WebContext webContext, Object obj)
 {
  logger.debug("WebContext.refresh() has been invoked, which invoked me(WebContextObserver) ...");
  if (isStartup)
  {  }
  this.webContext = webContext; 
  isStartup = false;
 }
 
 
 public static String arrayToString(Object[] array)
 {
  if (array == null)
   return null;
  
  StringBuilder sb = new StringBuilder();
  for (int i = 0; i < array.length; i++)
  {
   sb.append("\t"+array[i]+"\n");
  }
  return sb.toString();
 } 
 
 public void destroy()
 {
  logger.debug("WebContext.destory() has been invoked, which invoked me ... [Terminating Client session]\n");
  logger.debug("Printing stack trace [NOT ERROR]-\n" + arrayToString(Thread.currentThread().getStackTrace()));  
  isStartup = true;
  webContext = null;
 }

 public void init(WebContext webContext)
 {
  logger.debug("WebContext.init() has been invoked, which invoked me ... [Received new Client Session Request]");
  webContext.addObserver(this);
  this.webContext = webContext;
  
  if (logger.isDebugEnabled())
  {
   try
   {
    StringBuffer sb = new StringBuffer("NONE");
    Map attribs = webContext.getAttributes();
    if (!attribs.isEmpty())
    { 
     sb = new StringBuffer("[");
     for (Iterator iter = attribs.entrySet().iterator(); iter.hasNext();)
     {
      Map.Entry element = (Map.Entry) iter.next();
      sb.append(element.getKey()).append(", ");   
     }
     sb.replace(sb.length()-1, sb.length(), "]");
    }
    logger.debug("WebContext.attributes = " + sb.toString());
   } 
   catch (Exception ex) 
   {
    logger.warn("Unable to print webContext.attributes - " + ex.getMessage() + " - " + ex.getStackTrace()[1]);
   }
  }
  
  /**
   * NOTE IMPORTANT: some managed-beans could be instantiated at this stage, 
   * but their properties are set later during the webapp execution.
   * 
   */
 }
 
 public void activate()
 {
  logger.debug("WebContext.activate() has been invoked, which invoked me ... [Client http-request is received]"); 
  isStartup = false;
 }

 public void passivate()
 {
  logger.debug("WebContext.passivate() has been invoked, which invoked me ... [Done with servicing HTTP-request] \n");
  isStartup = false;
 }
}
To better understand the web adf lifecycle with respect to the adf events/servlet events, we should create a PhaseListener class that outputs a message before and after every phase. This class should be as follows:
package com.ncedis.adf.web.faces;

import javax.faces.event.PhaseEvent;
import javax.faces.event.PhaseId;
import javax.faces.event.PhaseListener;

import org.apache.log4j.Logger;

public class VerboseTestPhaseListener implements PhaseListener
{
 private static final long serialVersionUID = 131479863251034948L;
 private static final Logger logger = Logger.getLogger(VerboseTestPhaseListener.class);

 public void afterPhase(PhaseEvent event)
 {
   logger.debug(event.getPhaseId().toString() + " afterPhase");   
   if (event.getPhaseId() == PhaseId.RENDER_RESPONSE) 
    logger.debug("Done with Request!\n"); 
   
 }

 public void beforePhase(PhaseEvent event)
 {
  //this is actually not the beginning of the new request.
  //if (event.getPhaseId() == PhaseId.RESTORE_VIEW) logger.debug("Processing new Request!");          
    logger.debug(event.getPhaseId().toString() + " beforePhase");
 }

 public PhaseId getPhaseId()
 {
  return PhaseId.ANY_PHASE;
 }
 
}
The above java class must be instantiated by the jsf managed-bean facility and inserted as an attribute of webcontext using the dependency injection framework of jsf. In simpler words, you'll have to modify the faces-config.xml file and declaratively add this class as an attribute of the webcontext instance as follows below:

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:

Tuesday, December 23, 2008

Authentication/Login based ArcGIS ADF Task


How do you use the ArcGIS Server .net ADF Task Framework to create a Task which will take into account whether an user is authenticated or logged in? Easy. Basically, we can extend any ADF task or override the Render method to render only when logged in. We use a session variable to indicate if user is logged in. The benefits of this is that we can use any type of login mechanism, asp.net's login control, or anything else, as long as we set a session variable to indicate login status when the user is logged in. If we use asp.net's login control, we can set an eventHandler for loggedIn event to set the session variable. The code below extends the out of the box SearchAttributesTask from the ADF so that it renders only when our user is logged in. Now note that this customTask is not a full blown task, it is just a class that extends an existing task, as simple as that.
Imports Microsoft.VisualBasic
Imports ESRI.ArcGIS.ADF.Tasks
Imports log4net
Imports ESRI.ArcGIS.ADF.Web.UI.WebControls

Namespace WebMapApp
    Public Class LoginBasedSearchAttributesTask
        Inherits SearchAttributesTask

        Private Shared ReadOnly logger As ILog = LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType)


        Public Overrides Sub ExecuteTask()
            MyBase.ExecuteTask()
        End Sub

        Protected Overrides Sub OnLoad(ByVal e As System.EventArgs)
            MyBase.OnLoad(e)
        End Sub

        Protected Overrides Sub Render(ByVal writer As System.Web.UI.HtmlTextWriter)

            Dim Session As HttpSessionState = HttpContext.Current.Session
            Dim isRenderObj As Object = Session("loggedInSuccess")
            If (isRenderObj IsNot Nothing) Then
                Dim isRender As Boolean = CType(isRenderObj, Boolean)
                If (isRender) Then
                    MyBase.Render(writer)
                Else
                    writer.RenderBeginTag("div")
                    writer.AddStyleAttribute(HtmlTextWriterStyle.Color, "red")
                    writer.AddStyleAttribute(HtmlTextWriterStyle.FontSize, "9pt")
                    writer.Write("You are required to login to use this task, to login go to the CIP menu and then Login")
                    writer.RenderEndTag()
                    Return
                End If
            End If

        End Sub

    End Class
End Namespace

There's one more thing left, we need to write the declarative code inside default.aspx for this CustomTask. We should note that the Render() method is called during partial page postbacks or full postbacks, and not during callbacks. So this approach won't work without a asp.net ajax ScriptManager (e.g. in ADF9.2 apps). We wrap our task inside an ajax:UpdatePanel, and then put that ajax:UpdatePanel inside another esri:FloatingPanel. Note, that the updateMode is set to conditional for efficiency reasons, minimizing http response from the server on every postback. Because of this we need to manually update the UpdatePanel, so when the user logins, the codebehind page should call SearchCIPUP.update(), so that this task is re-rendered. Because our custom task is inside another FloatingPanel, we should remember to disable the visual elements, e.g. titlebar, closebutton, borders etc of the inner-task, as shown below.
        
        
            
                
                    
                        
                    
                                
            
        
    

The CSS code:
        .SearchCIPTaskTitleBarCSS
        {
         display:none;
         visibility:hidden;
         width:0px;
         height:0px;
        }
        .SearchCIPTaskCSS
        {
         height:100%;
         width:100%;
         border: none 0px transparent;
         position: relative;
         background-color:White;
        }
There you go, it's as simple as that. It works beautifully.

Great JavaScript articles, references

great resources:
  1. We know that dom manipulation in js is slower than innerHTML, but when innerHTML isn't fast enough? http://blog.stevenlevithan.com/archives/faster-than-innerhtml
  2. Object oriented programming and SetTimeout (what to do when we require context for a js object when executing global functions like settimeout?) http://www.kevlindev.com/blog/?p=72
  3. Need the reference info for the DOM element, refer to Mozilla's great developer center. Gecko DOM reference. https://developer.mozilla.org/en/Gecko_DOM_Reference https://developer.mozilla.org/en/DOM/element
  4. Object Oriented Programming in JS, an intro: http://nefariousdesigns.co.uk/archive/2006/05/object-oriented-javascript

Workflow for adding DataTable, DataSet to ADF TaskResults WebControl

I've found out the hard way on several occassions that the ArcGIS Server 9.3 .NET ADF has issues if things are not done the way they are explicitly outlined in the samples. Now any good API will contain necessary information to inform the developer if there is any particular patterns to follow, e.g. when addings results to a TaskResults webControl, if we set gl.renderOnClient = true after adding to the GraphicsDataSet, it won't render on client. Does this seem reasonable? There's lots and lots of peculiarities with the ArcGIS Server ADF API, and the API docs contains virtually no info other than input & return types, which is no more info than what Visual Studio's Object Browser offers. In fact, you'll probably get more info from using dotNetReflector than looking at the ADF API docs. 
How can it be made better?
  • Very simple, hire a skilled developer to write the docs who has some programming experience and exposure to other APIs as well. They should write the docs by looking at the code and samples, the docs are not inert. 
  • Take a look at other good API's out there, e.g. Sun Java API, Ext JS API. 
  • Add PreConditions, PostConditions to methods, constructors etc. for each input, return parameters
  • Add links to the return, input types (this is the least that could be expected)
  • Add diagrams to explain complex design patterns (object models, class diagrams etc)
  • Avoid the use of Utility classes for each namespace
Anyways, this is the workflow that I found I had to follow in order to add datatables to the TaskResults WebControl and render them on the client (any deviation and it won't work): 
  1. Convert the DataTable dt to a GraphicsLayer, GraphicsLayer gl = ESRI.ArcGIS.ADF.Web.Converter.ConvertToGraphicsLayer(dt)
  2. Get the appropriate LayerFormat, lf, from web.config or MapResourceManager or create your own one
  3. lf.apply(gl)
  4. gl.renderOnClient = true
  5. for each DataRow in GraphicsLayer (which is a subclass of DataTable), set the IsSelectedRow to true,  dataRow.IsSelectedColumn = true
  6. Create a new GraphicsDataSet, gds (which is a subclass of DataSet)
  7. Add that GraphicsLayer, gl to the gds gds.Tables.Add(gl)
  8. If you're inside the ExecuteTask() method of a Custom ADF Task, then you can Me.Results = gds
  9. Or you can create a TaskResultNode,  TaskResultNode resultTRN = adfTaskResultsInstance.CreateTaskResultNode(null, null, null, gds, false, true); Me.Results = resultTRN

ADF Task Framework lifecycle events

For ArcGIS Server 9.3 ADF for .NET, and as of today, the task framework lifecycle flow is undocumented in the ADF docs, so I had to write some test code to see how & when the events occur with respect to each other and the rest of the aspx page lifecycle. Whey is this info important? Because, it will allow me to understand when to execute certain code, e.g. removing nodes in a TreeViewPlusNode control, checking if user is logged in before rendering etc. 
The below summarizes my observations of the ADF task framework lifecycle events, hopefully the lifecycle flow won't change much in 9.4: 
  1. Page_Init
  2. Task.CreateChildControls
  3. Page_PreLoad
  4. Page_Load
  5. Page_PreRender
  6. Task.OnPreRender
  7. Page_PreRenderComplete
  8. Task.RenderControl
  9. Task.Render
  10. Task.RenderContents
  11. Task.OnRenderContents
Next is the list of the adf task framework's other events:
  1. Task.OnClose
  2. Task.OnExpand
  3. Task.OnMinimize
  4. Task.OnShow

Comparison of JSF / Java Servlet and asp.net lifecycle and events (similarities, differences)

Asp.net applications similar to JavaEE web apps have application events, and we can configure listeners/handlers for those events.
For a Java web app, the following events can be handled by wiriing up our listeners in the web.xml file (web deployment descriptor) (Ref: http://edocs.bea.com/wls/docs61/webapp/app_events.html#177041)
1) Application startup
2) Application shutdown
3) Session creation
4) Session invalidation/passivation
5) Session level attribute added/removed/replaced
6) Servlet attribute added/removed/replaced
JSF lifecycle events for a jsf page: (Listeners/Handlers are defined as PhaseListeners)
Similar to the IIS engine which passes the aspx request to a HttpHandler in a asp.net engine, the JavaEE app container (Tomcat, JBoss, WebLogic) passes the jsf request to the JSF Servlet (FacesServlet). The FacesServlet then goes through the below lifecycle events (the PhaseListener can execute code before and/or after a phase): 
 
1) Restore_View
2) Apply_Request_Values (can process events and exit lifecycle)
3) Process_Validations (can process events and exit lifecycle)
4) Update_Model_Values (can process events and exit lifecycle)
5) Invoke_Application (can process events and exit lifecycle)
6) Render_Response
  1. Servlet Context events: 1) Servlet Context created - javax.servlet.ServletContextListener.contextInitialized() 2) Servlet Context about to be shut down - javax.servlet.ServletContextListener.contextDestroyed() 3) Attribute added - javax.servlet.ServletContextListener.attributeAdded() 4) Attribute removed - javax.servlet.ServletContextListener.attributeRemoved() 5) Attribute replaced - javax.servlet.ServletContextListener.attributeReplaced()
  2. HTTP Session events: 1) Http session activated - javax.servlet.http.HttpSessionListener.sessionCreated() 2) Http session about to be passivated - javax.servlet.http.HttpSessionListener.sessionDestroyed() 3) Attribute added - javax.servlet.http.HttpSessionListener.attributeAdded() 4) Attribute removed - javax.servlet.http.HttpSessionListener.attributeRemoved() 5) Attribute replaced - javax.servlet.http.HttpSessionListener.attributeReplaced()
For a ASP.net web application: the below lifecycle-events (in order): 
(Ref: Pro asp.net 2.0 in C# 2005 from Apress)
These events occur for every request. Here an aspx page request is discussed.
The systems/actors in action here are the IIS engine (inetinfo.exe), and the asp.net engine (aspnet_wp.exe). The event handlers/listeners are registered/defined in the global.asax file.
1) Application_BeginRequest
2) Application_AuthenticateRequest
3) Application_AuthorizeRequest
4) Application_ResolveRequestCache
5) At this stage the http request is handed off the appropriate HttpHandler, e.g. for an aspx request it is passed to the asp.net engine (aspnet_wp.exe), where the page is compiled (if necessary) and instantiated. 
6) Application_AcquireRequestState
7) Application_PreRequestHandlerExecute
8) At this stage, the appropriate handler executes the request,e.g. for an aspx request, the event-handling code for the page is executed, page rendered to HTML, i.e. the ASPX page lifecycle is executed (start, init, load, validate/control events, postback, loadComplete, preRender, saveViewState, render, unload
9) Application_PostRequestHandlerExecute
10) Application_ReleaseRequestState
11) Application_UpdateRequestCache
12) Application_EndRequest
Asp.net application events:
These events do not always occur, they are triggered by the application or code.
These are commonly used to perform app initialization, cleanup, usage/stats logging/monitoring, profiling, and troubleshooting. 
1) Application_start
2) Session_start
3) Application_error
4) Session_end
5) Application_End
6) Application_Disposed
Asp.net aspx pafe lifecycle events:
This lifecycle is executed inside the asp.net engine (aspnet_wp.exe) for each aspx page. 
1) Page_Start
2) Page_PreInit
3) Page_Init
4) Page_InitComplete
5) Page_PreLoad
6) Page_Load
7) Process Control events/postback 
8) Page_LoadComplete
9) Page_PreRender
10) SaveStateComplete
11) Page_Render
12) Page_RenderComplete
13) Page_Unload

ASP.net books for software engineers

1) Advanced ASP.NET Ajax Server Controls: review1, link
2) more to come

Viewing the viewstate of asp.net

Most of us do not want to bother with the viewstate when we develop applications, but for large apps, the viewstate can become quite big and therefore a hindrance to performance. If we are not aware of what things may potentially end up in the viewstate, we'll get viewstate's as large as 100KB. In one app I was developing, the partial postback returned 232KB of data, although the viewstate was only 10KB, other things like the ArcGIS ADF's json callback data, update panel data etc. constituted the remainder. 
I have tried to quickly  understand how to minimize viewstate and its bloating, but this is a topic that requires a few hours of careful thinking. 
One thing to keep in mind is that postback data is different from viewstate data. Postback data is inspected and asp.net server-side controls are updated with the postback data, but that mechanism is independent of viewstate data. 
Here's some great articles on understanding asp.net viewstate:
1) Understanding ASP.NET View State- very good article from msdn written by Scott Mitchell from 4GuysFromRolla
Some tools to inspect viewstate data:
1) asp.net viewstate helper from binary fortress software - nice tool but can't dissect all of the viewstate into meaningful info
2) CodeProject: Yet, another ViewState viewer- havent' used it, requires you to login to download.
4) Denis Bauer's ViewstateViewer-havent' used it
5) Nikhil Kothari's Web Development Helper browser extension for IE - article on it