Running TestSwarm in WebMatrix

For last few weeks I have been exploring a subject of JavaScript Unit Testing. I looked at testing frameworks, runners, IDE and CI integration. One of the last items on my list of topics to explore was to find production ready solutions for continuous integration scenarios. Because I work in Microsoft .NET environment CI integration seemed to be a rather uncharted territory. This post describes my experience with getting TestSwarm to run in windows centric settings.

The next two paragraphs describe what TestSwarm is and why it might be an important to learn about it, a rationale behind the whole effort.

TestSwarm, John Resig’s open source project for supporting distributed continuous integration testing for JavaScript, while still in an alpha development stage, generates a lot of interest among web UI developers and project managers. As UI development shifts towards client side technologies, particularly JavaScript, the need for the same level of support that has been in place, for many years already, for server side technologies is now expected for client side development as well.

JavaScript unit testing has been used for quite some time now. There are more than a few established unit testing frameworks and IDE support for some of them is either available or in works. Missing though is support for running JavaScript unit tests in continuous integration scenarios. Before the TestSwarm the most popular option for handling CI scenarios seemed to be using JSTesDriver from Google. One inconvenience of using JSTestDriver is the need for developing an adapter to integrate a unit testing framework and JSTestDriver runner. When a framework and a runner evolve, quite a bit of maintenance overhead is being added for keeping them both in synch (updating an adapter). In contrast, TestSwarm integration is framework independent. The only moving part is a script that needs to understand test results. Thus, it seems that TestSwarm should be considered a serious player in the area of JavaScript Unit Testing and Continuous Integration.

Back to technology then, these are the parts that will be needed:

  • TestSwarm
  • WebMatrix
  • WordPress it’s an easy way of getting PHP and MySQL on windows machine and you will have some PHP code to look at or compare if needed.
  • MySQL Workbench if you need GUI for MySQL management tasks.
  • IIS URL Rewrite Module to get import Apache .htaccess url rewriting rules into IIS Express .
  • QUnitfor creating demo JavaScript unit testing project.

Getting it all working:

Step 1.

In WebMatrix open directory with TestSwarm files downloaded from git repository. Read TestSwarm configuration instructions either in ReadMefile or in Wiki. I used WebMatrix to run the TestSwarm which means that IIS Express was used as a web server and that TestSwarm ran in the root of the site (on random port).
In testswarm.json I needed to modify contextpath“: “/testswarm” to contextpath“: “/“. Otherwise, images and css would not load correctly. TestSwarm application depends on url rewriting rules in .htaccess file. Use IIS URL Rewrite extension to Import those rules to IIS. Just copy the results from the Import screen and add them to your IIS Express configuration file. Delete this: RewriteEngine On RewriteBase / from Rewrite Rules box to make the error seen in Converted Rules box disappear.

Import Rewrite Rules

Import Rewrite Rules

Add the ruele to IIS Express configuration file (applicationhost.config) which should be located in C:\Users\yourUserName\Documents\IISExpress\config folder. The section shown below should be added at the end of that configuration file.

<location path="testswarm">
        <system.webServer>

<rewrite>
            <rules>
                <rule name="Imported Rule 1" stopProcessing="true">
                    <match url="^" ignoreCase="false" />
                    <conditions logicalGrouping="MatchAny">
                        <add input="{REQUEST_FILENAME}" matchType="IsFile" ignoreCase="false" />
                        <add input="{REQUEST_FILENAME}" matchType="IsDirectory" ignoreCase="false" />
                    </conditions>
                    <action type="None" />
                </rule>
                <rule name="Imported Rule 2">
                    <match url="^(.*)/$" ignoreCase="false" />
                    <action type="Redirect" url="{R:1}" appendQueryString="true" redirectType="Permanent" />
                </rule>
                <rule name="Imported Rule 3">
                    <match url="^([a-z]*)$" ignoreCase="false" />
                    <action type="Rewrite" url="index.php?action={R:1}" appendQueryString="true" />
                </rule>
                <rule name="Imported Rule 4">
                    <match url="^([a-z]*)/(.*)$" ignoreCase="false" />
                    <action type="Rewrite" url="index.php?action={R:1}&amp;item={R:2}" appendQueryString="true" />
                </rule>
            </rules>
        </rewrite>

        </system.webServer>
    </location>

At this point TestSwarm should be looking like working. It should load pages and images. But, you still would not be able to add jobs or run any tests.

Step 2.

Go to http://localhost:51336/signup to create a user account (just notice 51336 is a random port number, yours will be different).
Your account should get created and when you open MySQL Workbench and navigate to a table you should see an entry in a users table.
The important thing to note about the user data is to copy the value from “auth” column. It will be needed for crating jobs. If you do not see a value of that field but just a “BLOB” word, go to Edit –> Preferences an open SQL Editor Tab and check “Treat BINARY/VARBINARY as nonbinary character string“.

SQL Editor

SQL Editor

Now, you should be ready for adding jobs.

Step 3.
Before you can actually add a job you need to have a JavaScript code to test. So, create a web application and add a JavaScript file to it. Next, decide which JavaScript unit testing framework you are going to use. My recommendation is to use QUnit as it’s one with greatest support in TestSwarm. For start, you just want to get things running. Next, create a unit test for your script. Verify that the tests passes.

Copy a inject.js from a js folder in TestSwarm to your demo application and make sure to add a link to it in a QUnit test html file.

The head tag in a test html file should look like this:

<head>
    <meta http-equiv="cache-control" content="no-cache">
    <meta http-equiv="X-UA-Compatible" content="IE=Edge">

    <title>QUnit Testing</title>
    <link href="qunit.css" rel="stylesheet" type="text/css" />
    <script src="qunit.js"></script>
    <script src="inject.js"></script>
</head>

You are still not ready, at least very likely, for adding jobs though. Go to the next step to see what else might be missing.

Step 4.

On my installation I ran into a problem. Adding jobs failed because a results column in run_client table would not allow NULL and somehow a NULL was being inserted. Results column is a TEXT field and a default value of empty string cannot be set on it. It’s probably an issue with MySQL version. So, I modified the column and made it nullable.

Results Column

Results Column

Step 4.

Go to http://localhost:51336/addjob page and enter the required information. For start, I would suggest to check desktop browsers option, it will be easier to analyze the data changes in a database in a response to TestSwarm UI actions. Also, enter only one run. It will be easier to troubleshoot if things do not go right. In the URL field for the run enter the URL of the test html page from your demo web app.

All should work now and you should see this confirmation message:

Add Job

Add Job

Step 5.

You ready to add agents (browsers) for running jobs now. Navigate to http://localhost:51336/run/testuser2 (testuser2 needs to be a username that you created in step 2) in your choice of the browser and you should see the message that a job run. Do the same for other browsers and you should similar messages.

Now if you go to http://localhost:51336/job/6 (6 is my job number, yours might be different of course, it’s just RESTful Url) you will see a page looking like this:

Job Status

Job Status

In this particular case you can see that runs in IE, Opera and Safari succeeded and that other agents are probably not available or have not been run yet. You can click on a note icon to see the details of the run. The details will be just html form a QUnit test page, see example below.

Run Details

Run Details

Read this if things still do not work

