Monday, December 15, 2008

Dynamic SQL Statements with SQLite

Recently I tried to build a dynamic SQL Statement within XCode and it proved more difficult than expected. As the parsing function used my SQLite enjoys getting a Const Char data type, building that from a NSString was not a simple matter. After some investigation my first attempt came up with using the int sqlite3_bind_text(sqlite3_stmt*, int, const char*, int n, void(*)(void*)); function while passing a sql statement like this "Select * from table1 where name=?". The idea was that this would replace the "?" with the value I passed through, however it never seemed to bring back any records. SQLite does not make it easy to see the actual statement (Oh the joys of MS SQL Profiler!), so I could not see what was going wrong.

An alternative I came up with was to use the following:


NSString *sql1 = [[NSString alloc] initWithFormat:@"Select * FROM Table1 where name='%@'", paramName];
NSString *sql = [NSString stringWithUTF8String:[sql1 UTF8String]];
sqlite3_stmt *selectstmt = nil;
if(sqlite3_prepare_v2(database, [sql UTF8String], -1, &selectstmt, NULL) == SQLITE_OK)
{
while(sqlite3_step(selectstmt) == SQLITE_ROW) { ....... }
}


This worked fine, however if someone has an alternative, please let me know.

Sunday, December 14, 2008

Unit Testing in XCode

With all previous applications I've written I have tried to implement a TDD (Test Development Development) process, as this leads to a more concise product which is also easier to support. With Microsoft Visual Studio I've tended to use NUnit rather than the built in option, however I've not seen anything out there for XCode. Recently I've come across a project in Google which does exactly the type of Unit Testing I was looking for. gives a number of simple steps for implementing TDD for use with the iPhone.

Here is another simple example.

Download the latest google-toolbox-for-mac file from the site. Extract the content of the ZIP file to your Hard Drive.

First we create an iPhone Window-Based Application, by choosing "File/New Project" from the xCode Menu. Click Choose and enter the name "Calculation" and click Save. This will be a simple non-functional harness that we can test against.


Add a simple object to the project by selecting "File/New" from the menu and selecting a NSObject subclass. Click Next and give it a filename of "Calculation" with a tick in the "Also create Calculation.h" box and click Finish.

Within Calculation.m we add a new method which takes an integer, add 5 to this and returns the result. The code would look like the following:
#import "Calculation.h"
@implementation Calculation
- (int) add5: (int) num
{
int result;
result = num 5;
return result;
}
@end



Within Calculation.h we the following:

#import <Foundation/Foundation.h>

@interface Calculation : NSObject {
}
- (int) add5: (int) num;
@end



Adding the Unit Test Framework


Next we add a UnitTest and supporting Framework to the project by adding a new target.

Right Click the Targets folder in XCode and select "Add/New Target" and select a Cococ Touch Application and give it the name "UnitTests", adding it to the Calculation project.


Create a new File Group within the XCode project by selecting "Project/New Group" from the menu and giving it the name UnitTests. Add the following files to the Group which are all part of the Google ToolBox for Mac ZIP file you extracted earlier. Be sure to set the target as UnitTets and not Calculation
  • GoogleToolsForMac/GTMDefines.h

  • GoogleToolsForMac/UnitTesting/GTMIPhoneUnitTestDelegate.h

  • GoogleToolsForMac/UnitTesting/GTMIPhoneUnitTestDelegate.m

  • GoogleToolsForMac/UnitTesting/GTMIPhoneUnitTestMain.m

  • GoogleToolsForMac/UnitTesting/GTMSenTestCase.h

  • GoogleToolsForMac/UnitTesting/GTMSenTestCase.m




Finally add a new Build Script to the project by right clicking the "UnitTests" Target and selecting "Add/New Build Phase/New Run Script Build Phase ".



The Shell should be "/bin/sh" and the Script should be the location of the "RunIPhoneUnitTest.sh" file located in the Google ToolBox for Mac ZIP file you extracted earlier. In my case it was:
/Volumes/Windows/Development/GoogleToolsForMac/UnitTesting/RuniPhoneUnitTest.sh


Change the Active Target in XCode to "UnitTests" by using the drop down box in the top left of the Project Window and click Build. Everything should compile without error. Now click "Build and Go" and the iPhone Simulator should start and the Debug Console will display a list of Unit Test messages ending with "Executed 0 tests, with 0 failures (0 unexpected) in 0.001 (0.001) seconds". This means that the Framework has been installed and is ready for our custom tests.

Adding a custom Unit Test


Create a new class in XCode, by right clicking the UnitTests group and selecting "Add/New File". Choose a Cocoa Objecttve-C test case class" from the list shown and call it "CalculationTest" making sure that the target is set to UnitTests and unticking the "Also create CalculationTest.h" box and click Finish.



The code for our class would be the following:
#import "GTMSenTestCase.h"
#import "Calculation.h"

@interface CalculationTest : GTMTestCase {
}
@end

@implementation CalculationTest
- (void) testCalculation
{
Calculation *calculation = [[Calculation alloc] init];
STAssertEquals(16, [calculation add5:12], @"Not Equal! %d", [calculation add5:12]);
[calculation release];
}
@end

Finally we need to add Calculation.m and Calculation.h to our new UnitTests target and the easiest way to do this is to select them in XCode and Drag them down to the "Targets/UnitTests/Compile Sources" folder.

Running the custom Unit Test


Clicking Build in Xcode should produce the following:



This is telling us that the UnitTest which called the add5 method was passed 12 and returned 17, which is correct, however our Assertion was expecting a 16, so the test failed. We correct this by editing CalculationTest.m and changing the line:
STAssertEquals(16, [calculation add5:12], @"Not Equal! %d", [calculation add5:12]);
with
STAssertEquals(17, [calculation add5:12], @"Not Equal! %d", [calculation add5:12]);

Following this change we recompile and everything builds correctly.



This is a fairly trivial example, but it shows the steps needed to create a TDD process using XCode.

Saturday, December 13, 2008

Folders for Source Files in XCode

Having to been used to Microsoft Visual Studio I'm more familiar with organising my source code into folders. From what I can see XCode has the same functionality, however this structure has no effect on the underlying structure stored on the disc. So to create a new folder in xCode you select the files you with to place in the folder and click "Project/Group" from the menu bar. Enter a name for the New Group and that's it. You can also create new groups by selecting "Project/New Group" from the menu bar.

Tuesday, December 9, 2008

SQLite Error Codes

I've been getting problems on how to debug SQL errors on the iPhone and found a list of all error codes generated:

#define SQLITE_OK 0 /* Successful result */
/* beginning-of-error-codes */
#define SQLITE_ERROR 1 /* SQL error or missing database */
#define SQLITE_INTERNAL 2 /* Internal logic error in SQLite */
#define SQLITE_PERM 3 /* Access permission denied */
#define SQLITE_ABORT 4 /* Callback routine requested an abort */
#define SQLITE_BUSY 5 /* The database file is locked */
#define SQLITE_LOCKED 6 /* A table in the database is locked */
#define SQLITE_NOMEM 7 /* A malloc() failed */
#define SQLITE_READONLY 8 /* Attempt to write a readonly database */
#define SQLITE_INTERRUPT 9 /* Operation terminated by sqlite3_interrupt()*/
#define SQLITE_IOERR 10 /* Some kind of disk I/O error occurred */
#define SQLITE_CORRUPT 11 /* The database disk image is malformed */
#define SQLITE_NOTFOUND 12 /* NOT USED. Table or record not found */
#define SQLITE_FULL 13 /* Insertion failed because database is full */
#define SQLITE_CANTOPEN 14 /* Unable to open the database file */
#define SQLITE_PROTOCOL 15 /* NOT USED. Database lock protocol error */
#define SQLITE_EMPTY 16 /* Database is empty */
#define SQLITE_SCHEMA 17 /* The database schema changed */
#define SQLITE_TOOBIG 18 /* String or BLOB exceeds size limit */
#define SQLITE_CONSTRAINT 19 /* Abort due to constraint violation */
#define SQLITE_MISMATCH 20 /* Data type mismatch */
#define SQLITE_MISUSE 21 /* Library used incorrectly */
#define SQLITE_NOLFS 22 /* Uses OS features not supported on host */
#define SQLITE_AUTH 23 /* Authorization denied */
#define SQLITE_FORMAT 24 /* Auxiliary database format error */
#define SQLITE_RANGE 25 /* 2nd parameter to sqlite3_bind out of range */
#define SQLITE_NOTADB 26 /* File opened that is not a database file */
#define SQLITE_ROW 100 /* sqlite3_step() has another row ready */
#define SQLITE_DONE 101 /* sqlite3_step() has finished executing */
/* end-of-error-codes */


