Archive

Archive for the ‘iPhone’ Category

Using .NET Web Services And Datasets with iPhone Revision 2!

May 28, 2010 o0joecool0o 23 comments

***Updated to work with iPhone iOS4.0.1 There seems to be a problem with the old code on version 4.0+ where the last record crashes the app in the XMLDataSetParser.m class Thanks to Douwe Querty for submitting a fix!

Well I must apologize for the amount of time it has taken me to post this, I have been finished this version for quite a while however our company has been extremely busy and I have not had time to post this! But fret no more for here it is! the second version of the iPhone Dataset Parser! I will now call this project the NETDataset Project.

Let me re-iterate before we begin that I am a self taught developer coming from .NET to iphone development, and while I am pretty well versed in objective C now, I am by no means an expert. If you see something that needs to be fixed such as memory leaks proper usage of functions etc, please contact me and tell me how to fix those problems instead of just spamming in the comments about how its full of memory leaks or how I am not coding this properly.

(by the way I discovered analyzer and its tantalizingness since the last version so this version is now memory leak free!)

Whats New in this version?

  • A Sample project has been provided to make things easier to understand and implement
  • Using Asynchronous calls now shown in sample project
  • Connecting Data to your UITableView in sample project
  • Better table structure, can now reference data by using column names as the key.
  • Sorting Data Alphabetically
  • Performance increases
  • Memory Leak fixes

If you are updating from the old code you will find that almost everything relating to the querying/dataset functions no longer work the same so be aware if this is an upgrade you will need to make some changes to the way you handle the data. (Which is far far easier now by the way)

The biggest difference is the way the data is now returned from the parser. We now have a structure that looks like this

Dataset

NSDictionary(Tables) key=tablename value=arrayofTables
----------NSDictionary (Table) key=rowid value=arrayofrows
------------------------------------NSDictionary(Rows)key=columnName value=columnvalue

this allows us to ask for a column value using the column name!!! so much nicer!
for example to get a column value all we do is call
[row objectForKey:@"Column1"]

Lets start again with the essential files

Download the new version and sample project here

add these files to your project including the example cacheDBCommands files

Simply drag the NETDataset folder from the sample project to your own project the folder should contain the same 6 files

To follow along this example you should ALSO copy the CacheDBCommands.h and .m files you should now have all these files in your project

DataSet.h
DataSet.m
XMLDataSetParser.h
XMLDataSetParser.m
WebServiceHelper.h
WebServiceHelper.m
CacheDBCommands.h
CacheDBCommands.m

1. First go to your view controller and include the CacheDBCommands.h file to your viewController.h file

#import "CacheDBCommands.h"

2. Add the CacheDBDelegate to your view controllers header file