If it looks like you still cannot run jobs. Here is another hint, something that I did to make attaching clients work on my system. I had a problem with a call to gzencode (line 57 in SeverunAction.php file.

ServerunAction.php page

ServerunAction.php page

I modified that line (57) to be like 58.

57. // $results = gzencode( $request->getVal( "results", "" ) );
58. $results = $request->getVal( "results", "" );

and to make things symmetrical also had to modify (comment out) a line 37


36. header( "Content-Type: text/html; charset=utf-8" );
37. // header( "Content-Encoding: gzip" );

in ResultsPage.php

Results Page

Results Page

In the end all troubles with a NULL in a Results table and trouble with gzip encoding might have been a LINUX vs Windows issue (I did not investigate a root cause, just tried to get things running).

Summary
The goal of this blog post was to show how to setup a TestSwarm with WebMatrix. Setting up TestSwarm in CI scenario would require more effort but steps would be very similar. Also, one easy step to take would be to use SQL Express in place of MySQL. Converting PHP code to MVC Razor syntax would be probably more difficult but also very likely some fun.

Posted in IIS Express, JavaScript Unit Testing, TestSwram, WebMatrix | Tagged , , , | Comments Off

Bing Maps Ajax Control 7.0 Widget – Search, Zoom and Undo

This is part 3 of the Bing Maps Ajax Control 7.0 Widget. Part 2 can be found here: Bing Maps Ajax Control 7.0 Widget – Drawing Circles and Rectangles. Refer to the previous post for the introduction. In this part I will quickly show how to add Find Location, Rectangle Zooming and Undo functionality.

First we need to add few controls to the HTML page hosting Bing Maps Control:

<!doctype html>
<html>
    <head>
        <title>Bings Map Widget Demo</title>
        <script type="text/javascript" src="scripts/jquery-1.7.1.js"></script>
        <script src="http://ajax.aspnetcdn.com/ajax/jquery.ui/1.8.10/jquery-ui.min.js"></script>
    </head>
<body>
    <button type="button" id="btnLineDrawCustom">Draw Line</button>
    <button type="button" id="btnPolyDrawCustom">Draw Polygon</button>
    <button type="button" id="btnRectangleDrawCustom">Draw Rectangle</button>
    <button type="button" id="btnCircleDrawCustom">Draw Circle</button>
    <input type="text" id="txtRadius" value="5" size="3" />
            <select id="ddlRadius">
				<option value="m" selected="selected">miles</option>
				<option value="k">kilometers</option>
            </select><button type="button" id="btnCircleDrawWithRadiusCustom">Draw Circle with Radius</button>
    <button type="button" id="btnCancel">Cancel Last Action</button>
    <button type="button" id="btnClearAll">Clear All</button>
    <br />
    <button type="button" id="btnZoomToRectangle">Zoom To Rectangle</button>
    <input type="text" id="txtSearch" size="30" value="College Park, md" />
    <button type="button" id="btnFindOnMap">Find Location</button>
   <hr />
    <div id="bingMapsToolbox" />
    <div id='mapDiv' name='mapDiv' style="position:relative; float: left;"/>
<script type="text/javascript" src="http://ecn.dev.virtualearth.net/mapcontrol/mapcontrol.ashx?v=7.0"></script>
<script src="scripts/cm-utils.js"></script>
<script src="scripts/cm-widgets.bingMaps7.js"></script>
<script src="scripts/startup.js"></script>
</body>

In a startup script buttons click events are bound to method calls on the Bing Maps Widget:

 ("#btnCancel").bind('click', function () {
     bingMaps7.bingMaps7('cancelLastAction'); return false;
 });
 $("#btnClearAll").bind('click', function () {
     bingMaps7.bingMaps7('clearMap'); return false;
 });
 $("#btnZoomToRectangle").bind('click', function () {
     bingMaps7.bingMaps7('zoomToRectangle'); return false;
 });
 $("#btnFindOnMap").bind('click', function () {
    bingMaps7.bingMaps7('findOnMap', $("#txtSearch").val()); return false;
 });

The new methods added to the Widget to handle new functionality:

Clear All

 clearMap: function () {
            logger.log("clearMap");
            that._detachEventHandlers();
            pushpins.clear();
            polylines.clear();
            shapes.clear();
        },

Undo Last Action

cancelLastAction: function () {
            that._detachEventHandlers();
            logger.log("cancelLastAction");
            logger.log("pushpins to clear: " + pushPinsStack.getLength());
            logger.log("polylines to clear: " + polylinesStack.getLength());
            logger.log("shapes to clear: " + shapesStack.getLength());

            while (true) {
                var pp = pushPinsStack.pop();
                if (pp == null) break;
                pushpins.remove(pp);
            }

            while (true) {
                var pl = polylinesStack.pop();
                if (pl == null) break;
                polylines.remove(pl);
            }

            while (true) {
                var sh = shapesStack.pop();
                if (sh == null) break;
                shapes.remove(sh);
            }

        },

Zoom To Rectangle

zoomToRectangle: function () {
            that._initializeMapAction("zoomToRectangle");

            points = [];
            var point1, point2, lastPoint;
            var isCapturingMouse = false;

            var handleRectangleZooming = function (e) {
                logger.log("handleRectangleZooming");
                if (e.targetType !== "map") return;
                logger.log(e.eventName);

                if (e.eventName === "mousedown") {
                    point1 = new MM.Point(e.getX(), e.getY());
                    lastPoint = point1;
                    isCapturingMouse = true;
                    that._addHandler(mapMousemoveEventName, handleRectangleZooming);
                } else if (isCapturingMouse && e.eventName === "mousemove") {
                    point2 = new MM.Point(e.getX(), e.getY());
                    if (!that._arePointsEqual(lastPoint, point2)) {
                        points = that._getRectangleVertices(point1, point2);
                        points.push(points[0].clone());
                        logger.log("Poly Line Points Count: " + points.length);
                        that._createPolyLine(points, lastPoint !== point1);
                        lastPoint = point2;
                    }
                } else if (e.eventName === "mouseup") {
                    that._detachEventHandlers();
                    logger.log("Finshing Zoom Rectangle ....");
                    isCapturingMouse = false;
                    point2 = new MM.Point(e.getX(), e.getY());
                    var withReplace = false;
                    if (!that._arePointsEqual(lastPoint, point2)) {
                        lastPoint = point2;
                        points = that._getRectangleVertices(point1, point2);
                        withReplace = true;
                    }
                    var drawRectangle = points.length > 0;
                    if (drawRectangle) {
                        points.push(points[0].clone());
                        that._createPolyLine(points, withReplace);

                        var cornerLocationA = map.tryPixelToLocation(points[0]);
                        var cornerLocationB = map.tryPixelToLocation(points[2]);
                        var viewRect = MM.LocationRect.fromCorners(cornerLocationA, cornerLocationB);
                        map.setView({ bounds: viewRect });
                        that.cancelLastAction();
                    } else {
                        logger.log("No rectangle to zoom into....;");
                    }
                    map.setOptions({ disablePanning: false, disableZooming: false });
                }
            }

            that._addHandler(mapMousedownEventName, handleRectangleZooming);
            that._addHandler(mapMouseupEventName, handleRectangleZooming);
        },

Find Location

findOnMap: function (searchTerm) {
            that._initializeMapAction("findOnMap");

            function geocode() {
                map.getCredentials(makeGeocodeRequest);
            }

            function makeGeocodeRequest(credentials) {

                var geocodeRequest = "http://dev.virtualearth.net/REST/v1/Locations?query=" + encodeURI(searchTerm) + "&key=" + credentials;
                $.ajax({
                    url: geocodeRequest,
                    dataType: 'jsonp',
                    jsonp: 'jsonp',
                    success: function (result) {
                        if (result &&
                                result.resourceSets &&
                                result.resourceSets.length > 0 &&
                                result.resourceSets[0].resources &&
                                result.resourceSets[0].resources.length > 0) {

                            var bbox = result.resourceSets[0].resources[0].bbox;
                            var viewBoundaries = MM.LocationRect.fromLocations(new MM.Location(bbox[0], bbox[1]), new MM.Location(bbox[2], bbox[3]));
                            map.setView({ bounds: viewBoundaries });

                            var location = new MM.Location(result.resourceSets[0].resources[0].point.coordinates[0], result.resourceSets[0].resources[0].point.coordinates[1]);
                            that._createPushpinFromLocation(location);
                            map.setOptions({ disablePanning: false, disableZooming: false });
                        }
                    }
                });
            }
            geocode();
        },

In the next post I will create a slick UI for the widget with Kendo UI.

Posted in Ajax, Bing Maps, Javascript, jQuery, Widget | Tagged , , , | Comments Off

Bing Maps Ajax Control 7.0 Widget – Drawing Circles and Rectangles

This is part 2 of the Bing Maps Ajax Control 7.0 Widget. Part 1 can be found here: Bing Maps Ajax Control 7.0 Widget – Drawing Lines and Polygons. Refer to the previous post for the introduction. In this part I will quickly show how to add rectangle and circle drawing capability.

First we need to add few controls to the HTML page hosting Bing Maps Control:

<!doctype html>
<html>
    <head>
        <title>Bings Map Widget Demo</title>
        <script type="text/javascript" src="scripts/jquery-1.7.1.js"></script>
        <script src="http://ajax.aspnetcdn.com/ajax/jquery.ui/1.8.10/jquery-ui.min.js"></script>
    </head>
<body>
    <button type="button" id="btnLineDrawCustom">Draw Line</button>
    <button type="button" id="btnPolyDrawCustom">Draw Polygon</button>
    <button type="button" id="btnRectangleDrawCustom">Draw Rectangle</button>
    <button type="button" id="btnCircleDrawCustom">Draw Circle</button>
    <input type="text" id="txtRadius" value="5" size="3" />
            <select id="ddlRadius">
				<option value="m" selected="selected">miles</option>
				<option value="k">kilometers</option>
            </select><button type="button" id="btnCircleDrawWithRadiusCustom">Draw Circle with Radius</button>
   <hr />
    <div id="bingMapsToolbox" />
    <div id='mapDiv' name='mapDiv' style="position:relative; float: left;"/>
<script type="text/javascript" src="http://ecn.dev.virtualearth.net/mapcontrol/mapcontrol.ashx?v=7.0"></script>
<script src="scripts/cm-utils.js"></script>
<script src="scripts/cm-widgets.bingMaps7.js"></script>
<script src="scripts/startup.js"></script>
</body>

Next we need to add a little bit of the jQuery code to the startup script to bind buttons click events to method calls on the Bing Maps Widget:

(function ($) {
    $(document).ready(function () {
        var bingMaps7 = $("#mapDiv").bingMaps7({
            appKey: "Your Bing Maps Key",
            zoom: 12, centerX: 38.809771, centerY: -77.0321543125,
            width: 1000, height: 800
        });
        $("#btnLineDrawCustom").bind('click', function () {
             bingMaps7.bingMaps7('drawLine'); return false;
        });
        $("#btnPolyDrawCustom").bind('click', function () {
             bingMaps7.bingMaps7('drawPolygon'); return false;
        });
        $("#btnRectangleDrawCustom").bind('click', function () {
             bingMaps7.bingMaps7('drawRectangle'); return false;
        });
        $("#btnCircleDrawCustom").bind('click', function () {
             bingMaps7.bingMaps7('drawCircle'); return false;
        });
        $("#btnCircleDrawWithRadiusCustom").bind('click', function () {
            bingMaps7.bingMaps7('drawCircleWithRadius',
                $("#txtRadius").val(),
                $("#ddlRadius :selected").val());
            return false;
        });
    });

} (jQuery));

The last step is to add rectangle and circle drawing code to the widget. Notice that I added a new class to the widget to handle finding locations on the circle given the center and the point somewhere on the circumference. I found the code for handling it in few blog posts and modified it slightly. Notice: Drawing a circle with a predefined radius requires selection of the Unit.

(function ($) {
    var map,
       mapDiv,
       jqMapDiv,
       that,
       actionType,
       step,
       points,
       lineDrawingStyle,
       polyDrawingStyle,
       polyFillStyle,
       pushpins,
       polylines,
       shapes,
       mapClickEventHandler = null,
       mapMouseDownEventHandler = null,
       mapMouseUpEventHandler = null,
       mapMouseMoveEventHandler = null,
       mapClickEventName = 'click',
       mapMousedownEventName = 'mousedown',
       mapMouseupEventName = 'mouseup',
       mapMousemoveEventName = 'mousemove',
       logger;

    //console.log("Start up");
    var MM = Microsoft.Maps;

    var bingUtils = {
        BingMapsHelper: function () {
            //console.log("BingMapsHelper");
            this.EarthRadiusInMiles = 3956.0;
            this.EarthRadiusInKilometers = 6367.0;
            this.rad = Math.PI / 180;
            this.invRad = 180 / Math.PI;
            this.angleToRadian = function (angle) {
                 return angle * this.rad;
            };
            this.radianToAngle = function (radian) {
                 return radian * this.invRad;
            };
            this.edgeSegment = 0.1;
            this.useEdgeSegment = false;

            this.getLocationsOnCircle = function (pointA, pointB) {
                var location1 = map.tryPixelToLocation(pointA);
                var location2 = map.tryPixelToLocation(pointB);

                var lat = this.angleToRadian(location1.latitude);
                var lon = this.angleToRadian(location1.longitude);
                var locs = [];
                var distanceInRadians = this.getDistanceInRadians(location1, location2);
                var latRadians, lngRadians;
                if (this.useEdgeSegment && this.edgeSegment) {
                    logger.log("Creating Circle with specified edge segment length of: "
                        + this.edgeSegment);
                    var limit = Math.ceil(2 * Math.PI / this.edgeSegment)
                        + this.edgeSegment;
                    logger.log("Limit is: " + limit);
                    var rds, rd; //, i = 0;
                    for (rds = 0; rds <= limit; rds += this.edgeSegment) {
                        rd = this.edgeSegment * rds;
                        //i++;
                        latRadians = Math.asin(Math.sin(lat) * Math.cos(distanceInRadians)
                            + Math.cos(lat) * Math.sin(distanceInRadians) * Math.cos(rd));
                        lngRadians = lon + Math.atan2(Math.sin(rd) * Math.sin(distanceInRadians) * Math.cos(lat),
                            Math.cos(distanceInRadians) - Math.sin(lat) * Math.sin(latRadians));
                        locs.push(new MM.Location(this.radianToAngle(latRadians),
                            this.radianToAngle(lngRadians)));
                    }
                } else {
                    logger.log("Creating Circle with 1 deg step");
                    for (var deg = 0; deg <= 360; deg += 1) {
                        var degAsRadian = this.angleToRadian(deg);
                        latRadians = Math.asin(Math.sin(lat) * Math.cos(distanceInRadians)
                            + Math.cos(lat) * Math.sin(distanceInRadians) * Math.cos(degAsRadian));
                        lngRadians = lon + Math.atan2(Math.sin(degAsRadian) * Math.sin(distanceInRadians) * Math.cos(lat),
                            Math.cos(distanceInRadians) - Math.sin(lat) * Math.sin(latRadians));
                        locs.push(new MM.Location(this.radianToAngle(latRadians), this.radianToAngle(lngRadians)));
                    }
                }
                logger.log("Created " + locs.length + " points.");
                return locs;
            };

            this.getLocationsOnCircleWithRadius = function (originPoint, radius, unit) {
                logger.log("Get Locations on Circle with given Radius");
                logger.log("Origin: " + originPoint + ", Radius: " + radius + ", Unit: " + unit);
                var earthRadius = unit === "m" ? this.EarthRadiusInMiles : this.EarthRadiusInKilometers;
                var originLocation = map.tryPixelToLocation(originPoint);
                var distanceAsAngle = parseFloat(radius) / earthRadius;
                var lat = this.angleToRadian(originLocation.latitude);
                var lon = this.angleToRadian(originLocation.longitude);
                var latRadians = Math.asin(Math.sin(lat) * Math.cos(distanceAsAngle)
                    + Math.cos(lat) * Math.sin(distanceAsAngle) * Math.cos(0));
                var lngRadians = lon + Math.atan2(Math.sin(0) * Math.sin(distanceAsAngle) * Math.cos(lat),
                    Math.cos(distanceAsAngle) - Math.sin(lat) * Math.sin(latRadians));
                var secondLocation = new MM.Location(this.radianToAngle(latRadians),
                    this.radianToAngle(lngRadians));
                var secondPoint = map.tryLocationToPixel(secondLocation);
                return this.getLocationsOnCircle(originPoint, secondPoint);
            };

            this.getDistanceInRadians = function (location1, location2) {
                return 2 * Math.asin(Math.min(1, Math.sqrt((Math.pow(Math.sin((this.angleToRadian(location2.latitude)
                    - this.angleToRadian(location1.latitude)) / 2.0), 2.0)
                    + Math.cos(this.angleToRadian(location1.latitude)) * Math.cos(this.angleToRadian(location2.latitude)) * Math.pow(Math.sin((this.angleToRadian(location2.longitude)
                        - this.angleToRadian(location1.longitude)) / 2.0), 2.0)))));
            };
        }
    };

    $.widget('cm-widgets.bingMaps7', {

        options: {
            width: 1000,
            height: 800,
            centerX: 38.809771,
            centerY: -77.0321543125,
            //38.889824, -77.008938
            zoom: 12,
            appKey: null,
            dataUrl: '',
            step: 20,
            lineDrawingStyle: { strokeColor: new MM.Color(255, 0, 0, 255), strokeThickness: 8 },
            polyDrawingStyle: { strokeColor: new MM.Color(255, 0, 0, 255), strokeThickness: 1 },
            polyFillStyle: { strokeColor: new MM.Color(255, 0, 0, 255),
                strokeThickness: 1, fillColor: new MM.Color(80, 0, 255, 0) },
            polyLocationsDrawingStyle: { strokeColor: new MM.Color(255, 128, 128, 128), strokeThickness: 2 },
            polyLocationsFillStyle: { strokeColor: new MM.Color(255, 0, 0, 255),
                strokeThickness: 1, fillColor: new MM.Color(80, 0, 0, 255) },
            logger: new cmUtils.Logger()
        },

        _create: function () {
            logger = this.options.logger;
            logger.enabled = false;
            that = this;
            var name = this.name, options = this.options, elem = this.element.context;
            step = options.step;
            lineDrawingStyle = options.lineDrawingStyle;
            polyDrawingStyle = options.polyDrawingStyle;
            polyFillStyle = options.polyFillStyle;
            polyLocationsDrawingStyle = options.polyLocationsDrawingStyle;
            polyLocationsFillStyle = options.polyLocationsFillStyle;
            var mapOptions = { credentials: options.appKey, zoom: options.zoom,
                center: new MM.Location(options.centerX, options.centerY) };
            mapDiv = document.getElementById(elem.id);
            mapDiv.style.height = options.height + "px";
            mapDiv.style.width = options.width + "px";
            map = new MM.Map(mapDiv, mapOptions);
            jqMapDiv = $('#' + mapDiv.id);

            pushpins = new MM.EntityCollection();
            polylines = new MM.EntityCollection();
            shapes = new MM.EntityCollection();

            map.entities.push(shapes);
            map.entities.push(polylines);
            map.entities.push(pushpins);

            logger.log("created " + name);
        },

        drawLine: function () {
            that._initializeMapAction("drawLine");
            points = [];

            var handleLineDrawingMousedown = function (e) {
                if (e.targetType !== "map") return;
                logger.log(e.eventName);
                var point = new MM.Point(e.getX(), e.getY());
                points.push(point);
                that._createPushpin(point);
                that._addHandler(mapMousemoveEventName, handleLineDrawingMousemove);
            }

            var handleLineDrawingMouseup = function (e) {
                logger.log(e);
                logger.log(e.eventName);
                //fix for chrome, ff and safari
                //if (e.targetType !== "map") return;
                if (e.eventName !== 'mouseup') {
                    logger.log("unbinding fixer");
                    jqMapDiv.unbind('mouseup', fixerHandler); return;
                }
                var point = new MM.Point(e.getX(), e.getY());
                if (!that._arePointsEqual(point, points[0])) {
                    logger.log("distance between points ok, adding second pushpin...");
                    if (points.lenght > 1) { points.pop(); }
                    points.push(point);
                    that._createPushpin(point);
                }
                else {
                    logger.log("distance between points too short, removing first pushpin...");
                    points.pop();
                    that._popPushpin();
                }
                that._detachEventHandlers();
                map.setOptions({ disablePanning: false, disableZooming: false });
            }

            var handleLineDrawingMousemove = function (e) {
                if (e.targetType !== "map") return;
                logger.log(e.eventName);
                var point = new MM.Point(e.getX(), e.getY());
                var lastPoint = (points.length === 1) ? points[0] : points[points.length - 2];
                logger.log(points.length);
                logger.log(point + "," + lastPoint);
                if (!that._arePointsEqual(point, lastPoint)) {
                    var withReplace = points.length > 1;
                    if (withReplace) points.pop();
                    points.push(point);
                    that._createLine(points[0], point, withReplace);
                }
            }

            that._addHandler(mapMousedownEventName, handleLineDrawingMousedown);
            that._addHandler(mapMouseupEventName, handleLineDrawingMouseup);
            //fix for chrome, ff and safari
            var fixerHandler = function (ev) { handleLineDrawingMouseup(ev); };
            jqMapDiv.bind('mouseup', fixerHandler);

        },

        drawPolygon: function () {
            that._initializeMapAction("drawPolygon");

            points = [];
            var firstPoint = null;
            var lastPoint = null;
            var linePoints = [];
            var isCapturingMouse = false;

            var handleLineDrawingMousedown = function (e) {
                if (e.targetType !== "map") return;
                logger.log(e.eventName);
                var point = new MM.Point(e.getX(), e.getY());
                if (firstPoint === null) {
                    points.push(point);
                    linePoints = [];
                    linePoints.push(point);
                    firstPoint = point;
                    that._createPushpin(point);
                    that._addHandler(mapMousemoveEventName, handleLineDrawingMousemove);
                    isCapturingMouse = true;
                }
                else if (that._arePointsEqual(point, lastPoint)) {
                    logger.log("clicked on the last point");
                    linePoints = [];
                    linePoints.push(lastPoint);
                    that._addHandler(mapMousemoveEventName, handleLineDrawingMousemove);
                    isCapturingMouse = true;
                }

            }

            var handleLineDrawingMouseup = function (e) {
                if (!isCapturingMouse) return;
                //fix for chrome, ff and safari
                //if (e.targetType !== "map") return;
                logger.log(e);
                logger.log(e.eventName);
                if (mapMouseMoveEventHandler !== null) MM.Events.removeHandler(mapMouseMoveEventHandler);
                if (e.eventName !== 'mouseup') {
                    logger.log("unbinding fixer");
                    jqMapDiv.unbind('mouseup', fixerHandler);
                    return;
                }
                var point = new MM.Point(e.getX(), e.getY());
                if (!that._arePointsEqual(point, linePoints[0])) {
                    if (linePoints.length > 1) { linePoints.pop(); }
                    if (points.length > 2 && that._arePointsEqual(firstPoint, point)) {
                        point = new MM.Point(firstPoint.x, firstPoint.y);
                        points.push(point);
                        that._detachEventHandlers();
                        map.setOptions({ disablePanning: false, disableZooming: false });
                        logger.log("Closing Poly....");
                        that._createLine(firstPoint, point, true, polyDrawingStyle);
                        that._createShape(points);
                        return; //done

                    }
                    lastPoint = point;
                    points.push(lastPoint);
                    that._createPushpin(lastPoint);
                    isCapturingMouse = false;
                }
                else {
                    logger.log("distance between points too short, removing first pushpin...");
                    linePoints.pop();
                    isCapturingMouse = false;
                    if (lastPoint === null) {
                        points.pop();
                        that._popPushpin();
                        that._detachEventHandlers();
                        map.setOptions({ disablePanning: false, disableZooming: false });
                        return;
                    }
                }
            }

            var handleLineDrawingMousemove = function (e) {
                if (!isCapturingMouse) return;
                if (e.targetType !== "map") return;
                logger.log(e.eventName);
                var point = new MM.Point(e.getX(), e.getY());
                var lastLinePoint = (linePoints.length === 1) ? linePoints[0] : linePoints[linePoints.length - 2];
                logger.log(linePoints.length);
                logger.log(point + "," + lastLinePoint);
                if (!that._arePointsEqual(point, lastLinePoint)) {
                    var withReplace = linePoints.length > 1;
                    if (withReplace) linePoints.pop();
                    linePoints.push(point);
                    that._createLine(lastLinePoint, point, withReplace, polyDrawingStyle);
                }
            }

            that._addHandler(mapMousedownEventName, handleLineDrawingMousedown);
            that._addHandler(mapMouseupEventName, handleLineDrawingMouseup);
            //fix for chrome, ff and safari
            var fixerHandler = function (ev) { handleLineDrawingMouseup(ev); };
            jqMapDiv.bind('mouseup', fixerHandler);
        },

        drawRectangle: function () {
            that._initializeMapAction("drawRectangle");

            points = [];
            var point1, point2, lastPoint;
            var isCapturingMouse = false;

            var handleRectangleDrawing = function (e) {
                logger.log("handleRectangleDrawing");
                if (e.targetType !== "map") return;
                logger.log(e.eventName);

                if (e.eventName === "mousedown") {
                    point1 = new MM.Point(e.getX(), e.getY());
                    lastPoint = point1;
                    isCapturingMouse = true;
                    that._addHandler(mapMousemoveEventName, handleRectangleDrawing);
                }
                else if (isCapturingMouse && e.eventName === "mousemove") {
                    point2 = new MM.Point(e.getX(), e.getY());
                    if (!that._arePointsEqual(lastPoint, point2)) {
                        points = getRectangleVertices(point1, point2);
                        points.push(points[0].clone());
                        logger.log("Poly Line Points Count: " + points.length);
                        that._createPolyLine(points, lastPoint !== point1);
                        lastPoint = point2;
                    }
                }
                else if (e.eventName === "mouseup") {
                    that._detachEventHandlers();
                    logger.log("Finshing Rectangle ....");
                    isCapturingMouse = false;
                    point2 = new MM.Point(e.getX(), e.getY());
                    var withReplace = false;
                    if (!that._arePointsEqual(lastPoint, point2)) {
                        lastPoint = point2;
                        points = getRectangleVertices(point1, point2);
                        withReplace = true;
                    }
                    var drawRectangle = points.length > 0;
                    if (drawRectangle) {
                        for (var i = 0; i < points.length; i++) {
                            that._createPushpin(points[i]);
                        }
                        points.push(points[0].clone());
                        that._createPolyLine(points, withReplace);
                        that._createShape(points);
                    }
                    else {
                        logger.log("No rectangle to draw....;");
                    }
                    map.setOptions({ disablePanning: false, disableZooming: false });
                }
            }

            function getRectangleVertices(pointA, pointC) {
                var pointB = new MM.Point(pointC.x, pointA.y);
                var pointD = new MM.Point(pointA.x, pointC.y);
                logger.log("Rectangle:");
                logger.log("A: " + pointA);
                logger.log("B: " + pointB);
                logger.log("C: " + pointC);
                logger.log("D: " + pointD);
                return [pointA, pointB, pointC, pointD];
            }

            that._addHandler(mapMousedownEventName, handleRectangleDrawing);
            that._addHandler(mapMouseupEventName, handleRectangleDrawing);
        },

        drawCircle: function () {
            that._initializeMapAction("drawCircle");

            points = [];
            var point1, point2, lastPoint; ;
            var isCapturingMouse = false;

            var handleCircleDrawing = function (e) {
                logger.log("handleCircleDrawing");
                if (e.targetType !== "map") return;
                logger.log(e.eventName);

                if (e.eventName === "mousedown") {
                    point1 = new MM.Point(e.getX(), e.getY());
                    lastPoint = point1;
                    isCapturingMouse = true;
                    that._createPushpin(point1);
                    that._addHandler(mapMousemoveEventName, handleCircleDrawing);
                }
                else if (isCapturingMouse && e.eventName === "mousemove") {
                    point2 = new MM.Point(e.getX(), e.getY());
                    if (!that._arePointsEqual(lastPoint, point2)) {
                        logger.log("need to add new point");
                        that._createLine(point1, point2, (lastPoint !== point1), polyLocationsDrawingStyle);
                        lastPoint = point2;
                    }
                }
                else if (e.eventName === "mouseup") {
                    that._detachEventHandlers();
                    logger.log("Finshing Circle ....");
                    isCapturingMouse = false;
                    point2 = new MM.Point(e.getX(), e.getY());
                    var withReplace = false;

                    if (!that._arePointsEqual(lastPoint, point2)) {
                        lastPoint = point2;
                        withReplace = true;
                    }
                    var drawCircle = !that._arePointsEqual(point1, lastPoint);
                    if (drawCircle) {
                        that._createPushpin(lastPoint);
                        that._createLine(point1, lastPoint, withReplace, polyLocationsDrawingStyle);
                        var bingHelper = new bingUtils.BingMapsHelper();
                        //bingHelper.edgeSegment = 0.05;
                        //bingHelper.useEdgeSegment = true;
                        var locations = bingHelper.getLocationsOnCircle(point1, lastPoint);
                        //that._createPolyLineFromLocations(locations, false);
                        that._createShapeFromLocations(locations);
                    }
                    else {
                        logger.log("No circle to draw....;");
                        that._popPushpin();
                    }
                    map.setOptions({ disablePanning: false, disableZooming: false });
                }
            }

            that._addHandler(mapMousedownEventName, handleCircleDrawing);
            that._addHandler(mapMouseupEventName, handleCircleDrawing);

        },

        drawCircleWithRadius: function (radius, unit) {
            that._initializeMapAction("drawCircleWithRadius");

            points = [];
            var centerPoint;

            var handleCircleDrawing = function (e) {
                logger.log("handleCircleDrawing");
                if (e.targetType !== "map") return;
                logger.log(e.eventName);

                if (e.eventName === "click") {
                    if (!radius || radius <= 0) {
                        logger.log("No circle to draw....;");
                        return;
                    }
                    that._detachEventHandlers();
                    centerPoint = new MM.Point(e.getX(), e.getY());
                    that._createPushpin(centerPoint);
                    var bingHelper = new bingUtils.BingMapsHelper();
                    //bingHelper.edgeSegment = 0.05;
                    //bingHelper.useEdgeSegment = true;
                    var locations = bingHelper.getLocationsOnCircleWithRadius(centerPoint, radius, unit);
                    //that._createPolyLineFromLocations(locations, false);
                    that._createShapeFromLocations(locations);
                    map.setOptions({ disablePanning: false, disableZooming: false });
                }
            }

            that._addHandler(mapClickEventName, handleCircleDrawing);

        },        

        _initializeMapAction: function (actionName) {
            logger.log(actionName);
            map.setOptions({ disablePanning: true, disableZooming: true });
            that._detachEventHandlers();
        },

        _addHandler: function (evtName, handler) {
            logger.log("_addHandler " + evtName);
            switch (evtName) {
                case 'mousedown':
                    mapMouseDownEventHandler = MM.Events.addHandler(map, evtName, handler);
                    break;
                case 'mouseup':
                    mapMouseUpEventHandler = MM.Events.addHandler(map, evtName, handler);
                    break;
                case 'mousemove':
                    mapMouseMoveEventHandler = MM.Events.addHandler(map, evtName, handler);
                    break;
                case 'click':
                    mapClickEventHandler = MM.Events.addHandler(map, evtName, handler);
                    break;
            }
        },

        _arePointsEqual: function (point1, point2) {
            var deltaX = point1.x - point2.x;
            var deltaY = point1.y - point2.y;
            var distance = 0.5 * (Math.sqrt(Math.pow(deltaX, 2) + Math.pow(deltaY, 2)));
            logger.log("distance - dx=" + deltaX + ", dy=" + deltaY + ": distance=" + distance + ", step=" + step);
            return distance < step;
        },

        _createPushpin: function (point) {
            logger.log("_createPushpin");
            var loc = map.tryPixelToLocation(point);
            pushpins.push(new MM.Pushpin(loc));
        },

        _popPushpin: function () {
            pushpins.pop();
        },

        _createLine: function (point1, point2, withReplace, drawingStyle) {
            logger.log("_createLine");
            var lineVertices = [map.tryPixelToLocation(point1), map.tryPixelToLocation(point2)];
            if (withReplace) polylines.pop();
            polylines.push(new MM.Polyline(lineVertices, (!drawingStyle) ? lineDrawingStyle : drawingStyle));
        },

        _createPolyLine: function (vertices, withReplace, drawingStyle) {
            logger.log("_createPolyLine");
            if (withReplace) {
                polylines.pop();
            }
            var lineVertices = [];
            for (var i = 0; i < vertices.length; i++) {
                var location = map.tryPixelToLocation(vertices[i]);
                lineVertices.push(location);
                logger.log(location);
            }
            polylines.push(new MM.Polyline(lineVertices, (!drawingStyle) ? polyDrawingStyle : drawingStyle));
        },

        _createPolyLineFromLocations: function (locations, withReplace, drawingStyle) {
            logger.log("_createPolyLineFromLocations");
            if (withReplace) {
                polylines.pop();
            }
            polylines.push(new MM.Polyline(locations, (!drawingStyle) ? polyLocationsDrawingStyle : drawingStyle));
        },

        _createShape: function (shapePoints, shapeStyle) {
            logger.log("_createShape");
            var locations = [];
            for (var i = 0; i < shapePoints.length; i++) {
                var loc = map.tryPixelToLocation(shapePoints[i]);
                locations.push(loc);
                logger.log("Point: " + shapePoints[i].x + ", " + shapePoints[i].y);
                logger.log("Loc: " + loc.longitude + ", " + loc.latitude);
            }
            shapes.push(new MM.Polygon(locations, (!shapeStyle) ? polyFillStyle : shapeStyle));
        },

        _createShapeFromLocations: function (locations, shapeStyle) {
            logger.log("_createShapeFromLocations");
            shapes.push(new MM.Polygon(locations, (!shapeStyle) ? polyLocationsFillStyle : shapeStyle));
        },

        _detachEventHandlers: function () {
            logger.log("_detachEventHandlers");
            if (mapClickEventHandler !== null) MM.Events.removeHandler(mapClickEventHandler);
            if (mapMouseDownEventHandler !== null) MM.Events.removeHandler(mapMouseDownEventHandler);
            if (mapMouseUpEventHandler !== null) MM.Events.removeHandler(mapMouseUpEventHandler);
            if (mapMouseMoveEventHandler !== null) MM.Events.removeHandler(mapMouseMoveEventHandler);
        },

        destroy: function () {
            logger.log("destroy");
            $.Widget.prototype.destroy.call(this);
            this.mapDiv.remove();
        }

    });

} (jQuery));

The resulting Widget:

Bing Maps Widget Screen - Part 2

Bing Maps Widget Screen - Part 2

In the next blog post I will add a few more features: Clear All, Undo Last Action, Search and Custom Zooming.

Posted in Ajax, Bing Maps, Javascript, jQuery, Widget | Tagged , , , | Comments Off

Bing Maps Ajax Control 7.0 Widget – Drawing Lines and Polygons

I recently explored Project Silk and experimented with Bing Maps Ajax Control 7.0 SDK. During the process I got an idea of creating a jQuery Widget for the latest version of Bing Maps SDK. Bing Maps Ajax Control 7.0 is a very lightweight version of its predecessor. You can explore an interactive demo of the SDK here or you can study the APIs in depth here.

The best tutorials about jQuery Widgets that I found are: Project Silk – Chapter 3 and Project Silk – Chapter 14 and of course jQuery Widget Tutorial on a jQuery site.

The Bing Maps Ajax Control Widget enhances the control by adding line and polygon drawing capabilities. In the next post I will expand drawing capabilities with rectangle and circle drawing.

HTML Page to Host the Widget

Making a widget out of Bing Map ontrol is easy. This is the bare bones html page that includes the Bing Maps Control.

<!doctype html>
<html>
    <head>
        <title>Bings Map Widget Demo</title>
        <script type="text/javascript" src="scripts/jquery-1.7.1.js"></script>
        <script src="http://ajax.aspnetcdn.com/ajax/jquery.ui/1.8.10/jquery-ui.min.js"></script>
    </head>
<body>
    <button type="button" id="btnLineDrawCustom">Draw Line</button>
    <button type="button" id="btnPolyDrawCustom">Draw Polygon</button>
   <hr />
    <div id="bingMapsToolbox" />
    <div id='mapDiv' name='mapDiv' style="position:relative; float: left;"/>
<script type="text/javascript" src="http://ecn.dev.virtualearth.net/mapcontrol/mapcontrol.ashx?v=7.0"></script>
<script src="scripts/cm-utils.js"></script>
<script src="scripts/cm-widgets.bingMaps7.js"></script>
<script src="scripts/startup.js"></script>
</body>
</html>

Buttons on the top are for testing the functionality and can be replaced with something else.

Listig of the included Javascript files

  • jQuery
  • jQuery Ui
  • Microsoft Bing Maps AJax Control
  • Helper utility for logging
  • Bing Maps Control Drawing Widget
  • Start-up script

Helper utility for logging

var cmUtils = {
    logType: { log: "log", info: "info", warn: "warn", error: "error" },
    Logger: function () {
        this.enabled = true;
        this.log = function (message, severity) {
            if (!this.enabled) return;
            var reason = severity || cmUtils.logType.log;
            switch (reason) {
                case cmUtils.logType.info:
                    console.info(message);
                    return;
                case cmUtils.logType.warn:
                    console.warn(message);
                    return;
                case cmUtils.logType.error:
                    console.error(message);
                    return;
                case cmUtils.logType.log:
                    console.log(message);
                    return;
            }
        }
    }
};

This is a helper logging class. It works with all latest version of the most popular browsers(IE, FF, Chrome, Safari).

* NoteIn IE you need to run the Developer tools otherwise you will get an error if you eneabel the logging.

Start-up script

(function ($) {
    $(document).ready(function () {
        var bingMaps7 = $("#mapDiv").bingMaps7({
            appKey: "Your Bing Maps key here", zoom: 12, centerX: 38.809771, centerY: -77.0321543125, width: 1000, height: 800
        });
        $("#btnLineDrawCustom").bind('click', function () { bingMaps7.bingMaps7('drawLine'); return false; });
        $("#btnPolyDrawCustom").bind('click', function () { bingMaps7.bingMaps7('drawPolygon'); return false; });
    });
} (jQuery));

This code initializes the widget and wires-up the buttons for testing.

Bing Maps Control Drawing Widget

//http://www.jslint.com/
(function ($) {

    var map,
       mapDiv,
       jqMapDiv,
       that,
       actionType,
       step,
       points,
       lineDrawingStyle,
       polyDrawingStyle,
       polyFillStyle,
       pushpins,
       polylines,
       shapes,
    //mapClickEventHandler = null,
       mapMouseDownEventHandler = null,
       mapMouseUpEventHandler = null,
       mapMouseMoveEventHandler = null,
    //mapClickEventName = 'click',
       mapMousedownEventName = 'mousedown',
       mapMouseupEventName = 'mouseup',
       mapMousemoveEventName = 'mousemove',
       logger;

    //console.log("Start up");
    var MM = Microsoft.Maps;

    $.widget('cm-widgets.bingMaps7', {

        options: {
            width: 1000,
            height: 800,
            centerX: 38.809771,
            centerY: -77.0321543125,
            zoom: 12,
            appKey: null,
            dataUrl: '',
            step: 20,
            lineDrawingStyle: { strokeColor: new MM.Color(255, 0, 0, 255), strokeThickness: 8 },
            polyDrawingStyle: { strokeColor: new MM.Color(255, 0, 0, 255), strokeThickness: 1 },
            polyFillStyle: { strokeColor: new MM.Color(255, 0, 0, 255), strokeThickness: 1, fillColor: new MM.Color(80, 0, 255, 0) },
            logger: new cmUtils.Logger()
        },

        _create: function () {
            logger = this.options.logger;
            logger.enabled = false;
            that = this;
            var name = this.name, options = this.options, elem = this.element.context;
            step = options.step;
            lineDrawingStyle = options.lineDrawingStyle;
            polyDrawingStyle = options.polyDrawingStyle;
            polyFillStyle = options.polyFillStyle;
            var mapOptions = { credentials: options.appKey, zoom: options.zoom, center: new MM.Location(options.centerX, options.centerY) };
            mapDiv = document.getElementById(elem.id);
            mapDiv.style.height = options.height + "px";
            mapDiv.style.width = options.width + "px";
            map = new MM.Map(mapDiv, mapOptions);
            jqMapDiv = $('#' + mapDiv.id);

            pushpins = new MM.EntityCollection();
            polylines = new MM.EntityCollection();
            shapes = new MM.EntityCollection();

            map.entities.push(shapes);
            map.entities.push(polylines);
            map.entities.push(pushpins);

            logger.log("created " + name);
        },

        drawLine: function () {
            logger.log("drawLine");

            map.setOptions({ disablePanning: true, disableZooming: true });
            that._detachEventHandlers();

            points = new Array();

            var handleLineDrawingMousedown = function (e) {
                if (e.targetType !== "map") return;
                logger.log(e.eventName);
                var point = new MM.Point(e.getX(), e.getY());
                points.push(point);
                that._createPushpin(point);
                that._addHandler(mapMousemoveEventName, handleLineDrawingMousemove);
            }

            var handleLineDrawingMouseup = function (e) {
                logger.log(e);
                logger.log(e.eventName);
                //fix for chrome, ff and safari
                //if (e.targetType !== "map") return;
                if (e.eventName !== 'mouseup') {
                    logger.log("unbinding fixer");
                    jqMapDiv.unbind('mouseup', fixerHandler); return;
                }
                //logger.log(e.eventName);
                var point = new MM.Point(e.getX(), e.getY());
                if (!that._arePointsEqual(point, points[0])) {
                    logger.log("distance between points ok, adding second pushpin...");
                    if (points.lenght > 1) { points.pop(); }
                    points.push(point);
                    that._createPushpin(point);
                }
                else {
                    logger.log("distance between points too short, removing first pushpin...");
                    points.pop();
                    that._popPushpin();
                }
                that._detachEventHandlers();
                map.setOptions({ disablePanning: false, disableZooming: false });
            }

            var handleLineDrawingMousemove = function (e) {
                if (e.targetType !== "map") return;
                logger.log(e.eventName);
                var point = new MM.Point(e.getX(), e.getY());
                var lastPoint = (points.length == 1) ? points[0] : points[points.length - 2];
                logger.log(points.length);
                logger.log(point + "," + lastPoint);
                if (!that._arePointsEqual(point, lastPoint)) {
                    var withReplace = points.length > 1;
                    if (withReplace) points.pop();
                    points.push(point);
                    that._createLine(points[0], point, withReplace);
                }
            }

            that._addHandler(mapMousedownEventName, handleLineDrawingMousedown);
            that._addHandler(mapMouseupEventName, handleLineDrawingMouseup);
            //fix for chrome, ff and safari
            var fixerHandler = function () { handleLineDrawingMouseup(event); };
            jqMapDiv.bind('mouseup', fixerHandler);

        },

        drawPolygon: function () {
            logger.log("drawPolygon");
            map.setOptions({ disablePanning: true, disableZooming: true });
            that._detachEventHandlers();

            points = new Array();
            var firstPoint = null;
            var lastPoint = null;
            var linePoints = new Array();
            var isCapturingMouse = false;

            var handleLineDrawingMousedown = function (e) {
                if (e.targetType !== "map") return;
                logger.log(e.eventName);
                var point = new MM.Point(e.getX(), e.getY());
                if (firstPoint === null) {
                    points.push(point);
                    linePoints = new Array();
                    linePoints.push(point);
                    firstPoint = point;
                    that._createPushpin(point);
                    that._addHandler(mapMousemoveEventName, handleLineDrawingMousemove);
                    isCapturingMouse = true;
                }
                else if (that._arePointsEqual(point, lastPoint)) {
                    logger.log("clicked on the last point");
                    linePoints = new Array();
                    linePoints.push(lastPoint);
                    that._addHandler(mapMousemoveEventName, handleLineDrawingMousemove);
                    isCapturingMouse = true;
                }

            }

            var handleLineDrawingMouseup = function (e) {
                if (!isCapturingMouse) return;
                //fix for chrome, ff and safari
                //if (e.targetType !== "map") return;
                logger.log(e);
                logger.log(e.eventName);
                if (mapMouseMoveEventHandler !== null) MM.Events.removeHandler(mapMouseMoveEventHandler);
                if (e.eventName !== 'mouseup') {
                    logger.log("unbinding fixer");
                    jqMapDiv.unbind('mouseup', fixerHandler);
                    return;
                }
                var point = new MM.Point(e.getX(), e.getY());
                if (!that._arePointsEqual(point, linePoints[0])) {
                    if (linePoints.length > 1) { linePoints.pop(); }
                    if (points.length > 2 && that._arePointsEqual(firstPoint, point)) {
                        point = new MM.Point(firstPoint.x, firstPoint.y);
                        points.push(point);
                        that._detachEventHandlers();
                        map.setOptions({ disablePanning: false, disableZooming: false });
                        logger.log("Closing Poly....");
                        that._createShape(points);
                        return; //done

                    }
                    lastPoint = point;
                    points.push(lastPoint);
                    that._createPushpin(lastPoint);
                    isCapturingMouse = false;
                }
                else {
                    logger.log("distance between points too short, removing first pushpin...");
                    linePoints.pop();
                    isCapturingMouse = false;
                    if (lastPoint === null) {
                        points.pop();
                        that._popPushpin();
                        that._detachEventHandlers();
                        map.setOptions({ disablePanning: false, disableZooming: false });
                        return;
                    }
                }
            }

            var handleLineDrawingMousemove = function (e) {
                if (!isCapturingMouse) return;
                if (e.targetType !== "map") return;
                logger.log(e.eventName);
                var point = new MM.Point(e.getX(), e.getY());
                var lastLinePoint = (linePoints.length == 1) ? linePoints[0] : linePoints[linePoints.length - 2];
                logger.log(linePoints.length);
                logger.log(point + "," + lastLinePoint);
                if (!that._arePointsEqual(point, lastLinePoint)) {
                    var withReplace = linePoints.length > 1;
                    if (withReplace) linePoints.pop();
                    linePoints.push(point);
                    that._createLine(lastLinePoint, point, withReplace, polyDrawingStyle);
                }
            }

            that._addHandler(mapMousedownEventName, handleLineDrawingMousedown);
            that._addHandler(mapMouseupEventName, handleLineDrawingMouseup);
            //fix for chrome, ff and safari
            var fixerHandler = function () { handleLineDrawingMouseup(event); };
            jqMapDiv.bind('mouseup', fixerHandler);
        },

        _addHandler: function (evtName, handler) {
            logger.log("_addHandler " + evtName);
            switch (evtName) {
                case 'mousedown':
                    mapMouseDownEventHandler = MM.Events.addHandler(map, evtName, handler);
                    break;
                case 'mouseup':
                    mapMouseUpEventHandler = MM.Events.addHandler(map, evtName, handler);
                    break;
                case 'mousemove':
                    mapMouseMoveEventHandler = MM.Events.addHandler(map, evtName, handler);
                    break;
                //                case 'click':
                //                    mapClickEventHandler = MM.Events.addHandler(map, evtName, handler);
                //                    break;
            }
        },

        _arePointsEqual: function (point1, point2) {
            var deltaX = point1.x - point2.x;
            var deltaY = point1.y - point2.y;
            var distance = 0.5 * (Math.sqrt(Math.pow(deltaX, 2) + Math.pow(deltaY, 2)));
            logger.log("distance - dx=" + deltaX + ", dy=" + deltaY + ": distance=" + distance + ", step=" + step);
            return distance < step;
        },

        _createPushpin: function (point) {
            logger.log("_createPushpin");
            var loc = map.tryPixelToLocation(point);
            pushpins.push(new MM.Pushpin(loc));
        },

        _popPushpin: function () {
            pushpins.pop();
        },

        _createLine: function (point1, point2, withReplace, drawingStyle) {
            logger.log("_createLine");
            var lineVertices = new Array(map.tryPixelToLocation(point1), map.tryPixelToLocation(point2));
            if (withReplace) polylines.pop();
            polylines.push(new MM.Polyline(lineVertices, (!drawingStyle) ? lineDrawingStyle : drawingStyle));
        },

        _createShape: function (shapePoints, shapeStyle) {
            logger.log("_createShape");
            var locations = new Array();
            for (var i = 0; i < shapePoints.length; i++) {
                var loc = map.tryPixelToLocation(shapePoints[i]);
                locations.push(loc);
                logger.log("Point: " + shapePoints[i].x + ", " + shapePoints[i].y);
                logger.log("Loc: " + loc.longitude + ", " + loc.latitude);
            }
            shapes.push(new MM.Polygon(locations, (!shapeStyle) ? polyFillStyle : shapeStyle));
        },

        _detachEventHandlers: function () {
            logger.log("_detachEventHandlers");
            //if (mapClickEventHandler !== null) MM.Events.removeHandler(mapClickEventHandler);
            if (mapMouseDownEventHandler !== null) MM.Events.removeHandler(mapMouseDownEventHandler);
            if (mapMouseUpEventHandler !== null) MM.Events.removeHandler(mapMouseUpEventHandler);
            if (mapMouseMoveEventHandler !== null) MM.Events.removeHandler(mapMouseMoveEventHandler);
        },
        destroy: function () {
            logger.log("destroy");
            $.Widget.prototype.destroy.call(this);
            this.mapDiv.remove();
        }
    });
} (jQuery));

The code should be self-explanatory (at leats I hope so).

Bing Maps Ajax Control jQuery Widget in Action

Bing Maps Ajax Control jQuery Widget in Action

In the next post I will add rectangle and circle drawing.

Posted in Ajax, Bing Maps, Javascript, jQuery, Widget | Tagged , , , | Comments Off

Cross-Platform Mobile Development with WP7, PhoneGap and .NET 4.0 WCF REST – Part 3

This is Part 3 of the walkthrough that demonstrates cross-platform mobile application development with PhoneGap, WP7 and .NET 4.0 WCF REST Services.

For a description of the PhoneGap, for a WP7 installation review Part 1. For a descripion of the .NET 4.0 WCF REST Template installation refer to Part 2.

Developing in a PhoneGap application is quite cumbersome. It takes too much time to start debugging and push the code to the phone emulator or the actual device. In order to expedite the process my suggestion is to develop and run web pages, css styles and javascript files in the WCF Rest Services application. That way there is no problem with AJAX and CORS (Cross-Origin Resource Sharing) when testing and all the code, with just few corrections, can be just copied and pasted to PhoneGap application (both for WP7 or iPad).

One problem that needs to be solved when using web pages running in the browser for PhoneGap development is the need to emulate device specific functionality, which is expected by the PhoneGap template code. This can be seen below in the copy of the index.html page supplied with the template. Notice PhoneGap specific event listener in init() function:

function init()
        {
            document.addEventListener("deviceready",
                onDeviceReady,false);
        }

The full index.html page:

<!DOCTYPE html>
<html>
  <head>
    <meta name="viewport" content="width=320; user-scalable=no" />
    <meta http-equiv="Content-type" content="text/html; charset=utf-8"/>

    <title>PhoneGap WP7</title>

	  <link rel="stylesheet" href="master.css" type="text/css" media="screen" title="no title" charset="utf-8"/>

	  <script type="text/javascript">
		  // provide our own console if it does not exist, huge dev aid!
		  if(typeof window.console == "undefined")
		  {
		  window.console = {log:function(str){window.external.Notify(str);}};
		  }

		  // output any errors to console log, created above.
		  window.onerror=function(e)
		  {
			  console.log("window.onerror ::" + JSON.stringify(e));
		  };

		  console.log("Installed console ! ");
      </script>

      <script type="text/javascript" charset="utf-8" src="phonegap-1.1.0.js"></script>

      <script type="text/javascript">
        function init()
        {
        document.addEventListener("deviceready",onDeviceReady,false);
        }

        // once the device ready event fires, you can safely do your thing! -jm
        function onDeviceReady()
        {
            document.getElementById("welcomeMsg").innerHTML += "PhoneGap is ready!";
        }
      </script>  

  </head>
  <body onLoad="init();">
    <h1>Hello PhoneGap</h1>
    <div id="welcomeMsg"></div>
  </body>
</html>

A very good solution to it (which I borrowed) can be found at code project (author: Colin Eberhardt). PhoneGapMock.js is a javascript code file that does the trick.

You should also expect problems with the “windows.console” code in the index.html. It will work in Chrome for example but not in IE.

The next step is to separte javascript code into separate files. The final content of the www folder inside the WCF template project should look like this:

www folder in WCF Rest Services Web Application

www folder in WCF Rest Services Web Application

The content of the files:

index.html

<!doctype html>
<html>
<head>
    <meta name="viewport" content="width=320 user-scalable=no" />
    <meta http-equiv="Content-type" content="text/html; charset=utf-8"/>
    <title>PhoneGap WP7</title>
    <link rel="stylesheet" href="master.css" type="text/css" media="screen" title="no title" charset="utf-8" />
    <script type="text/javascript" charset="utf-8" src="console.js"></script>
    <script type="text/javascript" charset="utf-8" src="jquery-1.6.4.min.js"></script>
    <script type="text/javascript" charset="utf-8" src="phonegapMock.js"></script>
    <script type="text/javascript" charset="utf-8" src="init.js"></script>
</head>
<body>
  <div>
    <h1 id="welcomeMsg">Welcome</h1>
    <p><a href="#" onclick="login(); return false;">log in</a></p>
    <p><a href="#" onclick="getAjax(); return false;">Get Ajax</a></p>
    <p><a href="#" onclick="postAjax(); return false;">Post Ajax</a></p>
    <p><a href="#" onclick="getSingle(); return false;">Get Single Item</a></p>
    <p><a href="#" onclick="deleteSingle(); return false;">Delete</a></p>
    <p><a href="#" onclick="updateSingle(); return false;">Update</a></p>
    <p><a href="#" onclick="identify(); return false;">Identify</a></p>
    <p><a href="#" onclick="logViaForm(); return false;">Log via Form</a></p>
    <p><a href="#" onclick="logout(); return false;">log out</a></p>

    <p id="errorMessage" class="err"></p>
    <p id="loginCall"></p>
    <p id="ajaxCall"></p>
    <p id="postAjaxCall"></p>
    <p id="getSingleCall"></p>
    <p id="deleteSingleCall"></p>
    <p id="updateSingleCall"></p>
    <p id="identifyCall"></p>
    <p id="logViaFormCall"></p>
    <p id="logoutCall"></p>
    <input type="text" id="myTest" value="1" name="myTest" />

   </div>

  </body>
</html>

console.js

// provide our own console if it does not exist, huge dev aid!
if (typeof window.console == "undefined") {
    window.console = { log: function (str) { window.external.Notify(str); } };
}

// output any errors to console log, created above.
window.onerror = function (e) {
    console.log("window.onerror ::" + JSON.stringify(e));
};

console.log("Installed console ! ");

phoneGapMock.js

document.addEventListener = function (evt, handler, capture) {
  $("body").bind(evt, handler);
};

$(document).ready(function () {
  setTimeout(function () {
    $("body").trigger("deviceready");
  }, 100);
});

init.js


$(document).ready(function () {
    document.addEventListener("deviceready", onDeviceReady, false);
});

// phonegap is initialised
function onDeviceReady() {
    $("#welcomeMsg").append("...Ready");
}

function showAlert(msg) {
    //alert(msg);
}

function showError(error, otherInfo) {
    var element = document.getElementById('errorMessage');
    element.innerHTML = "Errors: " + error.Message + "<br>" + (otherInfo ? otherInfo : "");
}

function getAjax() {
    var jqxhr = $.ajax({
        url: '../service1/',
        //headers:
        beforeSend: function (xhr) {
            //xhr.overrideMimeType('text/plain; charset=x-user-defined');
        },
        dataType: 'json'
    })
    .done(function (data) {
        var element = document.getElementById('ajaxCall');
        element.innerHTML = JSON.stringify(data, null, "\t");
    })
     .fail(function (xhr, status, error) {
         showError(error);
     })
     .always(function () { showAlert("complete"); });
}

function postAjax(parameters) {
    var jqxhr = $.ajax({
        url: '../service1/',
        type: 'POST',
        //headers:
        //beforeSend: function (xhr) {
        //},
        dataType: 'json',
        contentType: 'application/json; charset=utf-8',
        data: '{ "Id":2, "StringValue": "jerry" }'
    })
    .done(function (data) {
        var element = document.getElementById('postAjaxCall');
        element.innerHTML = JSON.stringify(data, null, "\t");
    })
     .fail(function (xhr, status, error) { showError(error); })
     .always(function () { showAlert("complete"); });
}

function login() {
    var jqxhr = $.ajax({
        url: '../login/',
        type: 'POST',
        //headers:
        //beforeSend: function (xhr) {
        //},
        dataType: 'json',
        contentType: 'application/json; charset=utf-8',
        data: '{ "Username":"test", "Password": "test" }'
    })
    .done(function (data) {
        var element = document.getElementById('loginCall');
        element.innerHTML = "Login Succesfull ? " + data;
    })
    .fail(function (xhr, status, error) { showError(error); })
    .always(function () { showAlert("complete"); });
}

function logout() {
    var jqxhr = $.ajax({
        url: '../login/logout',
        type: 'POST',
        //headers:
        //beforeSend: function (xhr) {
        //},
        dataType: 'json',
        contentType: 'application/json; charset=utf-8'
    })
    .done(function (data) {
        var element = document.getElementById('logoutCall');
        element.innerHTML = "Login Out Succesfull ? " + data;
    })
    .fail(function (xhr, status, error) { showError(error); })
    .always(function () { showAlert("complete"); });
}

function getSingle(parameters) {
    var jqxhr = $.ajax({
        url: '../service1/88',
        type: 'GET',
        //headers:
        beforeSend: function (xhr) {
            //xhr.overrideMimeType('text/plain; charset=x-user-defined');
        },
        dataType: 'json',
        contentType: 'application/json; charset=utf-8'
    })
    .done(function (data) {
        var element = document.getElementById('getSingleCall');
        element.innerHTML = JSON.stringify(data, null, "\t");
    })
    .fail(function (xhr, status, error) { showError(error); })
    .always(function () { showAlert("complete"); });
}

function deleteSingle(parameters) {
    var jqxhr = $.ajax({
        url: '../service1/88',
        type: 'DELETE',
        //headers:
        beforeSend: function (xhr) {
            //xhr.overrideMimeType('text/plain; charset=x-user-defined');
        },
        dataType: 'json',
        contentType: 'application/json; charset=utf-8'
    })
    .done(function (data) {
        var element = document.getElementById('deleteSingleCall');
        element.innerHTML = JSON.stringify(data, null, "\t");
    })
     .fail(function (xhr, status, error) { showError(error); })
     .always(function () { showAlert("complete"); });
}

function updateSingle(parameters) {
    var jqxhr = $.ajax({
        url: '../service1/99',
        type: 'PUT',
        //headers:
        beforeSend: function (xhr) {
            //xhr.overrideMimeType('text/plain; charset=x-user-defined');
        },
        dataType: 'json',
        contentType: 'application/json; charset=utf-8',
        data: '{ "Id":99, "StringValue": "JERRY " }'
    })
    .done(function (data) {
        var element = document.getElementById('updateSingleCall');
        element.innerHTML = JSON.stringify(data, null, "\t");
    })
    .fail(function (xhr, status, error) { showError(error); })
    .always(function () { showAlert("complete"); });
}

function identify(parameters) {
    var jqxhr = $.ajax({
        url: '../login/identify',
        type: 'GET',
        dataType: 'json',
        contentType: 'application/json; charset=utf-8'
    })
     .done(function (data) {
         var element = document.getElementById('identifyCall');
         element.innerHTML = JSON.stringify(data, null, "\t");
     })
      .fail(function (xhr, status, error) { showError(error); })
      .always(function () { showAlert("complete"); });
}

function logViaForm() {
    var jqxhr = $.ajax({
        url: '../login.aspx',
        type: 'GET',
        dataType: 'html'
    })
                .done(function (data) {
                    var eventVal = $(data).find('#__EVENTVALIDATION').attr('value');
                    var viewState = $(data).find('#__VIEWSTATE').attr('value');
                    //build post data
                    var postData = { __VIEWSTATE: viewState, __EVENTVALIDATION: eventVal, UserName: "test", Password: "test", LoginButton: "Log In" };

                    var jqxhr1 = $.ajax({
                        url: '../login.aspx',
                        type: 'POST',
                        dataType: 'html',
                        contentType: 'application/x-www-form-urlencoded; charset=utf-8',
                        data: postData
                    })
                    .done(function (data1, status, jqxhr1) {
                        //this works but we will get an error dues to the redirect to the home.aspx
                        //TODO: need to handle that
                        var element = document.getElementById('logViaFormCall');
                        element.innerHTML = "Login Succesfull ! " + jqxhr1.status;
                    })
                     .fail(function (xhr, status, error) {
                         showError(error, "TODO: Works but need to handle redirect!!");

                         //but it really works!
                         var element = document.getElementById('logViaFormCall');
                         element.innerHTML = "Login Succesfull ! Verify that Authenticated AJAX calls work!";
                     })
                     .always(function () { showAlert("complete login"); });
                })
                .fail(function (xhr, status, error) { showError(error); })
                .always(function () { showAlert("complete"); });
}

Web Page Preview:

Web Page Preview in Chrome

Web Page Preview in Chrome

The Web Page View after testing all functionality:

Web Page after testing functionality

Web Page after testing functionality

To Note:

  • AJAX requests for protected resources will succeed only after a call to the login service (“log in” link) or after a call to the login form (“Log via Form” link).
  • The code will not handle the redirects for failed unauthenticated AJAX requests
  • The code will not fully handle the login via the form (it will fail on the redirect to home.aspx), but the login will succeed and the subsequent AJAX requests will work. The error in red is the result of that redirect that is not beinh handled.

The Result:
The above results page demonstrates that GET, POST, PUT and DELETE AJAX requests are handled by .NET WCF REST Services hosted in the web application with FBA enabled.

Transferring HTML application to PhoneGap Application

  • Copy the content of the www folder to PhoneGap’s www folder with an exception of the PhoneGapMock.js file (also remove the link to PhoneGapMock.js from index.html.
  • Modify showAlert() function to use native dialogs. See below.
  • Modify all urls in the code to use an absolute link (for example): url: ‘https://machine.domain.com/appName/service1′
  • Modified showAlert() function:

    function showAlert(msg) {
                    navigator.notification.alert(
                        msg, // message
                        alertDismissed, // callback
                        'Alert', // title
                        'Done'  // buttonName
                        );
                }
    

    That’s all that should be needed to run the application on iPad.

    Windows Phone Only:
    Add CORS support in jQuery by adding one line of the code to $(document).ready(function ()…

    $(document).ready(function () {
        document.addEventListener("deviceready", onDeviceReady, false);
        jQuery.support.cors = true; //Cross-Origin Resource Sharing
    });
    

    Also (for Windows Phone 7), right click GapSourceDictionary.tt file and execute “Run Custom Tool” to make sure that all added files will be included in the build and transferred to the device.

    Final PhoneGap application files:

    www folder in PhoneGap application

    www folder in PhoneGap application

    Summary

    This post demonstrates PhoneGap application code that runs on both iOS and Windows Phone 7 devices. The application also runs as a web application (client side code only) which makes development more robust. It’s easier to troubleshot and test functionality in the regular browser during the initial development phase than using a simulator/emulator or the physical device for that purpose.

    Moreover, the post demonstrates how to create PhoneGap application utilizing .NET WCF REST Services hosted in a web application protected by Forms Based Authentication. This might be a likely scenario for legacy LOB applications.

    Test PhoneGap application was developed for and deployed to iPad and WP7.

Posted in .NET, iPad, Mobile, PhoneGap, VS 2010, WCF REST, Windows Phone, WP7 | Tagged , , , , , , | 3 Comments

Cross-Platform Mobile Development with WP7, PhoneGap and .NET 4.0 WCF REST – Part 2

This is Part 2 of the walkthrough that demonstrates cross-platform mobile application development with PhoneGap, WP7 and .NET 4.0 WCF REST Services.

The following section describes modifications to the installed, as described in Part 1 of this blog, .NET 4.0 WCF REST Template.

1) Add Forms Authentication to web.config file. The mobile application will be accessing protected data. Make note of the setting cookieless=”UseCookies”. This is to make sure that iPad Simulator will be correctly recognized as supporting cookies. I got this troubleshooting tip from here.

<authentication mode="Forms"> 
<forms defaultUrl="home.aspx" timeout="20"
     ticketCompatibilityMode="Framework40"
     loginUrl="login.aspx" name="Mobile-Rest-Api"
cookieless="UseCookies"/>
</authentication> 
<authorization> 
<deny users="?" /> <allow users="*"/>
 </authorization>

 

2) Add additional forms to the WcfRestServiceApp to test forms authentication:

  • home.aspx
  • login.aspx

The markup for the login page:

<form id=”form1″ runat=”server”>
<div>
<fieldset> <legend>Log In</legend>
<p>
<asp:Label ID=”UserNameLabel” runat=”server”
AssociatedControlID=”UserName”> Username:</asp:Label>
<asp:TextBox ID=”UserName” runat=”server” ></asp:TextBox>
</p>
<p><asp:Label ID=”PasswordLabel” runat=”server”
AssociatedControlID=”Password”> Password:</asp:Label>
<asp:TextBox ID=”Password” runat=”server” TextMode=”Password”></asp:TextBox>
</p>
</fieldset>
<p ><asp:Button ID=”LoginButton” runat=”server” CommandName=”Login” Text=”Log In” OnClick=”LoginButton_Click” />
</p>
<p><asp:label runat=”server” ID=”lblError” ForeColor=”red”></asp:label>
</p>
</div>
</form>

 

The code for the login page:

protected void Page_Load(object sender, EventArgs e)
{

}

protected void LoginButton_Click(Object source, EventArgs e)
{
    Session.Clear();

    if (IsValid)
    {
        var userNameAsEntered = UserName.Text;
        var userPassword = Password.Text;

        if (userNameAsEntered == "test" && userPassword == "test")
        {
            lblError.Text = String.Empty;
            FormsAuthentication.RedirectFromLoginPage(
                userNameAsEntered, false);
        }
        lblError.Text = "Inalid Login";
    }
}

 

The markup for the home page:

<form id="form1" runat="server"> <div> Home Page! Welcome:
<p> <asp:loginname ID="Loginname1" runat="server" /></p> </div> 
</form> 

Make sure that Forms Authentication works and that home page can be accesses only after the successful login (test,test).

3) Modify Global.aspx. Add Route to the login service:

