Building ASP.NET AJAX Controls (Pt 3 - Properties and Events)

Links to the other posts in this series 

Let's take a quick look at the control I created for the launch event. By adding an instance of my VEMapControl to my page thus:

     <cc1:VEMapControl ID="VEMapControl2" runat="server" 
        ImageUrl='~/Photos/Image1.png'
        Lat='52.0' 
        Lon='-1.0' />

I get an image (Image1.png) on my page which, when I mouseover, changes to show a Virtual Earth map (centred at 52.0, -1.0 in this case) like this:

Untitled

So I need to create a control that firstly has some properties I can set (ImageUrl, Lat and Lon) and also registers an interest in a DOM event (mouseover) to switch between image and map.

Clearly the control is associated with UI elements so I need to choose between either a behavior or a control. In this case, I plan to turn this into a standalone server control so Sys.UI.Control is the right choice. If I was building a control extender (the AJAX control toolkit has lots of examples) that I can "attach" to an existing ASP.NET control to enhance its capabilities (eg the CalendarExtender or the AutoCompleteExtender) then I would choose Sys.UI.Behavior as my base class.

This distinction between Sys.UI.Control and Sys.UI.Behavior is much clearer when you're thinking in terms of server controls. If you're building a control extender then your base class will by Sys.UI.Behavior (because you're not generating UI you're just attaching to it). If you're building a visual control your base class is Sys.UI.Control.

So, given that, here's what my VEMapControl client implementation looks like:

 Type.registerNamespace("ServerControls");

ServerControls.VEMapControl = function(element) {
  ServerControls.VEMapControl.initializeBase(this, [element]);
  
  this._lat = 0.0;
  this._lon = 0.0;
  this._icon = null;
  this._map = null;
}

ServerControls.VEMapControl.prototype = {
  initialize: function() {
      ServerControls.VEMapControl.callBaseMethod(this, 'initialize');
      
      // Add custom initialization here
            $addHandlers(    this.get_element(), 
                                { 'mouseover': this._onMouseOver,
                                    'mouseout': this._onMouseOut }, 
                                this);

      this._onMouseOut();
      
      try {
                this._map = new VEMap(this.get_element().getElementsByTagName("div")[0].id);
                
                this._map.LoadMap(    new VELatLong(this.get_lat(), this.get_lon()), 
                                                        14, 
                                                        VEMapStyle.Road, 
                                                        true);
                
                var _shape = new VEShape(VEShapeType.Pushpin, this._map.GetCenter());
                _shape.SetCustomIcon(this.get_icon());
                this._map.AddShape(_shape);
            }
            catch (e) {
                this.get_element().getElementsByTagName("div")[0].innerHTML 
                    = "<img style='width:100%;' src='' + this.get_icon() + "' />";
            }
  },
    
  _onMouseOver: function() {
  
        var a = this.get_element().childNodes;
        
        for (var i=0; i<a.length; i++) {
            switch (a[i].nodeName.toUpperCase()) {                
                case 'DIV':
                    a[i].style.visibility = "visible";
                    break;
                case 'IMG':
                    a[i].style.visibility = "hidden";
                    break;
                default:
            }
        }
  },
    
    _onMouseOut: function() {
    
        var a = this.get_element().childNodes;
        
        for (var i=0; i<a.length; i++) {
            switch (a[i].nodeName.toUpperCase()) {                
                case 'DIV':
                    a[i].style.visibility = "hidden";
                    break;
                case 'IMG':
                    a[i].style.visibility = "visible";
                    break;
                default:
            }
        }
  },
    
  get_lat: function() {
        return this._lat;
  },
  
  set_lat: function(value) {
   this._lat = value;
  },
  
  get_icon: function() {
        return this._icon;
  },
  
  set_icon: function(value) {
   this._icon = value;
  },
  
  get_lon: function() {
        return this._lon;
  },
  
  set_lon: function(value) {
   this._lon = value;
  },
    
  dispose: function() {        
        if (this._map) {
      this._map.Dispose();
  }
    $clearHandlers(this.get_element());
    ServerControls.VEMapControl.callBaseMethod(this, 'dispose');
  }
}

