Friday, 1 April 2011

UpdatePanels and Google Maps







An Example Using UpdatePanels and Google Maps. 

There have been many users who have asked how to make Google Maps work in their ASP.NET AJAX application. Most people tend to assume that if you put your whole page in an UpdatePanel then it will give you instant AJAX functionality. While this is true as far as ASP.NET functionality goes (the postback model). But the minute you make more complex clientside applications (like Google Maps) the UpdatePanel comes up short.
The main problem is that applications like Google Maps creates quite a lot of JavaScript objects which are not passed on to the server when the UpdatePanel performs the asynchronous postback. A big problem when writing AJAX applications is handling page state. The UpdatePanel doesn't help you here. Unless you do something yourself to ensure that in memory objects are persisted, the server cannot know anything about them. So for example if you create markers on a map using only clientside code, the server will never know anything about the marker. This problem can be remedied by have the server create the commands to add the markers. That way the server will be able to persist that information for the next time.
This is where the main problem comes in. The map itself in an in memory object that is not easily serialized back to the server. The map object uses a div element (most often, but not required) to display the map tiles, but everything depends on some complicated JavaScript. What the Google Maps .NET Control does is to create the JavaScript necessary to create a map. This works well the first time the page is displayed (i.e. it has not been asynchronously posted back to the server), and on subsequent displays when using classical postbacks. The problem with the UpdatePanel is that any script run, through either the ScriptManager or the ClientscriptManager, initially is remembered as being run. So what happens is that the server responds with a new block of HTML to be shown on the page, but the script that needs to be run to turn the map container (which is only really a div element) into a Google Map never gets run, because the ScriptManager thinks it has already been run. What you can do in this case is to force the map creation script to run again. Except if this has not been updated, then you are going to recreate the map as it was on first load.
So how do you get nice a nice AJAX application using UpdatePanels? The trick is to not wrap the whole page in one big UpdatePanel. It is perfectly possible to have several UpdatePanels on a page and have them do their separate asynchronous postbacks. The map itself should be kept outside any UpdatePanel. That way it is not being affected, and you pass change commands to the map.
Rob from GIS4Business Ltd has contributed an example of just such a solution.
HTML
<%@ Page Language="VB" AutoEventWireup="true" CodeFile="Default.aspx.vb" Inherits="_Default" %>
<%@ Register Assembly="GoogleMap" Namespace="Reimers.Map" TagPrefix="Reimers"
%><!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title>Untitled Page</title>
</head>
<body>
    <form id="form1" runat="server">
        <asp:ScriptManager ID="ScriptManager1" runat="server" />
        <div>
            <table style="width640pxheight208px">
                <tr>
                    <td>
                        <Reimers:GoogleMap
                            id="GoogleMap1"
                            runat="server"
                            ajaxscriptloading="False"
                            Height="500px"
                            Width="500px" />
                    </td>
                    <td colspan="2">
                        <asp:UpdatePanel ID="UpdatePanel2" runat="server">
                            <ContentTemplate>
                                Markers on Map<br />
                                <asp:ListBox
                                    ID="ListBox1"
                                    runat="server"
                                    AutoPostBack="True"
                                    Height="128px"
                                    Width="224px" />
                                <br />
                                <asp:Button
                                    ID="Button1"
                                    runat="server"
                                    OnClick="Button1_Click"
                                    Text="Remove Marker" />
                                <br /><br />
                                <asp:Button
                                    ID="Button3"
                                    runat="server"
                                    Text="_hiddenrefresh"
                                    style="display:none;"
                                    CausesValidation="true"
                                    UseSubmitBehavior="true"
                                    OnClick="Button3_Click"
                                    Width="32px" />
                            </ContentTemplate>
                        </asp:UpdatePanel>
                        <asp:UpdatePanel ID="UpdatePanel1" runat="server">
                            <ContentTemplate>
                                &nbsp; Manually Add Marker<br />
                                <table style="width208px">
                                    <tr>
                                        <td style="height21px">ID</td>
                                        <td style="height21px">
                                            <asp:TextBox
                                                ID="TextBox3"
                                                runat="server" />
                                        </td>
                                        <td style="height21px"></td>
                                    </tr>
                                    <tr>
                                        <td>Latitude</td>
                                        <td><asp:TextBox ID="TextBox1" runat="server" /></td>
                                        <td></td>
                                    </tr>
                                    <tr>
                                        <td>Longitude</td>
                                        <td><asp:TextBox ID="TextBox2" runat="server" /></td>
                                        <td></td>
                                    </tr>
                                </table>
                                <asp:Button
                                    ID="Button2"
                                    runat="server"
                                    OnClick="Button2_Click"
                                    Text="Add" />
                            </ContentTemplate>
                        </asp:UpdatePanel>
                    </td>
                </tr>
            </table>
            <div id="Container" onclick="__doPostBack('UpdatePanel2', '');">
                &nbsp; &nbsp; &nbsp;&nbsp;
            </div>
        </div>
    </form>
</body>
</html>
As you can see from the markup the page displays a map and a both a form where you can add markers to the map and a list that allows you to remove markers.
VB.NET
Imports Reimers.Map