private void RegisterRoutes()
       {
           RouteTable.Routes.Add(new ServiceRoute(
               "Service1", new WebServiceHostFactory(),
               typeof(Service1)));

RouteTable.Routes.Add(new ServiceRoute( “Login”, new WebServiceHostFactory(), typeof(LoginManager)));

       }

 

Make sure that the login service is accessible to unauthenticated users. Add location path to the web.config:

  <location path="login"> <system.web> 
<authorization> <allow users="*" /> </authorization> 
</system.web> </location> 

Also make sure that Service1 path can only be accessed by authenticated users.

4) Add two new classes LoginManager.cs and ServiceUser.cs.

public class ServiceUser {
    public string Username { get; set; }
    public string Password { get; set; }
}
[ServiceContract]
    [AspNetCompatibilityRequirements(
        RequirementsMode =
        AspNetCompatibilityRequirementsMode.Allowed)]
    [ServiceBehavior(
        InstanceContextMode = InstanceContextMode.PerCall)]
    public class LoginManager {
        [WebInvoke(UriTemplate = "", Method = "POST")]
        public bool Login(ServiceUser user)
        {
            //TODO: Add real login if ( user.Username == "test" 
&& user.Password == "test" )
            {
                FormsAuthentication.SetAuthCookie(
                    user.Username, false);
                return true;
            }
            return false;
        }

        [WebInvoke(UriTemplate = "logout", Method = "POST")]
        public bool LogOut(string userName)
        {
            FormsAuthentication.SignOut();
            return true;
        }

        [WebGet(UriTemplate = "identify")]
        public string Identify()
        {
            var uri = OperationContext.Current
                .Channel.LocalAddress.ToString();
            return uri;

        }
    }

 