@interface DatasetParserViewController : UITableViewController <CacheDBDelegate> {

3. Create a property/Variable in your view controller for the cachedb class

CacheDBCommands *cacheDB;
@property(nonatomic, retain)CacheDBCommands *cacheDB;

4.Initialize cachedb and set delegate in your view controllers viewDidLoad event

cacheDB = [[CacheDBCommands alloc] init];
cacheDB.delegate = self;

5.Call your download function from the view controller

[cacheDB initDownloadMyDataset:@"param1Value" Param2:@"pram2Value"];

6. Modify the CacheDBCommands class to  suit your needs

The initDownloadMyDataset function is simply a handler method that creates a new Asynchronous operation for your actual method call. This way your app doesnt hang waiting for a response. It creates an NSInvocationOperation which contains your actual working method and adds it to the NSOperationQueue.  We pass any parameters along with the operation, since the NSOperation only accepts a single object for parameters, we need to create an array of parameters and pass the single array object along if we have more than one parameter for our worker method.

In the example the working operation method is called getMyDataset and it accepts the single array parameter that we passed to the NSInvocationOperation.

This method is where the actual connection to the server is made and the data passed to the web service.
Lets break it down! o/< o|< o\<
ok all dancing aside…
first we get our parameters out of the array

NSString *param1 = [params objectAtIndex:0];
NSString *param2 = [params objectAtIndex:1];

Next we create a WebServiceHelper to make our connection to the server

// Create an object to the class above which is the connection to the WCF Service
WebServiceHelper *DataCon = [[WebServiceHelper alloc] init];

Now we set the connection info which includes the XMLNamespace (which must match EXACTLY to what you have set in your web service)

//set up service method and urls
DataCon.XMLNameSpace = self.XMLNamespace;
DataCon.XMLURLAddress = self.ServerURL;

Next we need to specify which method in the web service we want to invoke. Now the most important thing to remember is the method name and its parameters must
also match EXACTLY to what you have in your web service.

//set up method and parameters
DataCon.MethodName = @"getMyDataset";

DataCon.MethodParameters = [[NSMutableDictionary alloc] init];
[DataCon.MethodParameters setObject:param1 forKey:@"param1"];
[DataCon.MethodParameters setObject:param2 forKey:@"param2"];

This next part simply initiates the call and downloads the response as raw data into the NSMutableData object

NSMutableData *retData;
retData = [DataCon initiateConnection];

Now that we have our data we want to make it into something useful, like a dataset! This is where the Dataset and XMLDatasetParser classes do their magic!
If you wanted to instead retrieve something else like a string or a boolean or an integer from your service method, you would need to write your own NSXMLParser class to handle those. I have not had a need for this yet as I am fine with just passing back datasets. If you do end up writing one email it to me and I will add it to the NETDataset project :)
If you uncomment the doDebug line you will get the XML returned from the soap service NSLogged to the console. This becomes very useful when debugging.
once parseXMLWithData is called the dataset object will now contain the tables and rows from your dataset.

//self.myDataset.doDebug = YES; //uncomment to print all data to NSLog
[self.myDataset parseXMLWithData:retData];

[DataCon release];

The last step in the cacheDbCommands class is to call our delegate function which lets the View Controller know we are done downloading the dataset and we have our data!

//call delegate to let view know we have finished downloading this
[delegate performSelectorOnMainThread:@selector(didFinishDownloading:)
withObject:self
waitUntilDone:NO];

7.Handle the delegate callback events

#pragma mark -
#pragma mark CacheDBCommands Delegate

- (void)willStartDownloading:(CacheDBCommands *)cacheDB
{
//NSLog(@"Delegate called start downloading", nil);
[self.act startAnimating];
self.act.hidden = NO;
}
-(void)didFinishDownloading:(CacheDBCommands *)acacheDB
{
//NSLog(@"Delegate called finish downloading", nil);

NSMutableDictionary *rows = [[NSMutableDictionary alloc] initWithDictionary:[cacheDB.myDataset getRowsForTable:@"Table1"]];
//each object in the rows dictionary contains a Key which is the rowid number and the value is a corresponding array of columns and values
NSArray *rowValues = [[NSMutableArray alloc] initWithArray:[rows allValues]];
//each object in the rowValues array is a dictionary object containing all of the columns and values
NSMutableArray *data = [[NSMutableArray alloc] initWithArray:rowValues];

[rows release];
[self.tableView reloadData];
[self.act stopAnimating];
}

8. Do what you want! Your Done!
You can also query the dataset for a value using the function getRowsForTableWhereColumnEquals I often use this to join two tables together or use the first table as a section header in the tableview and the second table as the data

This accepts 3 parameters TableName ColumnName and Searchstring

NSMutableArray *filteredData= [[NSMutableArray alloc] initWithArray:[cacheDB.myDataset getRowsForTableWhereColumnEquals:@"Table1" Column:@"Column1" Where:@"happyfish"]]];

Sorting Your Data

The NSXMLParser runs Asynchronously as it parses through the document, this is why even though a dataset is ordered sometimes your data is still not in the right order when you add the cells to your UITableview. Here is a utility function that I use to resort the order of an array of rows by column name.

*** updated now supports numerical ordering and duplicate entries since caseInsensitiveSearch does not order numbers properly in a string.