The code to retrieve this would be used like so:

if (sqlite3_open([dbPath UTF8String], &database) == SQLITE_OK) {
const char *sql = "select tbl_name from sqlite_master";
sqlite3_stmt *selectstmt;
NSLog(@"SQLite= %d", sqlite3_prepare_v2(database, sql, -1, &selectstmt, NULL));

Monday, December 8, 2008

Creating a database project - iPhone

This is the second part of an article outlining the code to implement a Client/Server application between the iPhone and a Windows WCF Service. The first part focused on re-factoring an existing webservice to use a back end database. Everything in this article assumes that you have successfully implement the changes described.

Creating our local iPhone database


The easiest way to create a SQLite database is to download the SQLite Firefox extension. This provides a GUI to create and manage any SQLite databases. Simply select "Database/New Database" from the menu and type the name JokeGenLocal as the name. You will be asked for a location of the database, i'd recommend not having it within your XCode Application, especially if ypu're using SVC for source control. I simply created the file in a folder called iWCFDemoDB at the same level as the original iWCFDemo project folder.



The local database format will mirror the server database format (created previously) as much as possible at this stage of development so I create two tables Joke and JokeCategory.



JokeCategory


Joke


We should also create indexes for these database table, but as our record set is very low at this stage I'm not going to do that.

Add the Database File to Your XCode Project


Now we have a database we want to include this as part of the XCode project. Right click the Resources folder in XCode and select "Add/Existing File..". Browse to the location where you saved the JokeGenLocal.sqlite file and click OK. I would not copy the file as local resource, but that decision is up to you.

Next we need to link to the library that contains all of the SQLite functions. To link to that library, from your XCode project right click the frameworks folder and select “Add/Existing Frameworks…”. Browse to the iPhoneSDK folder (mine was in/Developer/Patforms/iPhoneOS.platform/Developer/SDKs/iPkoneOS2.2.sdk/usr/lib/) and select the libsqlite3.0.dylib file.

Adding classes to xCode to hold access the database information


Once you have your database added to the application we need to create proxy classes which mirror the database structure and provide a mechanism for reading the records from the DB File. These are much the same as the Entity Framework classes which are created in Visual Studio, except Microsoft make it far easier for the developer. We also need a class that checks whether the iPhone has the database or not, if not then we copy it to the phone this will be the job of the SQLAppDelegate.

The boolean "isDirty" tells the application if the object was changed in memory or not and "isDetailViewHydrated" tell the application, if the data which shows up on the detail view is fetched from the database or not. Our application allows for very little in the way of editing at present however I'm leaving these in there as best practice and to assist in future development.

Create three new classes with headers and add the following code to each:
Category.h
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#import <sqlite3.h>
@interface Category : NSObject {
NSInteger categoryID;
NSString *categoryDescription;

//Intrnal variables to keep track of the state of the object.
BOOL isDirty;
BOOL isDetailViewHydrated;
}

@property (nonatomic, readonly) NSInteger categoryID;
@property (nonatomic, copy) NSString *categoryDescription;
@property (nonatomic, readwrite) BOOL isDirty;
@property (nonatomic, readwrite) BOOL isDetailViewHydrated;

@end


Category.m


#import "Category.h"
@implementation Category
@synthesize categoryID, categoryDescription, isDirty, isDetailViewHydrated;

- (void) dealloc {
[categoryDescription release];
[super dealloc];
}
@end


Joke.h

#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#import <sqlite3.h>

@interface Joke : NSObject {

NSInteger jokeID;
NSInteger jokeCategory;
NSString *jokeText;
NSString *jokeSource;
NSString *jokeGraphic;

//Intrnal variables to keep track of the state of the object.
BOOL isDirty;
BOOL isDetailViewHydrated;
}

@property (nonatomic, readonly) NSInteger jokeID;
@property (nonatomic, readonly) NSInteger jokeCategory;
@property (nonatomic, copy) NSString *jokeText;
@property (nonatomic, copy) NSString *jokeSource;
@property (nonatomic, copy) NSString *jokeGraphic;

@property (nonatomic, readwrite) BOOL isDirty;
@property (nonatomic, readwrite) BOOL isDetailViewHydrated;

@end


Joke.m
#import "Joke.h"
@implementation Joke
@synthesize jokeID, jokeCategory, jokeText, jokeSource, jokeGraphic, isDirty, isDetailViewHydrated;
- (void) dealloc {
[jokeText release];
[jokeSource release];
[jokeGraphic release];
[super dealloc];
}
@end


SQLAppDelegate.h

#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>

@class Category;
@class Joke;

@interface SQLAppDelegate : NSObject <UIApplicationDelegate> {
UIWindow *window;
UINavigationController *navigationController;
//To hold a list of Category objects
NSMutableArray *categoryArray;
NSMutableArray *jokeArray;
}

@property (nonatomic, retain) IBOutlet UIWindow *window;
@property (nonatomic, retain) IBOutlet UINavigationController *navigationController;
@property (nonatomic, retain) NSMutableArray *categoryArray;
@property (nonatomic, retain) NSMutableArray *jokeArray;

- (void) copyDatabaseIfNeeded;
- (NSString *) getDBPath;

@end


SQLAppDelegate.m




Creating the contracts


We know the format of the message to be sent and how it will be received from the previous run through and I've bold the important areas.
Client Sends:
WSGetAllRequest
<s:Body xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<GetJokes xmlns="http://tempuri.org/">
<wsJokeRequest>
<RefreshDate>2008-12-09T16:18:36.5706144 08:00</RefreshDate>
</wsJokeRequest>
</GetJokes>
</s:Body>


WSGetAllResponse
<s:Body xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<GetJokesResponse xmlns="http://tempuri.org/">
<GetJokesResult>
<Jokes>
<Joke>
<EntityKey>
<EntitySetName>Joke</EntitySetName>
<EntityContainerName>JokeGenDBEntities</EntityContainerName>
<EntityKeyValues>
<EntityKeyMember>
<Key>ID</Key>
<Value xsi:type="xsd:int">1</Value>
</EntityKeyMember>
</EntityKeyValues>
</EntityKey>
<ID>1</ID>
<Text>"Knock, Knock". "Who's there?", "Sarah", "Sarah Who?", "Sarah doctor in the house."</Text>
<JokeCategoryReference>

<EntityKey>
<EntitySetName>JokeCategory</EntitySetName>
<EntityContainerName>JokeGenDBEntities</EntityContainerName>
<EntityKeyValues>
<EntityKeyMember>
<Key>Category</Key>
<Value xsi:type="xsd:int">1</Value>
</EntityKeyMember>
</EntityKeyValues>
</EntityKey>
</JokeCategoryReference>
</Joke>

... clipped there will be many, many iterations or this .....

</Jokes>
</GetJokesResult>
</GetJokesResponse>
</s:Body>

iPhone Simulator

I found an interesting tool for the Mac which allows for the testing of WebSites via the iPhone interface. Called iPhoney it provides a simple interface and fast way to verify how any website developed will be displayed on the iPhone.
Check out the following site for some other information regarding CSS.

Friday, December 5, 2008

Error when switching views on iPhone

I was developing a application with multiple views hanging off one application controller. When I received the following error during run-time:
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '*** -[iWCFDemoViewController dataviewView]: unrecognized selector sent to instance 0x5264c0'

The line of code which failed was :
- (void)switchToNewView:(id)sender
{
// get the view that's currently showing
UIView *currentView = self.view;
// get the the underlying UIWindow, or the view containing the current view view
UIView *theWindow = [currentView superview];
// remove the current view and replace with myView1
[currentView removeFromSuperview];
[theWindow addSubview:self.dataviewView]; // <<<--- Crash on this line
}


The header file seemed to be created correctly showing:
@interface iWCFDemoViewController : UIViewController {
IBOutlet UIView *generateView;
IBOutlet UIView *dataviewView;
}

@property(nonatomic, retain) UIView *generateView;
@property(nonatomic, retain) UIView *dataviewView;

- (void) switchToNewView:(id)sender;
@end

I checked the NIB file in Interface builder and everything was connected up correctly.

The solution was to make two minor changes to the code:
First the header was changed to:
IBOutlet UIView *_generateView;
IBOutlet UIView *_dataviewView;

Then in the view controller, just under the @implementation line, I added the two lines:
@synthesize dataviewView = _generatewView;
@synthesize dataviewView = _dataviewView;

I''m not really sure what the difference is between these two implementations as all I seem to be doing moving the pointer reference to a duplicate variable, however it seemed to work fine after that.

Wednesday, December 3, 2008

iPhone debug error

I recently had the following error message when trying to run my iPhone application on my debug device:
"Your mobile device has encountered an unexpected error (0xE800003A) during the install phase: Verifying application"

This was due to the fact I'd failed to download the new SDK, even though my iTouch had automatically been upgraded via iTunes. The original bundle had been developed for iPhone OS 2.0 while the debug device was running under OS 2.2. Downloading the new version of the SDK from Apple addressed the issue.


The problem I have with this is that the new SDK is over 1Gb in size! Its hard to believe I'll have to download that much each time there is a new update to the Framework.

Monday, December 1, 2008

Creating a database project - Server

As part of the ongoing development of an iPhone application, I want to all my iPhone application to read content from a SQL2008 database, store this locally and then refresh the data when requested by the user. This posting will focus on the Microsoft side of the equation, creating the database, creating the query methods and publishing that via the existing IJoke interface.

First we need add a Database project to our existing solution which was created in previous posts (apologies to anyone arriving late but you'll need to read the old postings to reach this point). We create this project by selecting the solution file in Visual Studio 2008. right click "Add" and select "New Project..". Browse to the "Other Project Types/Database" branch on tree in the dialogue and select "Database Project" from the Templates. enter "JokeGenDB" in the name field and set the location as required. The dialogue should look something like the following:

Enter the server name for the SQL Sever 2008 instance you want to use, I'm simply going to use a local installation i.e. "(local)". Select the database as "master", I'm assuming here that you have DBO access. Finally Click OK.

Following all of this you should see a new project added within your solution on the Solution Explorer window in Visual Studio 2008.

Database Scripts


I'm a big fan of scripting the database, table, index and test data within my Database project, so the following instructions focus on that. If you are more inclined to just using SQL Server Management Console, can you can probably by pass most of these settings.

Browse to the "Create Scripts" folder in your JokeGenDB project and create a number of folders. These will hold all the various scripts so we can quickly regenerate the entire database or tables. Right click the "Create Scripts" folder and select "New Folder", enter "Database" and click OK. Repeat these steps for folders named Tables, Indexes, Data. When complete it should look something like the following:


Within each of the folders create and run the following SQL Scripts:

Database\Database.sql Note the file locations as you may want to change these.

USE master
GO
IF EXISTS (select name from sysdatabases where name='JokeGenDB')
BEGIN
EXEC msdb.dbo.sp_delete_database_backuphistory @database_name = N'JokeGenDB'
DROP DATABASE [JokeGenDB]
END
GO

CREATE DATABASE [JokeGenDB] ON PRIMARY
( NAME = N'JokeGenDB', FILENAME = N'C:\Program Files\Microsoft SQL Server\MSSQL10.MSSQLSERVER\MSSQL\DATA\JokeGenDB.mdf' , SIZE = 3072KB , FILEGROWTH = 1024KB )
LOG ON
( NAME = N'JokeGenDB_log', FILENAME = N'C:\Program Files\Microsoft SQL Server\MSSQL10.MSSQLSERVER\MSSQL\DATA\JokeGenDB_log.ldf' , SIZE = 1024KB , FILEGROWTH = 10%)
GO
ALTER DATABASE [JokeGenDB] SET COMPATIBILITY_LEVEL = 100
GO
ALTER DATABASE [JokeGenDB] SET ANSI_NULL_DEFAULT OFF
GO
ALTER DATABASE [JokeGenDB] SET ANSI_NULLS OFF
GO
ALTER DATABASE [JokeGenDB] SET ANSI_PADDING OFF
GO
ALTER DATABASE [JokeGenDB] SET ANSI_WARNINGS OFF
GO
ALTER DATABASE [JokeGenDB] SET ARITHABORT OFF
GO
ALTER DATABASE [JokeGenDB] SET AUTO_CLOSE OFF
GO
ALTER DATABASE [JokeGenDB] SET AUTO_CREATE_STATISTICS ON
GO
ALTER DATABASE [JokeGenDB] SET AUTO_SHRINK OFF
GO
ALTER DATABASE [JokeGenDB] SET AUTO_UPDATE_STATISTICS ON
GO
ALTER DATABASE [JokeGenDB] SET CURSOR_CLOSE_ON_COMMIT OFF
GO
ALTER DATABASE [JokeGenDB] SET CURSOR_DEFAULT GLOBAL
GO
ALTER DATABASE [JokeGenDB] SET CONCAT_NULL_YIELDS_NULL OFF
GO
ALTER DATABASE [JokeGenDB] SET NUMERIC_ROUNDABORT OFF
GO
ALTER DATABASE [JokeGenDB] SET QUOTED_IDENTIFIER OFF
GO
ALTER DATABASE [JokeGenDB] SET RECURSIVE_TRIGGERS OFF
GO
ALTER DATABASE [JokeGenDB] SET DISABLE_BROKER
GO
ALTER DATABASE [JokeGenDB] SET AUTO_UPDATE_STATISTICS_ASYNC OFF
GO
ALTER DATABASE [JokeGenDB] SET DATE_CORRELATION_OPTIMIZATION OFF
GO
ALTER DATABASE [JokeGenDB] SET PARAMETERIZATION SIMPLE
GO
ALTER DATABASE [JokeGenDB] SET READ_WRITE
GO
ALTER DATABASE [JokeGenDB] SET RECOVERY FULL
GO
ALTER DATABASE [JokeGenDB] SET MULTI_USER
GO
ALTER DATABASE [JokeGenDB] SET PAGE_VERIFY CHECKSUM
GO
USE [JokeGenDB]
GO
IF NOT EXISTS (SELECT name FROM sys.filegroups WHERE is_default=1 AND name = N'PRIMARY') ALTER DATABASE [JokeGenDB] MODIFY FILEGROUP [PRIMARY] DEFAULT
GO


Tables\JokeCategory.sql

USE [JokeGenDB]
GO

IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[JokeCategory]') AND type in (N'U'))
DROP TABLE [dbo].[JokeCategory]
GO

CREATE TABLE [dbo].[JokeCategory](
[Category] [int] IDENTITY(1,1) NOT NULL,
[Description] [varchar](50) NOT NULL,
CONSTRAINT [PK_JokeCategory] PRIMARY KEY CLUSTERED
(
[Category] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]

GO


Tables\Joke.sql
USE [JokeGenDB]
GO

IF EXISTS (SELECT * FROM sys.foreign_keys WHERE object_id = OBJECT_ID(N'[dbo].[FK_Joke_JokeCategory]') AND parent_object_id = OBJECT_ID(N'[dbo].[Joke]'))
ALTER TABLE [dbo].[Joke] DROP CONSTRAINT [FK_Joke_JokeCategory]
GO

IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[Joke]') AND type in (N'U'))
DROP TABLE [dbo].[Joke]
GO

CREATE TABLE [dbo].[Joke](
[ID] [int] IDENTITY(1,1) NOT NULL,
[Category] [int] NOT NULL,
[Text] [varchar](2000) NOT NULL,
[Source] [varchar](200) NULL,
[Graphic] [varchar](200) NULL,
CONSTRAINT [PK_Joke] PRIMARY KEY CLUSTERED
(
[ID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]

GO


Indexes\FK_Joke_JokeCategory.sql


USE [JokeGenDB]
GO

IF EXISTS (SELECT * FROM sys.foreign_keys WHERE object_id = OBJECT_ID(N'[dbo].[FK_Joke_JokeCategory]') AND parent_object_id = OBJECT_ID(N'[dbo].[Joke]'))
ALTER TABLE [dbo].[Joke] DROP CONSTRAINT [FK_Joke_JokeCategory]
GO

ALTER TABLE [dbo].[Joke] WITH CHECK ADD CONSTRAINT [FK_Joke_JokeCategory] FOREIGN KEY([Category])
REFERENCES [dbo].[JokeCategory] ([Category])
GO

ALTER TABLE [dbo].[Joke] CHECK CONSTRAINT [FK_Joke_JokeCategory]
GO



Data\JokeCategory.sql


USE JokeGenDB

DELETE FROM [JokeGenDB].[dbo].[JokeCategory]
GO
SET IDENTITY_INSERT [JokeGenDB].[dbo].[JokeCategory] ON
GO
INSERT INTO [JokeGenDB].[dbo].[JokeCategory]
([Category], [Description])
VALUES
(1, 'Knock, Knock')
GO
INSERT INTO [JokeGenDB].[dbo].[JokeCategory]
([Category], [Description])
VALUES
(2, 'Doctor, Doctor')
GO
INSERT INTO [JokeGenDB].[dbo].[JokeCategory]
([Category], [Description])
VALUES
(3, 'Irish')
GO
INSERT INTO [JokeGenDB].[dbo].[JokeCategory]
([Category], [Description])
VALUES
(4, 'General')
GO
SET IDENTITY_INSERT [JokeGenDB].[dbo].[JokeCategory] OFF
GO



Data\Joke.sql


USE JokeGenDB
GO
DELETE FROM [JokeGenDB].[dbo].[Joke]
GO

SET IDENTITY_INSERT [JokeGenDB].[dbo].[Joke] ON
GO
INSERT INTO [JokeGenDB].[dbo].[Joke]
( [ID], [Category], [Text], [Source], [Graphic])
VALUES
(1, 1,
'"Knock, Knock". "Who''s there?", "Sarah", "Sarah Who?", "Sarah doctor in the house."'
,null,null )
GO
INSERT INTO [JokeGenDB].[dbo].[Joke]
( [ID], [Category], [Text], [Source], [Graphic])
VALUES
(2, 1,
'"Knock, Knock". "Who''s there?", "Orange", "Orange Who?", "Orange you glad to came-a-knocking?"'
,'http://www.knock-knock-joke.com',null )
GO

INSERT INTO [JokeGenDB].[dbo].[Joke]
( [ID], [Category], [Text], [Source], [Graphic])
VALUES
(3, 2,
'"Doctor, doctor I swallowed a bone.". "Are you choking?", "No, I really did!"'
,'http://www.ahajokes.com',null )
GO

INSERT INTO [JokeGenDB].[dbo].[Joke]
( [ID], [Category], [Text], [Source], [Graphic])
VALUES
(4, 3,
'"An Irish man is eating in a restaurant, when the waiter throws a prawn cocktail over him. "What''s that for?" he says shocked. The waiter replies "Thats just for starters!".'
,'http://irishjokes.com',null )
GO

INSERT INTO [JokeGenDB].[dbo].[Joke]
( [ID], [Category], [Text], [Source], [Graphic])
VALUES
(5, 4,
'Two zebras are talking and one asks the other, "Am I black with white stripes or white with black stripes?" The other replies, "Well I don''t know. You should pray to God about that and ask him." So that night he did and God replied, "You are what you are." The next day he said to the other zebra, "I still don''t understand what I am because God just said, You are what you are." The second zebra responds, "You must be white with black stripes or else God would have said, Yo is what yo is." '
,'http://www.coolfunnyjokes.com',null )
GO
SET IDENTITY_INSERT [JokeGenDB].[dbo].[Joke] OFF
GO


This will create a very simple database of 5 entires which we will use for testing. The database looks like the following:


Entity Framework Design


I've decided to use the Microsoft ADO.Net Entity Framework for no other reason other than its available and will eventually take over from Linq. You will not need to download this if you already have Service Pack 1 for .Net3.5 running on your machine. I'd also recommend downloading the ADO.Net Entity Framework Tools

First we create a Entity Data model within Visual Studio 2008 on the JokeGen project (i.e. our WCF application). We do this by right clicking the APP_CODE folder and selecting "Add New Item", then selecting "ADO.NET Entity Data Model" from the list of templates. Give it the name JokeGenDB and click Add.


Select Generate from database and click Next. the wizard will probably select the "master" database as this was the database selected previously, however we want to choose the recently created JokeGenDb database instead. Do this by clicking "New Connection" and enter the server name (local) and database name JokeGenDB. Again you should choose your own database name and location if you chose a different installation option. Click OK in the properties window and Next in the Entity Data Model Wizard window.



Select the tables we wish to use (joke and JokeCategory), we are not using Views or Stored Procedures at this stage. Then click "Finish" keeping all the standard defaults.



After creation of the file you should see something like the following:



Re-factoring the Service


Now we create three new classes which will at as the Request/Response contracts for our new web methods. I'm going to do this within the root folder of the project for ease of creation however I'd probably recommend creating a specific folder for these classes. Add two new class files and enter the following code to each:

WSGetAllRequest.cs

USE [JokeGenDB]
GO

IF EXISTS (SELECT * FROM sys.foreign_keys WHERE object_id = OBJECT_ID(N'[dbo].[FK_Joke_JokeCategory]') AND parent_object_id = OBJECT_ID(N'[dbo].[Joke]'))
ALTER TABLE [dbo].[Joke] DROP CONSTRAINT [FK_Joke_JokeCategory]
GO

ALTER TABLE [dbo].[Joke] WITH CHECK ADD CONSTRAINT [FK_Joke_JokeCategory] FOREIGN KEY([ID])
REFERENCES [dbo].[JokeCategory] ([Category])
GO

ALTER TABLE [dbo].[Joke] CHECK CONSTRAINT [FK_Joke_JokeCategory]
GO


WSGetAllResponse.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Runtime.Serialization;

/// <summary>
/// Summary description for WSGetAllResponse
/// </summary>
[DataContract(Namespace = "http://tempri.org/")]
public class WSGetAllResponse
{

private List<JokeGenDBModel.Joke> _jokes = new List<JokeGenDBModel.Joke>();

[DataMember]
public List<JokeGenDBModel.Joke> Jokes { get {return _jokes;}
set {_jokes = value;}}
}


WSGetAllCategoriesResponse.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Runtime.Serialization;

/// <summary>
/// Summary description for WSGetAllJokeCategoriesResponse
/// </summary>
[DataContract(Namespace = "http://tempri.org/")]
public class WSGetAllCategoriesResponse
{

private List _jokes = new List();

[DataMember]
public List JokeCategories {
get { return _jokes; }
set {_jokes = value;}}
}


Also we need to modify the Interface and create the method:

IJoke.cs

[OperationContract]
WSGetAllResponse GetJokes(WSGetAllRequest wsJokeRequest);

[OperationContract]
WSGetAllCategoriesResponse GetJokeCategories();



Joke.cs

public WSGetAllResponse GetJokes(WSGetAllRequest wsJokeRequest)
{
WSGetAllResponse wsGetAllResponse = new WSGetAllResponse();
using(JokeGenDBModel.JokeGenDBEntities jokeGenDBEntities = new JokeGenDBEntities())
{
ObjectQuery<JokeGenDBModel.Joke> jokeQuery = jokeGenDBEntities.Joke;
wsGetAllResponse.Jokes = jokeQuery.ToList();
}
return wsGetAllResponse;
}

public WSGetAllCategoriesResponse GetJokeCategories()
{
WSGetAllCategoriesResponse wsGetAllCategoriesResponse = new WSGetAllCategoriesResponse();
using (JokeGenDBModel.JokeGenDBEntities jokeGenDBEntities = new JokeGenDBEntities())
{
ObjectQuery jokeQuery = jokeGenDBEntities.JokeCategory;
wsGetAllCategoriesResponse.JokeCategories = jokeQuery.ToList();
}
return wsGetAllCategoriesResponse;
}




Compile the JokeGen project and it should build without error.

Re-factoring the Test Harness


Now we want to make sure that the method is working correctly and the easiest way to do this is to use our trusty Test Harness, which we've been using in previous articles.

First we update the service reference so it takes into account all our recent changes. Do this by right clicking the "JokeGen" file in the "Service Reference" folder and select "Update Service Reference".

Next we add a button to the form (I've really been lazy with this) and just dragged it onto the form and set the name and text).


Then we add the following code on the background:

private void btnGetAll_Click(object sender, EventArgs e)
{
using (JokeClient wcfJokeGen = new JokeClient())
{
WSGetAllRequest wsGetAllRequest = new WSGetAllRequest();
wsGetAllRequest.RefreshDate = DateTime.Now;
WSGetAllResponse wsGetAllResponse = wcfJokeGen.GetJokes(wsGetAllRequest);
MessageBox.Show(String.Format("Records returned = {0}", wsGetAllResponse.Jokes.Length), "Record Count", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
}


Compile and run the application. When the form appears press the "Get All" button and you should see an information box showing the number of records.



Check the message formats



Checking the SOAP messages sent to and from the application using the Service Trace Viewer gives us the format of the messages we have the send and decode on the iPhone.

WSGetAllRequest
<s:Body xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<GetJokes xmlns="http://tempuri.org/">
<wsJokeRequest>
<RefreshDate>2008-12-09T16:18:36.5706144 08:00</RefreshDate>
</wsJokeRequest>
</GetJokes>
</s:Body>


WSGetAllResponse
<s:Body xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<GetJokesResponse xmlns="http://tempuri.org/">
<GetJokesResult>
<Jokes>
<Joke>
<EntityKey>
<EntitySetName>Joke</EntitySetName>
<EntityContainerName>JokeGenDBEntities</EntityContainerName>
<EntityKeyValues>
<EntityKeyMember>
<Key>ID</Key>
<Value xsi:type="xsd:int">1</Value>
</EntityKeyMember>
</EntityKeyValues>
</EntityKey>
<ID>1</ID>
<Text>"Knock, Knock". "Who's there?", "Sarah", "Sarah Who?", "Sarah doctor in the house."</Text>
<JokeCategoryReference>
<EntityKey>
<EntitySetName>JokeCategory</EntitySetName>
<EntityContainerName>JokeGenDBEntities</EntityContainerName>
<EntityKeyValues>
<EntityKeyMember>
<Key>Category</Key>
<Value xsi:type="xsd:int">1</Value>
</EntityKeyMember>
</EntityKeyValues>
</EntityKey>
</JokeCategoryReference>
</Joke>

... clipped ....

</Jokes>
</GetJokesResult>
</GetJokesResponse>
</s:Body>

Next update we'll modify the iPhone application to store this locally within the applications memory.

Tuesday, November 18, 2008

Mac To WCF WebService - Passing Parameters Step 2

In the last post I'd reached the stage of being able to call a WCF service and receive a response, however had the problem that my iPhone application SOAP request has slightly out of alignment.
To recap my application expects:
<s:Body>
<Generate xmlns="http://tempuri.org/">
<noun1>hello</noun1>
<noun2>hello</noun2>
</Generate>
</s:Body>

While the iPhone application is sending:
<SOAP-ENV:Body>
<Generate xmlns=\"http://tempuri.org/\">
<parameters xsi:type=\"xsd:string\">hello</parameters>
</Generate>
</SOAP-ENV:Body>

Looking within the JokeGen.m file shows how the XML for the parameters are formed:
- (void) setParameters:(CFTypeRef) in_parameters
{
id _paramValues[] = {
in_parameters,
};
NSString* _paramNames[] = {
@"parameters",
};
[super setParameters:1 values: _paramValues names: _paramNames];
}

However as this is using a NSDictionary pointer to hold the information passed to the object base in WSGeneratedObj.m (shown below) we loose the ability to manipulate this information easily. Converting to a NSMutableDictionary would be the obvious answer, however this would mean changing the base generated code and really did not seem like the right approach:

- (void) setParameters:(int) count values:(id*) values names:(NSString**) names
{
NSDictionary* params = [NSDictionary dictionaryWithObjects:values forKeys: names count: count];
NSArray* paramOrder = [NSArray arrayWithObjects: names count: count];
WSMethodInvocationSetParameters([self getRef], (CFDictionaryRef) params, (CFArrayRef) paramOrder);
}


So how to get round this little issue? Well, what we need is a workable model or pattern that could be used over and over again and cope with the inevitable changes that would be required long term as the WCF is updated with additional fields or methods. So I chose to revert to an old fashioned WebContract pattern which has served me well over the years. This would unfortunately require a bit of re-factoring of both the iPhone application and the WCF Service, however the impact should be minimal.

First thing we need to do is to minimize as much of the hard coding that currently exists. Without the use of reflection in the SDK I can't use any of my usual short cuts however there are a few tricks available. We need to create a defined contract between the services which I'm calling WSJokeRequest. Create this in XCode using File/New File and select "NSObject subclass" and click Next. Enter the file name "WSJokeRequest" making sure that "Also create "WSJokeRequest.h" is ticked and click Finish

Edit WSJokeRequest.h and enter the following code:

@interface WSJokeRequest : NSObject {
NSMutableDictionary *resultDictionary;
}

-(id)initWithDictionary:(NSDictionary *)dictionary;
-(NSDictionary *)dictionary;

-(void)setNoun1:(NSString *)noun1;
-(NSString *)noun2;
-(void)setNoun2:(NSString *)noun2;
-(NSString *)noun1;

@end


We are going to create a defined object which holds all the required keys on initialization and then provide methods to alter these keys any time we need, i.e. simple get/set methods Object C style.

Edit WSJokeRequest.m and enter the following code:

@implementation WSJokeRequest
-(id)init
{
return [self initWithDictionary:[NSDictionary dictionaryWithObjectsAndKeys:
@"", @"noun1",
@"", @"noun2",
nil]];
}

-(id)initWithDictionary:(NSDictionary *)dictionary
{
self = [super init];
resultDictionary = [[dictionary mutableCopy] retain];
[resultDictionary setObject:@"http://tempuri.org/" forKey:(NSString *)kWSRecordNamespaceURI];
[resultDictionary setObject:@"WSJokeRequest" forKey:(NSString *)kWSRecordType];
return self;
}

-(void)dealloc
{
[resultDictionary release];
[super dealloc];
}

-(NSDictionary *)dictionary
{
return resultDictionary;
}

-(NSString *)noun1
{
NSString *value = [resultDictionary objectForKey:@"noun1"];
return value;
}

-(void)setNoun1:(NSString *)Noun1
{
[resultDictionary setObject:Noun1 forKey:@"noun1"];
}

-(NSString *)noun2
{
NSString *value = [resultDictionary objectForKey:@"noun2"];
return value;
}

-(void)setNoun2:(NSString *)Noun2
{
[resultDictionary setObject:Noun2 forKey:@"noun2"];
}
@end


Now we open the View Controller code (iWCFDemoViewController.m) and re-factor the pingTest method to make use of this new contract.

-(IBAction)pingTest:(id)sender
{
NSLog(@"Pressed!!");
WSJokeRequest *wsJokeRequest = [[WSJokeRequest alloc] init];
[wsJokeRequest setNoun1:@"hello"];
[wsJokeRequest setNoun2:@"there"];
NSString *result = [JokeService Generate:[wsJokeRequest dictionary]];
NSLog([result description]);
resultsField.text = [result objectForKey: @"GenerateResult"];
}


The last step is to change the JokeGen.m to use the name of the new Contract object:
- (void) setParameters:(CFTypeRef) in_parameters
{
id _paramValues[] = {
in_parameters,
};
NSString* _paramNames[] = {
@"wsJokeRequest",
};
[super setParameters:1 values: _paramValues names: _paramNames];
}


Running the application in debug mode now produces a well formed SOAP request in the form we can work with:
<s:Body>
<Generate xmlns="http://tempuri.org/">
<wsJokeRequest xsi:type="WSJokeRequest">
<noun1>hello</noun1>
<noun2>there</noun2>
</wsJokeRequest>
</Generate>
</s:Body>

Now it's time to re-factor the WCF side of the contract which currently still expects two string 'noun1' and 'noun2'.

First we add a new class to the WebService in Visual Studio called WSJokeReqest by right clicking on the APP_Code folder and selecting Add New Item. From the dialogue box select Class and enter WSJokeReqest.cs into the name field. When the file appears enter the following code:


using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Runtime.Serialization;

///
/// Summary description for WSJokeRequest
///

[DataContract(Namespace="http://tempri.org/")]
public class WSJokeRequest
{
public string noun1 { get; set; }
public string noun2 { get; set; }
}


Now we change the WCF Interface (IJoke,cs) and Class (Joke.cs) files to make use of this as a parameter into the Generate function.
In iJoke.cs the contract changes from:
string Generate(string noun1, string noun2);
to
[OperationContract]
string Generate(WSJokeRequest wsJokeRequest);

In Joke.cs the Generate method changes from:

public string Generate(string noun1, string noun2)
{
return String.Format("What do you call an Irish {0}?\n A: An {1}", noun1, noun2);
}

to

public string Generate(WSJokeRequest wsJokeRequest)
{
return String.Format("What do you call an Irish {0}?\n A: An {1}", wsJokeRequest.noun1, wsJokeRequest.noun2);
}

The application should now build successfully without any errors.

Finally we need to update our test harness application which was created to verify that our XML request is being formed correctly. If you followed the instructions in the previous post you should have a JokeGenTestHarness project already included in the current solution. If not you should open in Visual Studio that now. Right click the Service reference called JokeGen within the ServiceReferences folder and select "Update Service Reference". Once this in complete we edit the Form.cs code by right clicking the file and selecting "View code". Change the click method on the form to

private void btnPress_Click(object sender, EventArgs e)
{
using (JokeClient wcfJokeGen = new JokeClient())
{
WSJokeRequest WSJokeRequest = new WSJokeRequest();
WSJokeRequest.noun1 = txtEntry.Text;
WSJokeRequest.noun2 = txtEntry.Text;
txtResult.Text = wcfJokeGen.Generate(WSJokeRequest);
}
}



Recompile the application and start the Webservice and test harness in debug mode. Entering the value "Hello" into the Noun1 field in the test harness should return "What do you call an Irish Hello? A: An Hello" in the results field.



Checking the Service Trace Viewer shows that we are now sending the following XML:
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
<s:Header>
<To s:mustUnderstand="1" xmlns="http://schemas.microsoft.com/ws/2005/05/addressing/none">http://localhost:8888/JokeGen/Joke.svc</To>
<Action s:mustUnderstand="1" xmlns="http://schemas.microsoft.com/ws/2005/05/addressing/none">http://tempuri.org/IJoke/Generate</Action>
</s:Header>
<s:Body>
<Generate xmlns="http://tempuri.org/">
<wsJokeRequest xmlns:a="http://schemas.datacontract.org/2004/07/" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
<a:noun1>Hello</a:noun1>
<a:noun2>Hello</a:noun2>
</wsJokeRequest>
</Generate>
</s:Body>
</s:Envelope>

Woo Hoo!! All systems Go.... ! Now we try the iPhone application running in debug mode.

Unfortunately when we run the application we get the error:
"/FaultCode" = -1;
"/FaultString" = "The formatter threw an exception while trying to deserialize the message: There was an error while trying to deserialize parameter http://tempuri.org/:wsJokeRequest. The InnerException message was 'Error in line 10 position 79. Element 'http://tempuri.org/:wsJokeRequest' contains data of the 'http://tempuri.org/:WSJokeRequest' data contract. The deserializer has no knowledge of any type that maps to this contract. Add the type corresponding to 'WSJokeRequest' to the list of known types - for example, by using the KnownTypeAttribute attribute or by adding it to the list of known types passed to DataContractSerializer.'. Please see InnerException for more details.";

Luckily I'd run across this problem before in WCF. reason for this is that by default the deserializer is using DataContractSeriazer which is fine for Microsoft native applications but it won't play fair with other connections. To address this we decorate the interface file IJoke.cs with the XmlSerializerFormat attribute like so and all should be well:
[XmlSerializerFormat]
[ServiceContract(Namespace="http://tempuri.org/")]
public interface IJoke

Running the iPhone application now correctly takes in the values and returns the expected string.



Next iteration of this application will be local storage and retrieval of data.

Friday, November 14, 2008

Mac To WCF WebService - Passing Parameters

In my last posting I showed the process of creating a iPhone application which can be used to call a WCF WebService and return a valid result. The next stage in the evolution of this application is to modify the code to send values to the service which acts on those values and returns specific results.

Working on this I figured would be a simple matter of adding some structure to the generated proxy classes and away we go, but sadly Apple again let us down on the really bad implementation of the data structures. This of course leads me to describe the many. many extra functions you will need to do to get this element to work.

First lets review our WCF WebService, which was defined in my previous article, we have two simple public methods; "bool Ping()" and "string Generate(string noun1, string noun2)". I've already shown how to connect to the first, now lets focus on the second. Generate takes two string parameters, then concatenates these within a Joke which is returned to the User. On the Objective C side of the equation I was simply going to change the calling code in the pingTest method from:

-(IBAction)pingTest:(id)sender
{
NSLog(@"Pressed: ");
NSString *result = [[JokeGen Ping:@""] objectForKey:@"/Result"];
NSLog([result description]);
resultsField.text = [result objectForKey: @"PingResult"];
}

To
-(IBAction)pingTest:(id)sender
{
NSLog(@"Pressed: ");
NSString *result = [[JokeGen Genereate:@"hello"] objectForKey:@"/Result"];
NSLog([result description]);
resultsField.text = [result objectForKey: @"GenerateResult"];
}
-(IBAction)pingTest:(id)sender
{
NSLog(@"Pressed: ");
NSString *result = [[JokeGen Ping:@""] objectForKey:@"/Result"];
NSLog([result description]);
resultsField.text = [result objectForKey: @"GenerateResult"];
}


The rational being that all all I was doing was changing the parameters which went into the SOAP request and therefore everything should just work. Oh how wrong can one person be ..... :-) Make this change in the code worked fine for the service call, I got my string back from the WebService, but no parameters had been passed. Setting a break point with Visual Studio confirmed that the two values for noun1 and noun2 where null.

Luckily as part of my investigations I'd learned quite a bit about debugging the SOAP requests between both applications. Using svctraceviewer which I described previously seemed to give me some of the information but sadly not the actual XML messages sent and received. What I needed to do was to obtain the actual SOAP request being sent via the iPhone application and compare that to a working SOAP request from a .Net Client application.

First I reviewed the XML being sent from the iPhone in the Debug screen in XCode, I'd already managed to do that using extra settings in the Generate stub which is described in more detail here. This tells me that I was sending the following:

"/WSDebugInBody" = "<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\"><s:Header><ActivityId CorrelationId=\"ecb10aea-3e99-4472-bed1-0100aecb517e\" xmlns=\"http://schemas.microsoft.com/2004/09/ServiceModel/Diagnostics\">574f4c95-a28f-4a8b-ab6e-8ad0f16152c4</ActivityId></s:Header><s:Body><GenerateResponse xmlns=\"http://tempuri.org/\"><GenerateResult>What do you call an Irish ?\n A: An</GenerateResult></GenerateResponse></s:Body></s:Envelope>";
"/WSDebugInHeaders" = {
"Cache-Control" = private;
Connection = close;
"Content-Length" = 434;
"Content-Type" = "text/xml; charset=utf-8";
Date = "Thu, 04 Dec 2008 18:59:03 GMT";
Microsoftofficewebserver = "5.0_Pub";
Server = "Microsoft-IIS/6.0";
"X-Aspnet-Version" = "2.0.50727";
"X-Powered-By" = "ASP.NET";
};
"/WSDebugOutBody" = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\n <SOAP-ENV:Envelope\n\n xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\"\n\n xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\n xmlns:SOAP-ENC=\"http://schemas.xmlsoap.org/soap/encoding/\"\n\n SOAP-ENV:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\"\n\n xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\">\n\n <SOAP-ENV:Body>\n\n <Generate xmlns=\"http://tempuri.org/\">\n\n <parameters xsi:type=\"xsd:string\">hello</parameters>\n\n </Generate>\n\n </SOAP-ENV:Body>\n\n </SOAP-ENV:Envelope>\n\n";
"/WSDebugOutHeaders" = {
"Content-Type" = "text/xml";
Host = "192.168.1.106";
Soapaction = "http://tempuri.org/IJoke/Generate";
"User-Agent" = "Mac OS X; WebServicesCore.framework (1.0.0)";
};
"/WSHTTPMessageHeaders" = (
<NSCFType: 0x556730>
);
"/kWSHTTPResponseMessage" = <NSCFType: 0x543020>;
GenerateResult = "What do you call an Irish ?\n A: An";
}

Next I had to increase the logging for the Service and to do this I had to make additional changes to the services web.config file.

<system.diagnostics>
<trace autoflush="true" />
<sources>
<source name="System.ServiceModel.MessageLogging" switchValue="Verbose,ActivityTracing">
<listeners>
<add type="System.Diagnostics.DefaultTraceListener" name="Default">
<filter type="" />
</add>
<add name="ServiceModelMessageLoggingListener">
<filter type="" />
</add>
</listeners>
</source>
<source name="System.ServiceModel"
switchValue="Information, Verbose, ActivityTracing"
propagateActivity="true">
<listeners>
<add type="System.Diagnostics.DefaultTraceListener" name="Default">
<filter type="" />
</add>
<add name="ServiceModelTraceListener">
<filter type="" />
</add>
<add name="sdt"
type="System.Diagnostics.XmlWriterTraceListener"
initializeData= "SdrConfigExample.e2e" />
</listeners>
</source>
</sources>
<sharedListeners>
<!-- This is where the messages will be logged -->
<add initializeData="Web_messages.svclog"
type="System.Diagnostics.XmlWriterTraceListener, System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
name="ServiceModelMessageLoggingListener" traceOutputOptions="Timestamp">
<filter type="" />
</add>
<add initializeData="Web_tracelog.svclog"
type="System.Diagnostics.XmlWriterTraceListener, System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
name="ServiceModelTraceListener" traceOutputOptions="Timestamp">
<filter type="" />
</add>
</sharedListeners>
</system.diagnostics>

And adding to the existing system.serviceModel tags..

<diagnostics wmiProviderEnabled="true">
<messageLogging logEntireMessage="true" logMalformedMessages="true"
logMessagesAtServiceLevel="true" logMessagesAtTransportLevel="true" />
</diagnostics>

The above should produced two extra files Web_messages.svclog and Web_tracelog.svclog which can be opened by the the svctraceviewer application. The resulting values looked like the following, with the right panel showing the actual XML being passed e.g.:


Now all I needed to do compare the SOAL XML from the iPhone to a working version, so I created a small Client application in Visual Studio 2008 added the WebReference to my JokeGen application and executed the call. The code for this was very simple:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using JokeGenTestHarness.JokeGen;

namespace JokeGenTestHarness
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}

private void btnPress_Click(object sender, EventArgs e)
{
using (JokeClient wcfJokeGen = new JokeClient())
{
txtResult.Text = wcfJokeGen.Generate(txtEntry.Text, txtEntry.Text);
}
}
}
}

with a simple WinForm which looked like this:



The results of this showed that there was a slight difference in the way the Body tag was being formed.
iPhone:
<SOAP-ENV:Body>
<Generate xmlns=\"http://tempuri.org/\">
<parameters xsi:type=\"xsd:string\">hello</parameters>
</Generate>
</SOAP-ENV:Body>

Windows Test App, assuming all the NameSpace and Header information is correct:
<s:Body>
<Generate xmlns="http://tempuri.org/">
<noun1>hello</noun1>
<noun2>hello</noun2>
</Generate>
</s:Body>

OK.. Now we are getting somewhere! All thats needed is for the parameters tag to be changed to a <noun1> and <noun2> and it should just work. How to do this will have to wait until my next posting.......

Saturday, November 8, 2008

Tracing WCF messages on a Windows Server

If you use WCF for communication, you'll need to do quite a bit of debugging if you ever want to get to the bottom of serialization issues. Microsoft has thankfully provided a responsible tracing application which is installed with the SDK.

The Service Trace Viewer tool reads the default log file produced from the Server. To get this to be produced by your WCF Server simply add the following section to your web.config file.




switchValue="Information, ActivityTracing"
propagateActivity="true">

type="System.Diagnostics.XmlWriterTraceListener"
initializeData= "SdrConfigExample.e2e" />






This will produce a file in the root directory called SdrConfigExample.e2e. Start the Trace Viewer by clicking on the SvcTraceViewer.exe which is probably located in the default installation area C:\Program Files\Microsoft SDKs\Windows\v6.0A\Bin. Click File/Open and select the SdrConfigExample.e2e file, you'll probably have to select the "All Files (*.*)" option.
As soon as this is opened you should see all the messages which have been sent.

Tuesday, November 4, 2008

Adding Additional Debugging to a WCF call

I've recently been having a number of problems sending complex data types to a WCF service from an iPhone application I've been writing. I found this posting on the Apple site which increases the amount of debug information sent on each call.

// set debug props
WSMethodInvocationSetProperty(soapReq, kWSDebugIncomingBody, kCFBooleanTrue);
WSMethodInvocationSetProperty(soapReq, kWSDebugIncomingHeaders, kCFBooleanTrue);
WSMethodInvocationSetProperty(soapReq, kWSDebugOutgoingBody, kCFBooleanTrue);
WSMethodInvocationSetProperty(soapReq, kWSDebugOutgoingHeaders, kCFBooleanTrue);

Adding this to the genCreateInvocationRef method in your generated code from WSMakeStub did not require a lot of effort. I'd probably recommend it for each call.

e.g.

- (WSMethodInvocationRef) genCreateInvocationRef
{
WSMethodInvocationRef ref = [self createInvocationRef
/*endpoint*/: @"[URL]"
methodName: @"Generate"
protocol: (NSString*) kWSSOAP2001Protocol
style: (NSString*) kWSSOAPStyleDoc
soapAction: @"[method]"
methodNamespace: @"[namespace]"
];

WSMethodInvocationSetProperty(ref, kWSDebugIncomingBody, kCFBooleanTrue);
WSMethodInvocationSetProperty(ref, kWSDebugIncomingHeaders, kCFBooleanTrue);
WSMethodInvocationSetProperty(ref, kWSDebugOutgoingBody, kCFBooleanTrue);
WSMethodInvocationSetProperty(ref, kWSDebugOutgoingHeaders, kCFBooleanTrue);
return ref;

}

Creating Local DNS entries on the Leopard

I recently had a problem with a WCF service which would not resolve correctly on my Mac. It turned out that although I was accessing the Service using an IP address the WSDL XML file contained a number of references to the host name. To address this I needed to create a local DNS lookup so it could resolve correctly.


In Windows this is a simple matter of editing the Hosts file in the C:\WINDOWS\system32\drivers\etc\hosts file.

For the Mac OSX we use the command:
sudo dscl localhost -create /Local/Default/Hosts/myHost IPAddress [IPAddress], then Enter the administrator password. Where myHost is the name of the host and IPAddess the address to resolve too.
Removing the entry is a matter of entering:
sudo dscl localhost -delete /Local/Default/Hosts/myHost

Sunday, October 26, 2008

Mac to WCF Webservice - step 3

Following a substantial amount of head scratching with regard to connecting my Mac to a WCF WebService I'm finally at the stage of moving this to the iPhone environment. This application will build on the WCF service which was created in my earlier article.

  • Create a new iPhone application in XCode of type View Based Application.

  • Give it a meaningful name, I used iWCFDemo.

  • Add the following code to the iWCFDemoViewController.h file


  • @interface iWCFDemoViewController : UIViewController {
    IBOutlet UITextField *resultsField;
    }
    -(IBAction)pingTest:(id)sender;
    @end


  • Add the following code to the iWCFDemoViewController.h file


  • -(IBAction)pingTest:(id)sender
    {
    NSLog(@"Pressed!!");
    }


    This will allow for one button to invoke the WebService request and a text field to display the results following the call.


  • Double click the MainWindow.xib file to start the Interface Builder.

  • - Drag a Text Field from the Library onto the View Suface.
    - Drag a Round Rect Button onto the View Surface and give it the text "Press Me"
    - Drag a label onto the View Surface and give it the text "Results:"
    The result should look like the following:

  • Select the "File's Owner" from the xib view and drag a connection from the pingTest outlet to the button on the Layout Surface. Connect it to the "Touch Down" event.

  • Drag a connection from the resultsField Outlet to the Text Field on the Layout Surface.

  • The result should look like the following:

  • Save all the file and hit "Build and Go", when the Simulator appears, pressing the "Press Me" Button should produce the "Pressed!!" message in the Debugger Console.
    The results should look like something the following:

  • Now we add the connection to the WebService in by Generating the Proxi using WSMakeStub. Start a Terminal session go to the /Developer/Tools directory. Execute the command "WSMakeStubs -x ObjC -dir /Users/username/ -name JokeGen -url http://192.168.1.106/JokeGen/Joke.svc?wsdl" where the username is a valid location on your machine. This will produce the 4 files you need to access the WebService. Add these generated files to your XCode solution.

  • Modify the JokeGen.m file by adding the methodNamespace: @"http://tempuri.org/" and changing the Style parameter to (NSString*) kWSSOAPStyleDoc

  • Search for the resultsValue method in JokeGen.m and replace the generated with:
    - (id) resultValue
    {
    // return [[super getResultDictionary] objectForKey: @"parameters"];
    return [super getResultDictionary];
    }

    We do this so we can see Null values being passed back regardless of what type is sent via the WCF method.

  • Modify the WSGeneratedObj.m file by changing the generated code as its not correct first time out. Due to a conflict in the return values from the SOAP call we need to change the variable from "soapAction" to "soapActionKey".
    NSString* soapActionKey = @"SOAPAction";
    NSDictionary* headers = [self copyHeaderDictionary:1 extraVals:&soapAction extraKeys:&soapActionKey];

  • Add the import statements to the iWCFDemoViewController.m file, #import "WSGeneratedObj.h" and #import "JokeGen.h". Finally modify the pingTest method to invoke the WebService.

    -(IBAction)pingTest:(id)sender
    {
    NSLog(@"Pressed: ");
    NSString *result = [[JokeGen Ping:@""] objectForKey:@"/Result"];
    NSLog([result description]);
    resultsField.text = [result objectForKey: @"PingResult"];
    }


  • As these WebService calls require a number of additional functions which are not part of the standard application build so we have to add the CoreService. Select Add/Existing Framework from Frameworks folder in the Project tree. Browse to the Frameworks folder and select the CoreServices.framework and click OK

  • "Build and Go" the application and the result should be as follows:

    With the Debug window showing the full XML results for the Service Call:




OK so this is hardly an Earth shattering example, however it does show that the Mac OS and Windows .Net are valid options for developers.

Tuesday, September 23, 2008

Mac to WCF Webservice - step 2

Following the failure of my previous attempt to connect a Cocoa application to my Microsoft WCF WebService (Mac to WCF-It Never Happened), I spent some time investigating the issues. It would seem that all the elements are there within the Cocoa Framework to accomplish what I want, but from reading the various blogs its not a simple matter of using SVCUTIL to pump out a proxy class to the client.

Not to be discouraged, I have found that the Cocoa equivalent of SVCUTIL (the Microsoft Proxy class generator) is called "WSMakeStubs" and its accessible from the command line. Assuming your webservice is up and running (read step 2 for instructions), start the Terminal application and change your directory to /Developer/Tools. Type the following command:
WSMakeStubs -x ObjC -dir [output location] -name [ObjectName] -url [url to the WCF file]?wsdl

eg:
WSMakeStubs -x ObjC -dir /Users/User1 -name JokeGen -url http://192.168.1.106:8080/JokeGen/Joke.svc?wsdl

This created me 4 files;
  • WSGeneratedObj.m

  • WSGeneratedObj.h

  • JokeGen.m

  • JokeGen.h


Which can then be added to the XCode project and referenced in the AppContoller.m by adding the following lines:
#import "WSGeneratedObj.h"
#import "JokeGen.h"


Adding a button to the application and joining to the Outlet was followed by adding the following code to the AppContoller.m file:

-(IBAction)pingTest:(id)sender
{
// Show the User that something is going on
[progress startAnimation:nil];

//Create the URL
WSMethodInvocationRef soapCall;
WSGeneratedObj *sq = [[WSGeneratedObj alloc] init];
soapCall = [sq
createInvocationRef: @"http://192.168.1.106:8080/JokeGen/Joke.svc"
methodName: @"Ping"
protocol: (NSString*) kWSSOAP2001Protocol
style: (NSString*) kWSSOAPStyleRPC
soapAction: @"http://tempuri.org/IJoke/Ping"
methodNamespace: NULL
];

NSDictionary *result;

result = (NSDictionary *)WSMethodInvocationInvoke(soapCall);

if(WSMethodResultIsFault) {
NSLog(@"Error:");
NSLog([result description]);
} else {
NSLog(@"No Error:");
NSLog([result description]);
}
[sq release];

//Update the interface
[progress stopAnimation:nil];
}


Following a Build and Run I received the following error message:

Current language: auto; currently objective-c
2008-09-23 17:27:50.640 AmaZone[1166:813] Error:
2008-09-23 17:27:50.642 AmaZone[1166:813] {
"/FaultCode" = -1;
"/FaultString" = "The message with Action 'SOAPAction' cannot be processed at the receiver, due to a ContractFilter mismatch at the EndpointDispatcher. This may be because of either a contract mismatch (mismatched Actions between sender and receiver) or a binding/security mismatch between the sender and the receiver. Check that sender and receiver have the same contract and the same binding (including security requirements, e.g. Message, Transport, None).";
"/kWSHTTPResponseMessage" = ;
"/kWSResultIsFault" = 1;
}


I assume that this was due to some basic miss-match between the client SOAP request and that expected by the server. Doing a number of tests and investigations it turned out that the file created by WSMakeStubs misses out on some important information.
The method Namespace for the WebService call is initially set to NULL, when it should match the SoapAction "http://tempuri.org/", be sure to include the trailing "/".

Following all these changes you should be able to call and retrieve the WCF WebService from a Mac Application.