5) Modify Service1.cs class. Add some code to unfinished template methods. We will test all of them.

// Start the service and browse to 
//http://<machine_name>:<port>/Service1/help 
//to view the service's generated help page 
[ServiceContract]
    [AspNetCompatibilityRequirements(
        RequirementsMode =
        AspNetCompatibilityRequirementsMode.Allowed)]
    [ServiceBehavior(
        InstanceContextMode =
        InstanceContextMode.PerCall)]
    public class Service1 {
        [WebGet(UriTemplate = "")]
        public List<SampleItem> GetCollection()
        {
            return new List<SampleItem>()
                       {
                           new SampleItem()
                           {
                               Id = 1,
                               StringValue = "Hello" }
                       };
        }

        [WebInvoke(UriTemplate = "", Method = "POST")]
        public SampleItem Create(SampleItem instance)
        {
            return new SampleItem() {
                Id = 2,
                StringValue = "Hello 2" };
        }

        [WebGet(UriTemplate = "{id}")]
        public SampleItem Get(string id)
        {
            return new SampleItem()
                       {
                           Id = int.Parse(id),
                           StringValue = "Single Item " + id
                       };
        }

        [WebInvoke(UriTemplate = "{id}",
            Method = "PUT")]
        public SampleItem Update(string id, SampleItem instance)
        {
            instance.StringValue += "Updated";
            return instance;
        }

        [WebInvoke(UriTemplate = "{id}",
            Method = "DELETE")]
        public string Delete(string id)
        {
            return "deleted"; //make sure we are getting here }

    }

 

6) Starting the WcfRestServiceApp and browsing to “http://http://<machine_name>:<port>/Service1/help

