we'll look at how to create a custom ASP.NET AJAX server control as a wrapper for the Google Maps JavaScript API. The server-side code will be written in C# (which I highly recommend), but it could just as easily be written in VB.NET. The focus will be on creating the control, and we'll take a good look at the whole process of creating a custom ASP.NET AJAX server control, with client side functionality as well.
ire it up and go to File -> New -> Project. From the list on the left, select Web, and then select ASP.NET AJAX Server Control from the main panel on the right. Name the project
MapControl
,
and make sure that the option to create a new solution is selected (if
applicable). Click OK to create the project.
Looking in Solution Explorer, you'll
notice that Visual Studio has generated some files for us already. We'll
examine the generated code a bit, but before we do, let's rename the files and
the classes contained in the files.
- Rename ClientControl1.js to GoogleMap.js
- Rename ClientControl1.resx to GoogleMap.resx
- Rename ServerControl1.cs to GoogleMap.cs
Now, hit Control + H to bring up the
Quick Replace window. Select the appropriate options to replace ServerControl1 with GoogleMap. Be sure that it's set to look in the whole project, and
not just the current file, and then click Replace All. Now do the same thing to
replace ClientControl1 with GoogleMap.
Let's break GoogleMap.js apart piece by piece.
<reference name="MicrosoftAjax.js"/>
This line simply tells Visual
Studio's IntelliSense engine to include the types and methods contained within
MicrosoftAjax.js in the IntelliSense dropdowns.
Type.registerNamespace("MapControl");
MapControl.GoogleMap =
function(element) {
MapControl.GoogleMap.initializeBase(this,
[element]);
}
The first line registers the
namespace MapControl with the AJAX framework. The rest of the code here acts as
the constructor for the client class of our custom control. This is where we
will declare all private properties on the client side.
MapControl.GoogleMap.prototype = {
initialize:
function() {
MapControl.GoogleMap.callBaseMethod(this,
'initialize');
//
Add custom initialization here
},
dispose:
function() {
//Add
custom dispose actions here
MapControl.GoogleMap.callBaseMethod(this,
'dispose');
}
}
Here our custom control's client
class is defined using the prototype model. This is where all methods for the
client class are declared. As you probably guessed, the initialize and dispose
methods are called automatically upon the creation and destruction of an
instance of this class, respectively. In this case, we'll make use of the
initialize method to make a call to the Google Maps API and set up the map.
MapControl.GoogleMap.registerClass('MapControl.GoogleMap',
Sys.UI.Control);
if
(typeof(Sys) !== 'undefined')
Sys.Application.notifyScriptLoaded();
The first part here registers the
class with the defined class name and namespace, and also assigns a base class
(Sys.UI.Control). Lastly, a call is made to
Sys.Application.notifyScriptLoaded(), which notifies the Microsoft AJAX
framework that the script has finished loading. Note that this is no longer
necessary in the .NET Framework 4 and above.
The file named GoogleMap.cs is where
all of the server-side code is contained. Opening the file, the first thing
you'll notice is that it contains a class called GoogleMap, which inherits from
the ScriptControl class. ScriptControl is an abstract base class which inherits
from WebControl, and implements IScriptControl.
public class GoogleMap : ScriptControl
Although it would be fine to leave
that as it is, we can create a more flexible situation by implementing
IScriptControl directly, and inheriting from WebControl instead. By doing so,
we open up the possibility of inheriting from a more complex base class, such
as ListControl. I also have run across problems inheriting from ScriptControl
under various circumstances. Let's change it now to the following:
public class GoogleMap : WebControl,
IScriptControl
Passing by the constructor, you'll
see the GetScriptDescriptors and GetScriptReferences methods.
protected override
IEnumerable<ScriptDescriptor>
GetScriptDescriptors()
{
ScriptControlDescriptor
descriptor = new ScriptControlDescriptor("MapControl.GoogleMap",
this.ClientID);
yield return
descriptor;
}
// Generate the script reference
protected override
IEnumerable<ScriptReference>
GetScriptReferences()
{
yield return
new ScriptReference("MapControl.GoogleMap.js",
this.GetType().Assembly.FullName);
}
Since
we're implementing IScriptControl directly, the access level modifiers will need to be
changed from protected to public, and the override modifiers should be removed
altogether.
In the GetScriptDescriptors method, the ScriptControlDescriptor object that is created and returned tells the framework
which client class to instantiate, and also passes the ID of the HTML element
associated with the current instance of the control. As you'll see in the next
section, this is also the mechanism through which property values are passed
from server-side code to the client class.
The code in the GetScriptReferences method simply adds a ScriptReference to our client code file - it will be loaded automatically
by the framework when needed.
Alright, now that we have some
background information, it's time to start building the map control. To start
out, we'll add some properties to the server-side class (in GoogleMap.cs), sticking with just the basics for now. The zoom and
center point of the map is what comes to mind as necessary properties.
- Zoom
- CenterLatitude
- CenterLongitude
private int _Zoom = 8;
public int Zoom
{
get { return this._Zoom; }
set {
this._Zoom = value; }
}
public double CenterLatitude { get;
set; }
public double CenterLongitude { get;
set; }
You might be wondering how the
values of these properties defined in the server-side class are going to end up
in the client class. Well, this is where the ScriptControlDescriptor comes into
play. By simply calling the AddProperty method of the ScriptControlDescriptor
and passing in the client-side property name and current value, the framework
takes care of all the details.
public
IEnumerable<ScriptDescriptor> GetScriptDescriptors()
{
ScriptControlDescriptor
descriptor = new ScriptControlDescriptor("MapControl.GoogleMap",
this.ClientID);
descriptor.AddProperty("zoom",
this.Zoom);
descriptor.AddProperty("centerLatitude",
this.CenterLatitude);
descriptor.AddProperty("centerLongitude",
this.CenterLongitude);
yield return
descriptor;
}
Now we need to define the properties
in the client class. Open GoogleMap.js and modify the constructor to look like
the following:
MapControl.GoogleMap =
function(element) {
MapControl.GoogleMap.initializeBase(this,
[element]);
this._zoom =
null;
this._centerLatitude
= null;
this._centerLongitude
= null;
}
To make these properties accessible
to the ASP.NET AJAX framework, we need to define get and set accessors. These
accessor methods must follow the naming conventions of the framework - as an
example, for the zoom property the accessors should be named get_zoom and
set_zoom. Add the following code to the prototype declaration for the class:
get_zoom: function() {
return this._zoom;
},
set_zoom: function(value) {
if (this._zoom !== value) {
this._zoom
= value;
this.raisePropertyChanged("zoom");
}
},
get_centerLatitude: function() {
return this._centerLatitude;
},
set_centerLatitude: function(value)
{
if (this._centerLatitude !== value) {
this._centerLatitude
= value;
this.raisePropertyChanged("centerLatitude");
}
},
get_centerLongitude: function() {
return this._centerLongitude;
},
set_centerLongitude: function(value)
{
if (this._centerLongitude !== value) {
this._centerLongitude
= value;
this.raisePropertyChanged("centerLongitude");
}
}
The raisePropertyChanged method is
defined on an ancestor class, Sys.Component, and raises the propertyChanged
event for the specified property.
We'll be writing the code that
creates the map in just a minute, but first we need to define a property that
will store the map object. That way we will be able to access the map after
it's created - in an event handler, for example. Add the following property
declaration to the constructor for the client class (GoogleMap.js) after the
other properties:
this._mapObj = null;
Now let's add a createMap function
to the prototype:
createMap: function() {
var centerPoint = new google.maps.LatLng(this.get_centerLatitude(),
this.get_centerLongitude());
var options = {
zoom:
this.get_zoom(),
center:
centerPoint,
mapTypeId:
google.maps.MapTypeId.ROADMAP
};
this._mapObj
= new google.maps.Map(this._element,
options);
}
The google.maps.LatLng type is defined in the Google Maps JavaScript API (which we
will reference later), and as you probably guessed, represents a point on the
map defined by latitude/longitude. In the map options, we're setting the zoom
and center point of the map to the values passed in by the framework. A map
type of roadmap is set, but this could easily be set to satellite or terrain.
The last line creates the google.maps.Map object, storing a reference to it in the property we
created above.
You'll notice the constructor takes
two parameters - most notably the first one is a reference to the HTML element
associated with the control - the second one is just passing in the map
options.
All that remains now on the client
side is to call our new createMap function from the initialize function, so that the map is
created when the control is initialized.
initialize: function() {
MapControl.GoogleMap.callBaseMethod(this,
'initialize');
this.createMap();
},
Back in the server-side code
(GoogleMap.cs), we need to override the TagKey property in our GoogleMap class,
and return a value of HtmlTextWriterTag.Div. This will ensure that the control
is rendered as an html div element.
protected override HtmlTextWriterTag TagKey
{
get
{
return HtmlTextWriterTag.Div;
}
}
Now let's add a private field of
type ScriptManager to the class - we'll call it sm. This will store a reference
to the page's ScriptManager, which we'll use in a bit.
private ScriptManager sm;
Next, we'll override the OnPreRender
and Render methods of the GoogleMap class.
protected override void OnPreRender(EventArgs
e)
{
if (!this.DesignMode)
{
//
Test for ScriptManager and register if it exists
sm
= ScriptManager.GetCurrent(Page);
if (sm == null)
throw new
HttpException("A ScriptManager control must exist on
the current page.");
sm.RegisterScriptControl(this);
}
base.OnPreRender(e);
}
Here, we're basically just getting
the current page's ScriptManager (and making sure it exists!), and then
registering the current instance of the control with it. This step, as well as
the next, is absolutely necessary - otherwise the client side of the control
won't work.
protected override void Render(HtmlTextWriter
writer)
{
if (!this.DesignMode)
sm.RegisterScriptDescriptors(this);
base.Render(writer);
}
This registers the control's script
descriptors with the page's ScriptManager - sm
is a reference to the ScriptManager that we retrieved in the OnPreRender method.
Last of all (for now!), we'll make
this control compatible with partial trust scenarios, as is quite common due to
the popularity of shared web hosting. In Solution Explorer, open the Properties
folder, and then open AssemblyInfo.cs. Add the following reference near the top of the file.
using System.Security;
Add the following line somewhere in
the middle or near the bottom of the file.
[assembly:
AllowPartiallyTrustedCallers()]
Sign up here with your email
ConversionConversion EmoticonEmoticon