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.