should bring this page:

 

Service1 help page

Service1 generated help page

Starting the WcfRestServiceApp and browsing to “http://:

/login/help”>:/login/help”>http://<machine_name>:<port>/login/help

should bring this page:

Login service generated help page

Login service generated help page

7) The next step is to create an Html page that will be our PhoneGap application. The page will function as a PhoneGap page with only one minor change. Check out Part 3 of Cross-Platform Mobile Development with WP7, PhoneGap and .NET 4.0 WCF REST for details.

Posted in .NET, PhoneGap, WCF REST | Tagged , , | 3 Comments

Cross-Platform Mobile Development with WP7, PhoneGap and .NET 4.0 WCF REST – Part 1

This is Part 1 of the walkthrough that demonstrates cross-platform mobile application with PhoneGap, WP7 and .NET 4.0 WCF REST Services. At least Part 2 will be needed to demonstrate a working application without making this post too long.

In order to create the demo app the following items will be needed:

  1. .NET 4.0 WCF REST Template for quick and easy service starter.
  2. PhoneGap 1.1.0 (current version is: callback-phonegap-1.1.0-0-gc81c02b).

To install WCF REST Template open Extensions Manager in Visual Studio and search for WCF REST Service Template 40 in Online Gallery.

Locate .NET 4.0 WCF REST Template