ServerControls.VEMapControl.registerClass('ServerControls.VEMapControl', Sys.UI.Control);

if (typeof(Sys) !== 'undefined') Sys.Application.notifyScriptLoaded();

Let's take that a chunk at a time:

Picture4

Here we set up our backing fields and any other instance variables. Lat, Lon and Icon (so we can customise the pushpin icon) are going to be properties and I want to hold on to the map reference so I can dispose of it.

Picture5

$addHandlers is a cross-browser function in ASP.NET AJAX to add a list of event handlers to DOM element. In Part 2 I mentioned that both controls and behaviors have an element property to store the associated DOM element. We can access this property (more on property accessors in a sec) with this.get_element() . We then provide a dictionary of event handlers (so we want to wire up to the mouseover and mouseout events - we'll get to the handlers in a second) and finally a reference to the handler owner which is the context for the delegates that are created. This is an important point (and note $addHandler doesn't allow you to specify the handlerOwner, $addHandlers does) as you want the handler to execute in the context of your control rather than the DOM element that raised the event.

Let's switch to properties for a second. As part of our prototype, we have to add property getters and setters to expose the backing fields. Unlike in .NET, there is no way to enforce access modifiers (such as private, protected, friend etc) so this is enforced by convention in JavaScript. You should not access fields with an _ preceding their name directly, you should always use the relevant accessor. The getter function name is the "private" field name preceded by get and the setter with set (as below). So for each property you want to expose you need to create getters and / or setters.

Picture6

Now switching to handlers for a second, we wired up mouseover and mouseout events to "private" (again, the underscore) handlers in our class. These are also implemented in the prototype definition. The handler below switches the visibility of all DIV and IMG elements in our control. It's a bit ugly I'm afraid (I had a very neat way that worked in IE only and despite my best efforts with approaches I thought should work in both IE and FireFox using getElementsByTagName() I never got it to work so I had to revert to this).

Picture7 

The rest of the initialisation code gets us to a known state, creates a new Virtual Earth map centred at the relevant lat/lon and adds a pushpin at the center of the map. If we throw an exception during this, chances are there's been a problem loading the Virtual Earth script (perhaps we've lost our internet connection?) so the pushpin icon is displayed in place of the map to provide a graceful failure.

Picture8

Finally, our override of the dispose method ensures we clean up our map and detach the event handlers we hooked up in initialisation.

Picture9 

There's still more work to be done to turn this into a server control (when it really becomes useful) but it can be made to work as it is as a purely client side component as the following aspx page demonstrates:

 <%@ Page Language="C#" %>

<html xmlns="https://www.w3.org/1999/xhtml">

<head runat="server">

    <script src="https://dev.virtualearth.net/mapcontrol/mapcontrol.ashx?v=6" 
                    type="text/javascript"></script>

    <script type="text/javascript">    
    function pageLoad() {
            $create(    ServerControls.VEMapControl,
                                { 'lat' : 52.0, 'lon' : -1.0 },
                                {},
                                {},
                                $get("div1"))
        }
    </script>

</head>

<body>
    <form id="form1" runat="server">

        <asp:ScriptManager ID="ScriptManager1" runat="server">
            <Scripts>
                <asp:ScriptReference Path="~/JavaScript/VEMapControl.js" />
            </Scripts>
        </asp:ScriptManager>

        <div id="div1">
            <div id="map" style="width: 150px; height: 150px; position: relative;">
            </div>
            <img src="Photos/Image1.png" />
        </div>

    </form>
</body>
</html>

[EDIT: Apologies - spot the deliberate (ahem) mistake above. That pageLoad() method should be a handler for the Application init event (not the load event). You should create components during Application init as I stated in Pt 2]

This creates an instance of our VEMapControl with lat and lon properties set to 52.0 and -1.0 respectively and hooks it up to the "div1" element. Things get really interesting when we encapsulate the UI along with the client side control in an ASP.NET server control. That's what we'll do next...

Technorati Tags: asp.net,ajax,visual studio,windows live,virtual earth