Partial Class _Default
    Inherits System.Web.UI.Page
    Dim Shared GetRandom As New Random


    Protected Sub GoogleMap1_Click(ByVal sender As ObjectByVal e As Reimers.Map.CoordinatesEventArgs) _
    Handles GoogleMap1.Click

        'Create new random ID for marker
        Dim ID As String = "Testing" & GetRandom.Next

        'Create the overlay
        Dim myOverlay As New Reimers.Map.GoogleMarker( _
        ID, _
        e.Coordinates.Latitude, _
        e.Coordinates.Longitude, _
        New GoogleMarkerOptions(ID))

        'create mapcommand for client side
        'The second argument ensures that it is also added to the serverside collection
        Dim Mapcommand As String = GoogleMap1.AddOverlay(myOverlay, True)
        Mapcommand += GoogleMap1.UpdateOverlays()
        'Triggers the updatepanel to refresh using javascript
        'Be careful here, because this is for IE
        Mapcommand += String.Format( _
        "document.getElementById('{0}').click();", Button3.ClientID)

        'return mappcommands to be processed clientside
        e.MapCommand = Mapcommand


    End Sub

    Protected Sub GoogleMap1_ExternalCallback(ByVal Argument As StringByRef MapCommand As String) _
    Handles GoogleMap1.ExternalCallback

        'check to see what triggered the callback
        If Argument = "Add" Then

            'add new overlay serverside
            Dim myOverlay As New Reimers.Map.GoogleMarker( _
            TextBox3.Text, _
            TextBox1.Text, _
            TextBox2.Text, _
            New GoogleMarkerOptions(TextBox3.Text))

            'GoogleMap1.Overlays.Add(myOverlay)
            MapCommand = GoogleMap1.AddOverlay(myOverlay, True)

        ElseIf Argument.StartsWith("Remove"Then

            ''add remove overlay serverside
            Dim Args() As String = Argument.Split("&")

            Dim myOverlay As Reimers.Map.GoogleMarker
            myOverlay = GoogleMap1.Overlays.Item(Args(1))
            GoogleMap1.Overlays.Remove(myOverlay)


        End If

    End Sub

    ''' <summary>
    ''' runs a javascript code clientside
    ''' </summary>
    ''' <param name="Mapcommand"></param>
    ''' <remarks></remarks>
    Public Sub ChangeMap(ByVal Mapcommand As String)
        ScriptManager.RegisterStartupScript(Page, Page.GetType(), "redrawMap", Mapcommand, True)
    End Sub

    Protected Sub Button2_Click(ByVal sender As ObjectByVal e As System.EventArgs)

        Dim mapcommand As String

        'create new marker based on manually added text
        Dim myOverlay As New Reimers.Map.GoogleMarker( _
        TextBox3.Text, _
        TextBox1.Text, _
        TextBox2.Text, _
        New GoogleMarkerOptions(TextBox3.Text))

        'add it serverside
        GoogleMap1.Overlays.Add(myOverlay)

        'add it clientside
        mapcommand = GoogleMap1.AddOverlay(myOverlay)
        mapcommand += GoogleMap1.CreateMapCallback("Add"False)

        'refresh the marker listbox
        PopulateListbox()

        'run the client side script
        ChangeMap(mapcommand)

    End Sub

    ''' <summary>
    ''' Populate the listbox
    ''' </summary>
    ''' <remarks></remarks>
    Public Sub PopulateListbox()
        ListBox1.Items.Clear()

        'GoogleMap1.Overlays.ForEach(AddressOf AddToList)
        For Each ov As GoogleOverlay In GoogleMap1.Overlays
            ListBox1.Items.Add(ov.ID)
        Next

    End Sub

    Protected Sub Button1_Click(ByVal sender As ObjectByVal e As System.EventArgs)

        Dim mapcommand As String
        'get the map overlay that is highlighted in the listbox
        '( we are assuming it is a google marker in this case)
        Dim myOverlay As Reimers.Map.GoogleMarker
        myOverlay = GoogleMap1.Overlays.Item(ListBox1.SelectedValue.ToString)

        'remove it serverside
        'GoogleMap1.Overlays.Remove(myOverlay)

        'remove it clientside
        'The second arguments ensures that it gets removed from the map's overlay collection.
        mapcommand = GoogleMap1.RemoveOverlay(myOverlay, True)

        'create call back to ensure it is removed on both client and serverside
        mapcommand += GoogleMap1.CreateMapCallback("Remove&" & myOverlay.ID, False)

        'run client script
        ChangeMap(mapcommand)

        'update the listbox
        PopulateListbox()

    End Sub

    Protected Sub Button3_Click(ByVal sender As ObjectByVal e As System.EventArgs)
        'refresh the listbox items
        PopulateListbox()

    End Sub
End Class
This is the interesting bit. As you can see from the code there is almost no JavaScript. While this is not true in practice, it is just being called using the extensive list of helper methods supplied bt the Google Maps .NET Control.
The code itself is quite self-explanatory. However if you look carefully at the code you will see that the UpdatePanel postbacks result in an ExternalCallback being triggered through the map. While you could say that this generates two round trips to the server for one action, it does free you from having to think too much about synchronizing state of client and server, since this is all handled by the map methods.







No comments:

Post a Comment