Locate .NET 4.0 WCF REST Template

After installation the template should be available in Visual Studio.

Select WCF REST Template

Select WCF REST Template

Visit PhoneGap to obtain PhoneGap template for Visual Studio. The downloaded file callback-phonegap-1.1.0-0-gc81c02b.zip will contain the template in callback-phonegap-1.1.0-0-gc81c02b\callback-phonegap-c81c02b\WP7 directory. Copy zipped template to your Documents\Visual Studio 2010\Templates\ProjectTemplates as in the image below (GapAppStarter.zip is a template).

PhoneGap Template Location

PhoneGap Template Location

 

Now the PhoneGap template should be available in Visual Studio when you navigate all the way up to Visual C# in Add New Project Window. If you do not see it type phonegap in the Search Installed Templates box in the top right. It should show up after you start typing.

PhoneGap Installed Template

PhoneGap Installed Template

Next create a solution with two projects. First created from WCF REST template and a second created from PhoneGap template.

WCF REST Project

WCF REST Project

 

PhoneGap Project

PhoneGap Project

Make PhoneGap project a startup project to verify that it compiles and deploys to the emulator.

PhoneGap App in the Emulator

PhoneGap App in the Emulator

After that make WCF REST Service the startup project because this is where the cross-platform demo app development will start in Part 2. In the process functionalities, capabilities and limitations of .NET 4.0 WCF REST and PhoneGap for Windows Phone will be explored.