-(void)sortArrayContainingDictionaryItemsByKey:(NSString *)keyName ArrayToSort:(id)arrayToSort Ascending:(BOOL)asc isNumeric:(BOOL)isNumeric
{
NSMutableArray *unsortedValues = [[NSMutableArray alloc] init];
NSMutableArray *sortedArray = [[NSMutableArray alloc] init];
NSUInteger i,e;
for (i=0; i<[arrayToSort count]; i++) {
[unsortedValues addObject:[[arrayToSort objectAtIndex:i] objectForKey:keyName]];
}
//NSArray *sortedValues = [unsortedValues sortedArrayUsingSelector:@selector(caseInsensitiveCompare:)];
NSSortDescriptor *sortDesc;
if(isNumeric == YES){
sortDesc = [[NSSortDescriptor alloc] initWithKey:nil ascending:asc selector:@selector(numericCompare:)];
}
else {
sortDesc = [[NSSortDescriptor alloc] initWithKey:nil ascending:asc selector:@selector(caseInsensitiveCompare:)];
}
NSArray *sortedValues = [unsortedValues sortedArrayUsingDescriptors:[NSArray arrayWithObject:sortDesc]];
NSMutableDictionary *dupCount = [[NSMutableDictionary alloc] init];
for (i=0; i<[sortedValues count]; i++) {
//loop through each sorted item
NSInteger newcnt = 0;//reset newcnt for duplicates
for (e=0; e<[arrayToSort count]; e++) {
//set duplicate counter
//find the matching StyleName
NSString *avalue = [[NSString alloc] initWithString:[[arrayToSort objectAtIndex:e] objectForKey:keyName]];
if ([avalue isEqualToString:(NSString *)[sortedValues objectAtIndex:i]])
{
NSInteger cnt = (NSInteger)[dupCount objectForKey:avalue];
//this has been found previously we need to skip this index if we are not at the matchign duplciate position
if (newcnt == cnt || cnt == 0) {
//this is the right record to add
cnt++;
[dupCount setObject:[NSString stringWithFormat:@"%d", cnt, nil] forKey:avalue];
[sortedArray addObject:[arrayToSort objectAtIndex:e]];
[avalue release];
break;//break out in case there are duplicate matches
}else {
//this is the wrong record skip over this one
newcnt++;
}
}
[avalue release];
}
}
[dupCount release];
for (i=0; i<[sortedArray count]; i++) {
[arrayToSort replaceObjectAtIndex:i withObject:[sortedArray objectAtIndex:i]];
}
[sortDesc release];
[unsortedValues release];
[sortedArray release];
}


Now lets say I have a dataset containing multiple vehicles but I only want the cars and I want them sorted alphabetically.

NSMutableArray *myData= [[NSMutableArray alloc] initWithArray:[acacheDB.myDataset getRowsForTableWhereColumnEquals:@"Table1" Column:@"VehicleType" Where:@"Car"]];

[self sortArrayContainingDictionaryItemsByKey:@"VehicleType" ArrayToSort:myData isNumeric:NO];

Thats it! mydata should now be an array of rows sorted by the column!
to get the first row you just call

NSDictionary *firstRow = [myData objectAtIndex:0];

to get say the vehicle model I would add on

NSString *model = [firstRow objectForKey:@"VehicleModel"];

I hope this framework helps you along your way to creating some amazing apps! Please send me the links to your apps on the app store! I will post them here! As always if you have questions please ask in the comments I do my best to answer everyone!

Categories: Programming, iPhone

Using .NET web services and dataset objects in your iphone app

October 19, 2009 o0joecool0o 112 comments

iPhone XML SOAP or .NET Web Services

can seem overwhelming at first but its actually not too hard to do, you can even download a .NET dataset and query it on the iPhone!

