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.