Posted in .NET, Mobile, PhoneGap, WCF REST, Windows Phone, WP7 | Tagged , , , , , | Comments Off

SharePoint 2007 WCF Services Reference in VS 2010 and XmlElement vs. XElement confusion.

I have been a victim of this VS 2010 behavior the second time around. For the record then, I am going to make a note of it (who knows, it might hunt me again).

I added a Service Reference to SharePoint 2007 asmx services in VS 2010 and tried to reuse my old code for parsing the returned XML. The old code was created in VS 2008.

Surprisingly, I saw red and blue squiggles under parameters and return values from my parsing methods. The error message stated that I tried to substitute XmlElement in System.Xml with XElement in System.Xml.Linq.

I found the solution here: Answer 7 is the solution. To stop VS 2010 from mixing up XmlElement vs. XElement problem make sure that System.Xml.Linq is not being referenced in a project when you create the proxy. VS will use XmlElement for Xml nodes returned from the service methods and the legacy code will compile.

Posted in SharePoint, VS 2010, WCF, XML | Tagged , , , | Comments Off

Investigating EnableViewState and ViewStateMode in .NET 4.0

Today, I stumbled upon a problem with enabling a view state for a control.  It took me about 2 hours to fully comprehend the scope of the problem and to find the fix. I hope that sharing my findings might actually help out somebody.