Being a complete n00b to iphone’s cocoa touch api / objective C and then jumping in feet first to iphone programming is like being swallowed by an enormous whale. Its very dark, you can see the blow hole, but you know the only way of reaching it is to be blown out by a mucous-infused geisure of  hot and sticky whale snot. (if you have ever tried to obtain help on irc or various forums you know what I am talking about) not to mention the endless sea of apple docs that may or may not contain any information that you actually need depending on your luck.

That being said after much hair pulling reading docs forums and some examples from everywhere I have put together a simple solution to connect to a .NET web service over https and return a dataset object back to the iphone that can be queried and used accordingly.

Please Note this article is now deprecated you should use the new framework here
I Can not support this version any more if you ask for help with this version you will not be answered.

Download This: iPhone .NET Dataset Framework.zip

In the attached file you will find 6 documents:

DataSet.h
DataSet.m
XMLDataSetParser.h
XMLDataSetParser.m
WebServiceHelper.h
WebServiceHelper.m

These files are pretty much self explanatory. DataSet.h/.m contain code for creating your DataSet Object and contain methods for querying the dataset.

WebServiceHelper contains the code to connect to the appropriate web service with the appropriate methods and parameters

XMLDataSetParser will take the data received from the webservice and pipe it into your DataSet Object.

To use an XML .NET Web service all you need to do is include these files in your project and then call the following procedures.

//First import the relevant files to your viewcontroller class (XMLDataSetParser is
//included by the DataSet class so there is no need to import it here)

#import "WebServiceHelper.h"
#import "DataSet.h"

// Create a connection to the web service
WebServiceHelper *DataCon = [[WebServiceHelper alloc] init];
//set up service method and urls please visit the webservice address in a browser to
//get the exact strings the service expects

DataCon.XMLNameSpace = @"https://myurl.com/WebService";
DataCon.XMLURLAddress = @"https://myurl.com/WebService/service.asmx";
DataCon.MethodName = @"getDataSetFromServer";

//add parameters **PARAMETERS ARE CASE SENSITIVE MAKE SURE THEY ARE TYPED CORRECTLY
//AS THE SERVICE EXPECTS THEM**

DataCon.MethodParameters = [[NSMutableDictionary alloc] init];
[DataCon.MethodParameters setObject:@"Parameter1" forKey:@"P1"];
[DataCon.MethodParameters setObject:@"Parameter2" forKey:@"P2"];

//Connect to the service and retrieve the xml raw data into a NSMutableDataObject
//be sure to include a NSMutableData object called "data" or whatever you wish in
//your viewcontroller

self.data = [DataCon initiateConnection];

//Create a dataset object using the new raw data we received from the service
DataSet *dsMyDataSet = [[DataSet alloc] initWithXMLData:self.data];

//Now we have a dataset filled with data from the initWithXMLData command lets
//query some data from it!
//Set up a dictionary object to hold our query data from the dataset

NSMutableDictionary *mydata= [[NSMutableDictionary alloc] init];

//retrieve all rows from the selected table and column
mydata = [dsMyDataSet getRowsForTableAndColumn:@"Table1" col:@"Username" ];
//mydata should contain all rows from column "Username"

//retrieve all rows from the selected table and column where the CURRENT column
//matches a string

mydata = [dsMyDataSet
getRowsForTableAndColumnWhereEqualsString:@"Table1" col:@"username" where:@"bob"];
//mydata should contain all rows from column username containing the exact match "bob"

//retrieve all rows from the selected table and column where a DIFFERENT column
//matches a string

mydata = [dsMyDataSet
getRowsForTableAndColumnWhereColumnEqualsString:@"Table1"
col:@"username" whereColumn:@"email" whereValue:@"bob@microsoft.com"];
//mydata should contain all rows from column "username" where the column "email"
//contains the exact match "bob@microsoft.com"

//you can do what you want with the data at this point like a simple iteration
NSEnumerator *userIterator= [mydata objectEnumerator];

NSString *username;

while(username =  [userIterator nextObject])
{
NSLog(@"Username: %@", username);
}
[mydata release];

Thats it!!! Hope this helps someone else save as much time as it is now saving me!

Categories: Programming, iPhone