Friday, June 26, 2009

Resolution of Operation Aborted error in IE7 for an ESRI ArcGIS 9.3 Web ADF application

This post is merely a summary of work done by others.  My colleague, Paul Angelino, put the pieces together.  His work was based off of two other blog posts, one from ESRI and another from Joel Rumerman.  Joel determined the cause of the problem and provided a solution.

I’m making this post because it took Paul a while to figure out how to implement Joel’s solution, so I’m hoping this post provides clarity.

Our ASP.NET Web ADF application has a master page containing a MapResourceManager.  The Map control is located on the content page.  The solution was to place Joel’s javascript fix as the very last line in the aspx page:

    <script language="javascript" type="text/javascript" src="javascript/AjaxFix.js"></script>
</asp:Content>


Here is the AjaxFix.js file:



//
// Paul Angelino, 6/12/2009
// This code is a workaround to a known issue with IE and ASP.NET Ajax where an "Operation Aborted" popup error
// would occur sporadically.  This effectively replaces some of the out-of-the-box ASP.NET Ajax functions.
// Correct placement of this code within an .aspx file is essential to it working properly.  See the following
// links for more information on the issue and the workiaround:
//      http://forums.esri.com/Thread.asp?c=158&f=2272&t=255293
//      http://blogs.esri.com/Dev/blogs/arcgisserver/archive/2008/09/08/Operation-aborted-error-in-Internet-Explorer.aspx 
//      http://seejoelprogram.wordpress.com/2008/10/03/fixing-sysapplicationinitialize-again/
//
Sys.Application.initialize = function Sys$_Application$initialize() {
    if (!this._initialized && !this._initializing) {
        this._initializing = true;
        var u = window.navigator.userAgent.toLowerCase(),
                          v = parseFloat(u.match(/.+(?:rv|it|ml|ra|ie)[\/: ]([\d.]+)/)[1]);
        var initializeDelegate = Function.createDelegate(this, this._doInitialize);
        if (/WebKit/i.test(u) && v < 525.13) {
            this._load_timer = window.setInterval(function() {
                if (/loaded|complete/.test(document.readyState)) {
                    initializeDelegate();
                }
            }, 10);
        }
        else if (/msie/.test(u) && !window.opera) {
            document.attachEvent('onreadystatechange',
                              function(e) {
                                  if (document.readyState == 'complete') {
                                      document.detachEvent('on' + e.type, arguments.callee);
                                      initializeDelegate();
                                  }
                              }
                          );
            if (window == top) {
                (function() {
                    try {
                        document.documentElement.doScroll('left');
                    } catch (e) {
                        setTimeout(arguments.callee, 10);
                        return;
                    }
                    initializeDelegate();
                })();
            }
        }
        else if (document.addEventListener
                          && ((/opera\//.test(u) && v > 9) ||
                              (/gecko\//.test(u) && v >= 1.8) ||
                              (/khtml\//.test(u) && v >= 4.0) ||
                              (/webkit\//.test(u) && v >= 525.13))) {
            document.addEventListener("DOMContentLoaded", initializeDelegate, false);
        }
        else {
            $addHandler(window, "load", initializeDelegate);
        }
    }
}
Sys.Application._doInitialize = function Sys$_Application$_doInitialize() {
    if (this._initialized) {
        return;
    }
    Sys._Application.callBaseMethod(this, 'initialize');
    if (this._load_timer !== null) {
        clearInterval(this._load_timer);
        this._load_timer = null;
    }
    var handler = this.get_events().getHandler("init");
    if (handler) {
        this.beginCreateComponents();
        handler(this, Sys.EventArgs.Empty);
        this.endCreateComponents();
    }
    if (Sys.WebForms) {
        this._beginRequestHandler = Function.createDelegate(this, this._onPageRequestManagerBeginRequest);
        Sys.WebForms.PageRequestManager.getInstance().add_beginRequest(this._beginRequestHandler);
        this._endRequestHandler = Function.createDelegate(this, this._onPageRequestManagerEndRequest);
        Sys.WebForms.PageRequestManager.getInstance().add_endRequest(this._endRequestHandler);
    }
    var loadedEntry = this.get_stateString();
    if (loadedEntry !== this._currentEntry) {
        this._navigate(loadedEntry);
    }
    this.raiseLoad();
    this._initializing = false;
}
Sys.Application._loadHandler = function Sys$_Application$_loadHandler() {
    if (this._loadHandlerDelegate) {
        Sys.UI.DomEvent.removeHandler(window, "load", this._loadHandlerDelegate);
        this._loadHandlerDelegate = null;
    }
    this._initializing = true;
    this._doInitialize();
}

Resolution of ESRI ArcGIS 9.3 Web ADF Map control base map display

My team has developed an ASP.NET Web ADF application where we are dynamically initializing the MapResourceManager, and setting one of the layers based on user role.  See my Part 1 post for more details.  Our map contains a blend of a dynamic “Projects” layer, several cached layers from an in-house server, and several cached base layers from ArcGIS online.

We were having a problem where our base map, an ArcGIS online service, was displaying as solid gray after the user logged in.  The display would be fine when the user initially browsed to our site, but would be a solid gray after login and postback.

It turns out the problem was that we were setting the PrimaryMapResource property on our Map control to be our dynamic “Projects” layer.  The resolution is simple:  either set the PrimaryMapResource to be one of the ArcGIS online cached layers, or don’t set the PrimaryMapResource at all, in which case the default becomes the last layer added to the MapResourceManager, which in our case is one of the ArcGIS online cached layers.

See this EDN article for more details on the role of the PrimaryMapResource property.  I suspect our problem was caused by having the default tiling scheme set by a dynamic map layer and then applied to a collection of cached map layers.

As a side benefit, our site performance has improved noticeably with this code change.

Monday, June 22, 2009

Dynamic MapResourceManager Part 1: Custom Config Section

This is part 1 of a 2-part post on how I set up a dynamically-configurable ESRI Web ADF MapResourceManager control. Part 1 is how I used a custom web.config section to store the ResourceItem properties for the MapResourceManager.  Part 2 shows how to dynamically initialize a MapResourceManager using the ResourceItem properties I set up here.

I got a great start using the information from James Simmonds' blog.  This MSDN article on custom config sections is helpful also.

I inserted a line like this into my web.config in <configSections>:

<section name="MapResourceItemSettings" type="TNC.GIS.WebControlsLibrary.CustomConfig.MapResourceItemSettings, TNC.GIS.WebControlsLibrary"/>


Note that the class name referenced by the type parameter must be fully-qualified.  I added this custom section farther down inside the web.config <configuration> section:



<MapResourceItemSettings> 
  <LayerCollection> 
    <LayerSettings id="1" layerOrder="0" roles="*" name="Conservation Projects" visible="true" transparency="30" transparentBackground="true" transparentColor="#fffffefd" 
                   dataSourceType="ArcGIS Server Local" dataSourceDefinition="localhost" resourceDefinition="(default)@AllUsers" imageFormat="" height="" 
                   width="" dpi="" returnMimeData="" displayInTableOfContents="" dataSourceShared="" /> 
    <LayerSettings id="2" layerOrder="0" roles="role1,role2" name="Conservation Projects" visible="true" transparency="30" transparentBackground="true" transparentColor="#fffffefd" 
                   dataSourceType="ArcGIS Server Local" dataSourceDefinition="localhost" resourceDefinition="(default)@AuthUsers" imageFormat="" height="" 
                   width="" dpi="" returnMimeData="" displayInTableOfContents="" dataSourceShared="" /> 
    <LayerSettings id="3" layerOrder="1" roles="*" name="Aerial Photography" visible="true" transparency="0" transparentBackground="false" transparentColor="" 
                   dataSourceType="ArcGIS Server Internet" dataSourceDefinition="http://server.arcgisonline.com/arcgis/services" 
                   resourceDefinition="(default)@ESRI_Imagery_World_2D" imageFormat="" height="" width="" dpi="" returnMimeData="" displayInTableOfContents="" dataSourceShared="" /> 
  </LayerCollection> 
</MapResourceItemSettings>


Each LayerSettings entry represents all of the property values to be set on an ESRI ResourceItem instance.  The layerOrder attribute configures the display order in the map, from lowest (on top) to highest (on bottom, e.g., base layers).  The roles attribute allows for dynamic switching of map layers based on user role.  In the code sample above, the top-most layer will have a different display depending on user role, but the base layer will be the same for all users.


Three classes are required to retrieve the values from the MapResourceItemSettings custom configuration section:  MapResourceItemSettings, LayerCollection, and LayerSettings.  Code samples below.



internal class MapResourceItemSettings : ConfigurationSection 
{ 
    [ConfigurationProperty("LayerCollection")] 
    internal LayerCollection Layers 
    { 
        get { return this["LayerCollection"] as LayerCollection; } 
    } 
} 
[ConfigurationCollection(typeof(LayerSettings), AddItemName = "LayerSettings")] 
internal class LayerCollection : ConfigurationElementCollection 
{ 
    protected override ConfigurationElement CreateNewElement() 
    { 
        return new LayerSettings(); 
    } 
    protected override object GetElementKey(ConfigurationElement element) 
    { 
        return ((LayerSettings)element).Id; 
    } 
    internal LayerSettings this[int index] 
    { 
        get { return (LayerSettings)BaseGet(index); } 
    } 
} 
internal class LayerSettings : ConfigurationElement 
{ 
    [ConfigurationProperty("id", IsRequired = true, IsKey = true)] 
    virtual internal int Id 
    { 
        get { return (int)this["id"]; } 
    } 
    [ConfigurationProperty("layerOrder", IsRequired = true)] 
    virtual internal uint LayerOrder 
    { 
        get { return (uint)this["layerOrder"]; } 
    } 
// to save space, I omitted most of the Configuration Properties from this code sample
    [ConfigurationProperty("dataSourceShared")] 
    virtual internal bool? DataSourceShared 
    { 
        get { return (bool?)this["dataSourceShared"]; } 
    } 
}



The LayerSettings class is not ideal, because as far as I could tell, each ConfigurationProperty is limited to a built-in value type.  I made all properties virtual so that LayerSettings can be mocked by Rhino Mocks.  LayerSettings also is difficult to unit-test because it requires a web.config file.  Therefore, I created two adapter classes.  The LayerProperties class adapts LayerSettings, and the LayerPropertiesCollection class adapts LayerCollection:



public class LayerProperties : IComparable<LayerProperties> 
{ 
    public LayerProperties(int id, uint layerOrder, string roles, string name, bool visible, 
        int transparency, bool transparentBackground, string transparentColor, 
        string dataSourceType, string dataSourceDefinition, string resourceDefinition, 
        string imageFormat, int? height, int? width, int? dpi, bool? returnMimeData, 
        bool? displayInTableOfContents, bool? dataSourceShared) 
    { 
        try 
        { 
            _id = id; 
            _layerOrder = layerOrder; 
            SetRoles(roles); 
            _name = name; 
            _visible = visible; 
            _transparency = transparency; 
            _transparentBackground = transparentBackground; 
            SetTransparentColor(transparentColor); 
            _dataSourceType = dataSourceType; 
            _dataSourceDefinition = dataSourceDefinition; 
            _resourceDefinition = resourceDefinition; 
            SetImageFormat(imageFormat); 
            SetHeight(height); 
            SetWidth(width); 
            SetDpi(dpi); 
            SetReturnMimeData(returnMimeData); 
            SetDisplayInTableOfContents(displayInTableOfContents); 
            SetDataSourceShared(dataSourceShared); 
        } 
        catch { throw; } 
    } 
    private int _id; 
    public int Id 
    { 
        get { return _id; } 
    } 
    private uint _layerOrder; 
    public uint LayerOrder 
    { 
        get { return _layerOrder; } 
    } 
// to save space, I omitted most of the Properties from this code sample
    private bool _dataSourceShared; 
    public bool DataSourceShared 
    { 
        get { return _dataSourceShared; } 
    } 
    private void SetDataSourceShared(bool? value) 
    { 
        if (value == null) _dataSourceShared = true; 
        else _dataSourceShared = (bool)value; 
    } 
    #region IComparable<LayerProperties> Members 
    public int CompareTo(LayerProperties other) 
    { 
        if (other == null) 
            throw new ArgumentNullException("other"); 
        return LayerOrder.CompareTo(other.LayerOrder); 
    } 
    #endregion 
}
public class LayerPropertiesCollection : Collection<LayerProperties>
{
    public LayerPropertiesCollection()
        : this("MapResourceItemSettings")
    { }
    public LayerPropertiesCollection(string configSectionName) :
        this(GetMapResourceItemSettingsFromConfigSection(configSectionName))
    { }
    internal LayerPropertiesCollection(MapResourceItemSettings settings)
        : base(AddAllLayerPropertiesFromConfigSection(settings))
    { }
    private static MapResourceItemSettings GetMapResourceItemSettingsFromConfigSection(string configSectionName)
    {
        if (string.IsNullOrEmpty(configSectionName))
            throw new ArgumentNullException("configSectionName");
        try
        {
            return System.Configuration.ConfigurationManager.GetSection(configSectionName) as MapResourceItemSettings;
        }
        catch (Exception ex) 
        { 
            throw new ExplanationException("The provided custom configuration section name (" + configSectionName 
                + ") is invalid", ex); 
        }
    }
    internal static LayerProperties GetLayerPropertiesFromLayerSettings(LayerSettings settings)
    {
        try
        {
            return new LayerProperties(
                        settings.Id,
                        settings.LayerOrder,
                        settings.Roles,
                        settings.Name,
                        settings.Visible,
                        settings.Transparency,
                        settings.TransparentBackground,
                        settings.TransparentColor,
                        settings.DataSourceType,
                        settings.DataSourceDefinition,
                        settings.ResourceDefinition,
                        settings.ImageFormat,
                        settings.Height,
                        settings.Width,
                        settings.Dpi,
                        settings.ReturnMimeData,
                        settings.DisplayInTableOfContents,
                        settings.DataSourceShared
                        );
        }
        catch { throw; }
    }
    private static Collection<LayerProperties> AddAllLayerPropertiesFromConfigSection(MapResourceItemSettings settings)
    {
        if (settings == null)
            throw new ArgumentNullException("settings");
        Collection<LayerProperties> allLayers = new Collection<LayerProperties>();
        try
        {
            for (int i = 0; i < settings.Layers.Count; i++)
                allLayers.Add(GetLayerPropertiesFromLayerSettings(settings.Layers[i]));
        }
        catch (Exception ex)
        {
            throw new ExplanationException("The supplied configuration section instance is invalid, " +
                "or there is an invalid value in one of the LayerSettings values", ex);
        }
        return allLayers;
    }
}



The internal constructor and internal static GetLayerPropertiesFromLayerSettings method allow for unit testing of the LayerPropertiesCollection class.  The final step was to create a factory class to create LayerProperties instances:



public class LayerPropertiesFactory 
{ 
    private Collection<LayerProperties> _allLayers = new Collection<LayerProperties>(); 
    public LayerPropertiesFactory(Collection<LayerProperties> layerPropertiesCollection) 
    { 
        if (layerPropertiesCollection == null) 
            throw new ArgumentNullException("layerPropertiesCollection"); 
        else if (layerPropertiesCollection.Count == 0) 
            throw new ArgumentException("Must not be an empty collection", "layerPropertiesCollection"); 
        try 
        { 
            _allLayers = SortedCollectionFromList(new List<LayerProperties>(layerPropertiesCollection)); 
        } 
        catch { throw; } 
    } 
    public LayerProperties LayerById(int id) 
    { 
        try 
        { 
            return (from layer in _allLayers 
                    where layer.Id == id 
                    select layer).First<LayerProperties>(); 
        } 
        catch { throw; } 
    } 
    public Collection<LayerProperties> AllLayers 
    { 
        get { return _allLayers; } 
    } 
    public Collection<LayerProperties> AllLayersByRole(string role) 
    { 
        if (string.IsNullOrEmpty(role)) 
            throw new ArgumentNullException("role"); 
        List<LayerProperties> lpList = new List<LayerProperties>(); 
        lpList.AddRange(from layer in _allLayers 
                        where ((layer.Roles.Contains("*") == true 
                            & !((from filter in _allLayers 
                                 where filter.Roles.Contains(role) == true 
                                 select filter.LayerOrder).ToArray()).Contains(layer.LayerOrder)) 
                            | layer.Roles.Contains(role) == true) 
                        select layer); 
        return SortedCollectionFromList(lpList); 
    } 
    public Collection<LayerProperties> LayersByRoleAndLayerOrder(string role, params uint[] layers) 
    { 
        if (string.IsNullOrEmpty(role)) 
            throw new ArgumentNullException("role"); 
        if (layers == null) 
            throw new ArgumentNullException("layers"); 
        try 
        { 
            Collection<LayerProperties> lpColl = AllLayersByRole(role); 
            List<LayerProperties> lpList = new List<LayerProperties>(); 
            lpList.AddRange(from layer in lpColl 
                    where layers.Contains(layer.LayerOrder) == true 
                    select layer); 
            return SortedCollectionFromList(lpList); 
        } 
        catch { throw; } 
    } 
    private static Collection<LayerProperties> SortedCollectionFromList(List<LayerProperties> listToSort) 
    { 
        listToSort.Sort(); 
        return new Collection<LayerProperties>(listToSort); 
    } 
}



The code to use the LayersCollection and LayerPropertiesFactory classes looks like this:



//  read from the MapResourceItemSettings custom config section 
LayerPropertiesFactory factory = new LayerPropertiesFactory(new LayerPropertiesCollection()); 
// get a sorted collection of LayerProperties objects for a particular role 
Collection<LayerProperties> layers; 
if (Roles.IsUserInRole("role1")) 
    layers = factory.AllLayersByRole("role1"); 
else 
    layers = factory.AllLayersByRole("*"); 



In Part 2 of this post, I will show the code I used to dynamically manage an ESRI MapResourceManager control.