In .NET 4.0 there are two properties that control the ViewState.
ViewStateEnabled (true or false) and ViewStateMode (Enabled, Disabled, Inherit).
ViewStateEnabled can be set for the whole application in a web.config

<pages enableViewState="true|false" />

I used an asp dropdown list on my page and I needed to get the selected value upon the post back. Sounds simple and straightforward, as long as the pages “enableViewState” property is set to true (it is by default).

Let’s start an experiment and create a web application with two pages. Both pages host a dropdown list and the first objective is to retrieve the value of the selected item upon the postback. To make things not trivial set this in the web.config<pages enableViewState="false" />. The pages are almost identical but one of them is a page with a Master and the other is Master less.

We want the pages look like this:

Postback with Master Page

Postback without Master Page

And this is how they start:

Empty Postback with Master Page

Empty Postback without Master Page

In case of the Page without the Master enabling ViewState on the page level is the quickest way to make the ViewState work for the control.

<%@ Page Language="C#" AutoEventWireup="true"
    CodeBehind="NoMasterPage.aspx.cs"
    Inherits="ViewStateApp.NoMasterPage"
    EnableViewState="true" %>

 

In a case of the Page with a Master Page, enabling the ViewState on the page level is not enough. Setting this:

<%@ Page Title="Home Page" Language="C#"
    MasterPageFile="~/Site.master"
    AutoEventWireup="true" EnableViewState="true"
    CodeBehind="Default.aspx.cs"
    Inherits="ViewStateApp._Default" %>

 

will not make the control ViewState work. But enabling the ViewState on the Master Page as well, will do the trick and make the ViewState work for the dropdown list.

<%@ Master Language="C#" AutoEventWireup="true"
CodeBehind="Site.master.cs" Inherits="ViewStateApp.SiteMaster"
EnableViewState="true"  %>

 

While both solution are quick. They bloat the site of the pages because the view state gets enabled for all controls. So, now let’s move  to an objective number two. Make the ViewState work for the dropdown lists but keep the ViewState as small as possible.

This is where the ViewStateMode comes to the rescue.

The Page directive for a  No Master Page and the control settings should look like this:

<%@ Page Language="C#" AutoEventWireup="true"
    CodeBehind="NoMasterPage.aspx.cs"
    Inherits="ViewStateApp.NoMasterPage"
    EnableViewState="true" ViewStateMode="Disabled" %>
 <asp:dropdownlist runat="server" ID="ddlItems" EnableViewState="true" ViewStateMode="Enabled"  >

 

The page directive, the Master Page directive and the control settings should look like this for the Page with the Master:

<%@ Page Title="Home Page" Language="C#"
    MasterPageFile="~/Site.master"
    AutoEventWireup="true" EnableViewState="true"
    ViewStateMode="Disabled"
    CodeBehind="Default.aspx.cs"
    Inherits="ViewStateApp._Default" %>

 

<%@ Master Language="C#" AutoEventWireup="true"
CodeBehind="Site.master.cs" Inherits="ViewStateApp.SiteMaster"
EnableViewState="true"
ViewStateMode="Disabled"  %>

 

  <asp:dropdownlist runat="server" ID="ddlItems"
  EnableViewState="true" ViewStateMode="Enabled">
  </asp:dropdownlist>

 

Hope it helps. Just remember about the Master Page when considering how ViewState settings are inherited.

Posted in .NET, ViewState | Tagged , | Comments Off

Refreshing Entities in WCF Data Services–Handling Database Triggers

WCF Data Service Context is very smart when it comes to loading entities the data source. It attempts to do it just once. This is to avoid problems related to requesting the same data multiple times, which is bad in a case of the local database and even worse in a case of the remote service.

The side effect of this behavior can cause some trouble for WCF Data Services beginners when the reload behavior is actually needed.

There are few options to force the context to refresh entities.

To refresh a child entity that was modified by some external process (for example a trigger fired when the entity was added to the database) you can use LoadProperty method:

var child = new ChildEntity { Name = “child 1”, Property1 = 1 }
_context.AddObject("ChildEntities", child);
_context.SaveChanges();
_context.LoadProperty(parent, "ChildEntities");

Calling this method forces the context object to reload the requested property and bring the changed entity back.

To refresh the root entity you can do this:

_context.AddObject("Profiles", profile);
_context.SaveChanges();
var mergeOptions = _context.MergeOption;
_context.MergeOption = MergeOption.OverwriteChanges;
profile = GetProfile(profile.UserName); //on another context
_context.MergeOption = mergeOptions;

or you can try this:

_context.AddObject("Profiles", profile);
var userName = profile.UserName;
_context.Detach(profile);
profile = _context.CrmProfiles
.Where(p => p.UserName == userName).Single();

In both cases the entity will be updated with the changes caused by the database trigger.

Posted in .NET, Database, Triggers, WCF Data Services | Tagged , , , | Comments Off