Contents
Introduction
There are a lot of examples on how to use a Web Service from the managed world, but sometimes, having the whole runtime installed is an unneeded luxury. If you want something light, this article will give you a taste of how this can be done.
For the past few years, Web Service development has been one of my main areas of interest. I have been designing and developing all kinds of Web Services as well as client applications that consume Web Services. I have used different environments/languages to build web based clients as well as standalone desktop applications. One thing I didn't try before is to build a Web Service client application on a mobile device. This is one of the reasons why I have decided to build a mobile application for this article. I also wanted to have something a little bit more exciting than a stock ticker app or Amazon product search app that will use the capabilities of the device it will be hosted on. Fortunately, I have just received a new GPS enabled SmartPhone, MOTO Q 9h. Having a GPS on my phone was the second reason for a mobile application example, because I have a good web service to complete the picture, which is described below.
Background
The demo application was tested on the MOTO Q 9h SmartPhone. Screen size on that phone is 320x240. The demo application is not orientation and/or size aware. It uses s static 320x240 bitmap for background image. However, it would not be hard to make it orientation/size aware.
Using the code
Make sure you have read the article and has everything that is needed. Download, unzip, compile, deploy, and enjoy! I have also included a pre-built CAB file.
ATL, WTL
I will be using the ATL library which has built-in support for Web Services. For the UI, I will be using one of my favorites - Windows Template Library. WTL is a lightweight extension to ATL. WTL provides template based wrappers for Windows controls and various Win32 APIs. WTL can be downloaded here. This article uses WTL 8.0, which integrates very well with Visual Studio 2005. After downloading and installing WTL, please run setup80.js from the AppWizMobile folder. That script will install the Application Wizard for Visual Studio 2005.
Application
The following section provides a step by step instruction on how to create the application project and add a Web Service reference.
Creating the project using the WTL Wizard
Let's go ahead and start Visual Studio 2005.
If you have installed WTL and registered (by running setup80.js) the WTL Application Wizard, you will have a new "WTL Mobile Application Wizard" template under the Visual C++/WTL Project types category.
Name your project, select the location, and click "OK". I have named my project GPSDemographics. The WTL Mobile Application Wizard dialog has four pages:
1. General Overview Page
2. Platforms Page
- Select Windows Mobile 6 Standard SDK, and remove all other SDKs from the "Selected SDKs" section.
- Click "Next >".
3. Application Type Page
- Select "SDI Application"
- Check "Enable ActiveX Control Hosting". This option sets some threading related defines in the project that are used by the ATL SOAP implementation. Leaving it unchecked will generate an error with the explanation of what define has to be set.
- Check Generate .cpp files.
- Click "Next >".
4. User Interface Features Page
- We will be using the View window, and "Generic window" as a View type.
- Click "Finish".
Project settings
Since we are using WTL, we have to let Visual Studio know where the header files are located. There are several ways of doing that, which I am sure you are aware of. But just in case, I will show you how to do that using the project properties dialog.
Open the project properties window. Project -> Properties.
Please add the path to your WTL include folder for the following properties:
- Configuration Properties > C/C++ > General > Additional Include Directories.
- Configuration Properties > Resources > General >Additional Include Directories.
Web Service
I will be using CDYNE's Demographics Web Service. This service provides socio-economic data for a given address.
The Web Service has 15 different methods. I will be using the GetLocationInformationByLatitudeLongitude
method that takes the geographical position as an input.
Adding Web Reference
Make sure your Solution Explorer is open.
- Right click on the project item in the Solution Explorer. Select "Add Web Reference...".
- Type the Web Service URL or the WSDL URL into URL edit box. In our case, it is: http://ws.cdyne.com/DemographixWS/DemographixQuery.asmx.
- Click "Go".
If everything is right, you will see the Web Service page. The name of the service in this case is DemographixQuery.
- Click "Add Reference" to finish it.
At this point, you should be able to compile the project. You might receive an error that looks like this:
This error is due to a name conflict. In order to fix it, add the following to the stdafx.h file just before #include "WebService.h"
.
#ifdef lstrlenW
#undef lstrlenW
#endif
GPS
There are several ways of working with the GPS hardware. In this article, I will be using the GPS Intermediate Driver API. The main benefit of using the GPS Intermediate Driver is that it provides an intermediate layer that hides the complexity and implementation details of the actual GPS device. Applications that use the GPS Intermediate Driver architecture should work with any GPS hardware.
There are two modes to working with the GPS hardware under the GPS Intermediate Driver:
- Parsed mode, using the GPS Intermediate Driver API.
- Raw mode, using
CreateFile/ReadFile/CloseHandle
.
I will be using Parsed mode. The API consists of four functions:
GPSOpenDevice
GPSCloseDevice
GPSGetPosition
GPSGetDeviceState
GPSOpenDevice
is the first call you have to make. The first two parameters are the most important for that function. These two parameters tell the driver what kind of information you are interested in as well as provide the means of notifying the application that the information is available. The first parameter is for location information: it is handle to an Event object, created using the CreateEvent
function. The driver sets that event whenever it has new GPS location information. The second parameter is a handle to a Windows event as well. The driver uses that event to signal any device state changes.
GPSGetPosition
and GPSGetDeviceState
functions are for information retrieval. Whenever an event is set, use the appropriate function to get new information.
When you are done with the driver, close it by calling GPSCloseDevice
.
Code
Pretty simple, right? OK, let's get to the actual implementation. In my demo application, I have a wrapper class for the GPS API - CGPSDevice
. This class is responsible for creating event objects, opening the device and closing the device, as well as events monitoring. Events monitoring is done on a separate thread. Whenever an event is set, the monitoring thread calls one of the two methods that are wrapped into the custom interface, IGPSSink
. That interface has to be implemented by the application. In my case, the application view implements that interface and reflects any changes in its OnPaint
handler.
#pragma once
#include <gpsapi.h>
interface IGPSSink
{
virtual HRESULT SetGPSPosition( GPS_POSITION gps_Position ) = 0;
virtual HRESULT SetGPSDeviceInfo( GPS_DEVICE gps_Device ) = 0;
};
#pragma once
#include <GPSApi.h>
#include "IGPSSink.h"
class CGPSDevice
{
private:
static CGPSDevice * s_pInstance;
HANDLE m_hGPS_Device;
HANDLE m_hNewLocationData;
HANDLE m_hDeviceStateChange;
HANDLE m_hThread;
DWORD m_dwThreadID;
HANDLE m_hExitThread;
IGPSSink * m_pSink;
private:
CGPSDevice(void);
HRESULT StartThread();
HRESULT StopThread();
static CGPSDevice * Instance();
static DWORD WINAPI GPSThreadProc(__opt LPVOID lpParameter);
public:
~CGPSDevice(void);
static HRESULT TurnOn(IGPSSink * pSink);
static HRESULT TurnOff();
};
#include "StdAfx.h"
#include "GPSDevice.h"
CGPSDevice * CGPSDevice::s_pInstance = NULL;
#define MAX_WAIT 5000
#define MAX_AGE 3000
#define GPS_CONTROLLER_EVENT_COUNT 3
CGPSDevice::CGPSDevice(void)
{
m_pSink = NULL;
m_hGPS_Device = NULL;
m_hNewLocationData = NULL;
m_hDeviceStateChange = NULL;
m_hExitThread = NULL;
}
CGPSDevice::~CGPSDevice(void)
{
}
CGPSDevice * CGPSDevice::Instance()
{
if( CGPSDevice::s_pInstance == NULL )
{
s_pInstance = new CGPSDevice();
}
return CGPSDevice::s_pInstance;
}
DWORD WINAPI CGPSDevice::GPSThreadProc(__opt LPVOID lpParameter)
{
DWORD dwRet = 0;
GPS_POSITION gps_Position = {0};
GPS_DEVICE gps_Device = {0};
CGPSDevice * pDevice = (CGPSDevice*) lpParameter;
HANDLE gpsHandles[GPS_CONTROLLER_EVENT_COUNT] =
{ pDevice->m_hNewLocationData,
pDevice->m_hDeviceStateChange,
pDevice->m_hExitThread
};
gps_Position.dwSize = sizeof(gps_Position);
gps_Position.dwVersion = GPS_VERSION_1;
gps_Device.dwVersion = GPS_VERSION_1;
gps_Device.dwSize = sizeof(gps_Device);
do
{
dwRet = WaitForMultipleObjects(
GPS_CONTROLLER_EVENT_COUNT,
gpsHandles, FALSE, INFINITE);
if (dwRet == WAIT_OBJECT_0)
{
dwRet = GPSGetPosition(
pDevice->m_hGPS_Device,
&gps_Position, MAX_AGE, 0);
if (ERROR_SUCCESS != dwRet)
continue;
else
pDevice->m_pSink->SetGPSPosition(gps_Position);
}
else if (dwRet == WAIT_OBJECT_0 + 1)
{
dwRet = GPSGetDeviceState(&gps_Device);
if (ERROR_SUCCESS != dwRet)
continue;
else
pDevice->m_pSink->SetGPSDeviceInfo(gps_Device);
}
else if (dwRet == WAIT_OBJECT_0 + 2)
break;
else
ASSERT(0);
} while( TRUE );
return 0;
}
HRESULT CGPSDevice::StartThread()
{
HRESULT hr = E_FAIL;
DWORD dwRet = 0;
m_hNewLocationData = CreateEvent(NULL, FALSE, FALSE, NULL);
if ( m_hNewLocationData )
{
m_hDeviceStateChange = CreateEvent(NULL, FALSE, FALSE, NULL);
if (m_hDeviceStateChange)
{
m_hExitThread = CreateEvent(NULL, FALSE, FALSE, NULL);
if (m_hExitThread)
{
m_hThread = ::CreateThread(NULL, NULL,
GPSThreadProc, this, NULL, &m_dwThreadID);
if ( m_hThread )
hr = S_OK;
}
}
}
if( FAILED(hr) )
{
dwRet = GetLastError();
hr = HRESULT_FROM_WIN32(dwRet);
}
if (FAILED(hr))
{
if (m_hNewLocationData)
{
CloseHandle(m_hNewLocationData);
m_hNewLocationData = NULL;
}
if (m_hDeviceStateChange)
{
CloseHandle(m_hDeviceStateChange);
m_hDeviceStateChange = NULL;
}
if (m_hExitThread)
{
CloseHandle(m_hExitThread);
m_hExitThread = NULL;
}
}
return hr;
}
HRESULT CGPSDevice::StopThread()
{
HRESULT hr = E_FAIL;
DWORD dwRet = 0;
if( SetEvent(m_hExitThread) )
{
dwRet = WaitForSingleObject(m_hThread, MAX_WAIT);
if(WAIT_OBJECT_0 == dwRet)
hr = S_OK;
}
if( FAILED(hr) )
{
dwRet = GetLastError();
hr = HRESULT_FROM_WIN32(dwRet);
}
if (m_hNewLocationData)
{
CloseHandle(m_hNewLocationData);
m_hNewLocationData = NULL;
}
if (m_hDeviceStateChange)
{
CloseHandle(m_hDeviceStateChange);
m_hDeviceStateChange = NULL;
}
if (m_hExitThread)
{
CloseHandle(m_hExitThread);
m_hExitThread = NULL;
}
if (m_hThread)
{
CloseHandle(m_hThread);
m_hThread = NULL;
m_dwThreadID = 0;
}
return hr;
}
HRESULT CGPSDevice::TurnOn(IGPSSink * pSink)
{
if( !pSink )
return E_INVALIDARG;
CGPSDevice * pDevice = Instance();
if( pDevice->m_hGPS_Device )
return E_UNEXPECTED;
if( pDevice->m_pSink != NULL )
return E_UNEXPECTED;
pDevice->m_pSink = pSink;
HRESULT hr = pDevice->StartThread();
if( SUCCEEDED(hr) )
{
pDevice->m_hGPS_Device = GPSOpenDevice(
pDevice->m_hNewLocationData,
pDevice->m_hDeviceStateChange,
NULL, NULL);
if( pDevice->m_hGPS_Device )
hr = S_OK;
else
hr = HRESULT_FROM_WIN32(GetLastError());
}
if( FAILED(hr) )
TurnOff();
return hr;
}
HRESULT CGPSDevice::TurnOff()
{
CGPSDevice * pDevice = Instance();
if( !pDevice->m_hGPS_Device )
return E_UNEXPECTED;
HRESULT hr = pDevice->StopThread();
pDevice->m_pSink = NULL;
DWORD dwRet = GPSCloseDevice(pDevice->m_hGPS_Device);
pDevice->m_hGPS_Device = NULL;
if( SUCCEEDED(hr) )
{
if( ERROR_SUCCESS != dwRet )
hr = HRESULT_FROM_WIN32(GetLastError());
}
return hr;
}
As I mentioned before, the view class implements IGPSSink
.
class CDemographicsView :
public CWindowImpl<cdemographicsview />,
public IGPSSink
{
...
HRESULT SetGPSPosition(GPS_POSITION gps_Position);
HRESULT SetGPSDeviceInfo(GPS_DEVICE gps_Device);
...
}
The most interesting method is SetGPSPosition
. This is the place where the actual call to the Web Service is made. I have to point out that for performance reasons, I have a distance threshold of about 0.5 mile to limit the number of times the Web Service is called. It is a good practice to make sure that the Web Service is called only when it is needed.
HRESULT CDemographicsView::SetGPSPosition(GPS_POSITION gps_Position)
{
::EnterCriticalSection(&m_cs);
m_gpsPosition = gps_Position;
if( m_gpsPosition.dwSatelliteCount != 0 )
{
double dist = (m_oldLat - gps_Position.dblLatitude) *
(m_oldLat - gps_Position.dblLatitude) +
(m_oldLong - gps_Position.dblLongitude) *
(m_oldLong - gps_Position.dblLongitude);
if( dist > DIST_THRESHOLD )
{
m_oldLat = gps_Position.dblLatitude;
m_oldLong = gps_Position.dblLongitude;
DemographixQuery::CDemographixQuery webService;
HRESULT hr = webService.GetLocationInformationByLatitudeLongitude(
m_gpsPosition.dblLatitude,
m_gpsPosition.dblLongitude,
CComBSTR(DEMO_LICENSE),
&m_info);
}
Invalidate();
}
::LeaveCriticalSection(&m_cs);
return S_OK;
}
HRESULT CDemographicsView::SetGPSDeviceInfo(GPS_DEVICE gps_Device)
{
::EnterCriticalSection(&m_cs);
m_gpsDevice = gps_Device;
Invalidate();
::LeaveCriticalSection(&m_cs);
return S_OK;
}
Resources
History
- January 17, 2008 - Added "Contents" section.
- January 16, 2008 - Initial version.