Sunday, September 21, 2008

Mac to WCF WebService - it never happened.

This article came about from my investigations into developing an iPhone application. As part of my test project I was looking for the iPhone to make a WebService call to my server for data updates. This would be the approach I've used for many .Net application. Other iPhone application also seem to have the same Service Orientated Architecture (SOA) design, so I figured "How hard could it be"?

First off the development was to be broken down into three parts;
1) Build a standard Cocoa application to run on on the Mac calling a generic webservice. Why do this, simply it's because there are good samples already out on the net.
2) Build a WCF Service serving up some content over basicHTTP protocol. I've chosen WCF rather than a standard WebService as I'm more comfortable in the environment and I figure that most Corporate customers would have lots of .Net coders who need to expose internal data using this method.
3) Link the existing Cocoa application to the WCF WebService.

If all this worked I was going then going to convert the Cocoa application to the iPhone.

--- 1) Cocoa application to read from a webservice -------
Building the Cocoa Application to read from a webservice was not difficult. As an example I chose the example from Aaron Hillegass's excellent book "Cocoa Programming for Mac OSX" (ISBN:0-321-50361-9) pp.345-352, where he connects to the Amazon website to get book lists. He used the built in framework to build the request to the URL and parsed the results into an itemNodes array:


// Get the string and precent-escape for insertion into URL
NSString *input = [searchField stringValue];
NSString *searchString = [input stringByAddingPercentEscapesUsingEncoding: NSUTF8StringEncoding];
//Create the URL
NSString *urlString = [NSString stringWithFormat:
@"http://ecs.amazonaws.com/onca/xml?"
@"Service=AWSECommerceService&"
@"AWSAccessKeyId=%@&"
@"Operation=ItemSearch&"
@"SearchIndex=Books&"
@"Keywords=%@&"
@"Version=2007-07-16",
AXWS_ID, searchString];
// Make the call
NSURL *url = [NSURL URLWithString:urlString];
NSURLRequest *urlRequest = [NSURLRequest requestWithURL:url
cachePolicy:NSURLRequestReturnCacheDataElseLoad
timeoutInterval:30];
// Fetch the XML response
NSData *urlData;
NSURLResponse *response;
NSError *error;
urlData = [NSURLConnection sendSynchronousRequest:urlRequest
returningResponse:&response
error:&error];
//Parse the XML Response
[doc release];
doc = [[NSXMLDocument alloc] initWithData:urlData
options:0
error:&error];
[itemNodes release];
itemNodes = [[doc nodesForXPath:@"ItemSearchResponse/Items/Item"
error:&error] retain];



While the Table view columns have been set with identified attributes of the xPath to the data e.g. "ItemAttributes/Title".

--- 2) Window WCF application to serve content -------
Building a WCF application using visual studio 2008 is a dream, simply create the service using File/New Web Site and select WCF Service from the list of options. I choose the name "JokeGen" for the project and renamed the default 'service.c" and "IService.c" files to "Joke". By default this will create the service using the wsHttpBinding within the web.config file. This provides message encryption for any traffic and from a Window Client this is a great idea. Unfortunately we are dealing here with a Mac, so I'm going to downgrade this to basicHttpBinding, that way we won't have to deal with manually decrypting the traffic. To do this edit the web.config file and change the line:
endpoint address="" binding="wsHttpBinding" contract="IJoke"
to
endpoint address="" binding="basicHttpBinding" contract="IJoke"

One other suggestion I'd make would be to remove the dynamic assignment of IP address while debugging. This is a setting on the web project file, setting the "Use Dynamic ports" to false and set the Port number to 8888. I do this as I like to test directly on my development box and as I'm using a VM I want to keep the URL consistent. We still need however to create the application in IIS for the site to be visible outside of the server.

We create two simple functions; first is "Ping" which just returns a "true" for testing purposes. The second is Generate which takes two nouns as string and returns a joke created from these values.
IJoke.C

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.Text;

// NOTE: If you change the interface name "IService" here, you must also update the reference to "IService" in Web.config.
[ServiceContract]
public interface IJoke
{
[OperationContract]
bool Ping();

[OperationContract]
string Generate(string noun1, string noun2);

}


Joke.c

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.Text;

public class Joke : IJoke
{
public bool Ping()
{
return true;
}

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


--- Changing the Mac application to the WCF service -------

Now we need to modify our previous application to point to the WCF Service. First off we need to check if the service is visible. The simplest way to do this if from your Mac browse to the Windows server (or VM in my case). First ping your Mac from the Windows server, then ping the Windows box from your Mac. Use IPConfig on the Windows box if your unsure what your external IP address would be or use the DNS name you have created. I received a "Safari can’t open the page “http://192.168.1.106:8080/JokeGen/Joke.svc” [The location of the WebSite in IIS] because it can’t find the server" which was a nice way of saying my Firewall was not co-operating. I also got a Username and Password login prompt so I needed to change the permissions for the virtual directory. Eventually you will see the standard service description page.

So now I went off looking for an example and this is what I found:
consuming_a_wsdl_web_service_from_cocoa__failed_attempt_1

Basically this post indicates that this task is going to be a lot harder than I first expected. I'll post again on the use of the WSMakeStubs application and maybe, just maybe be able to finish my dream of having a simple webservice that can be read from the Mac.

Wednesday, September 3, 2008

iPhone development

And so the adventure begins...

Following a few days off (I'm at the start of a long break from work) I've decided to deep dive into iPhone/iTouch Objective-C development. Seeing how the product has taken the market by storm it's easy to get swept up in the hype and loose sight of the down sides; however from what I've seen, these small hand held devices are going to make it into the Corporate space much faster than anyone expects. Also with wireless technology improvements in speed, availability and even reasonable 3G tariffs, these devices will be used for accessing lots of corporate information.

OK, so why then did the Microsoft offering never make it? How can the iPhone break the hold that Blackberry has on Corporate? These are valid questions that probably need to be answered, but I'll leave that to the history books. From my point of view the iPhone has something that the Microsoft/Blackberry/Symbian offering never had and that's a new way to interact with the software... touch screens. Can the other devices catch up? Sure, but for now iPhone is riding a wave that does not seem to be ready to crash anytime soon.

To be honest starting this project has been on my mind for some time, being in the Microsoft space for about 20 years you'd think I'd stick to what I know, but I've always been a generalist at heart and knowing how something works helps me in many ways. Mostly I find it helps in the management of projects using these technologies. A little bit of knowledge is very powerful, I'm sure we've all sat down with a third party supplier or staff developer and been told "Oh, that's going to take a few weeks", when you know it's only going to really take a few hours. It's not that the developer is wrong, maybe they just don't know or don't understand your requirements. Bridging that gap in understanding is vital to setting customers expectations and (more importantly) setting and keeping to your deadlines.

So I start this work in earnest.. moving from the comfort of C#, Visual Studio and Windows 2003 to the new spaces of OSX is going to be a hard slog. In the following blogs I'll document my experiences, which I hope will assist other developers along the same road.

My plan is simple.. document how I got a simple application from the planning to production environment over the next few weeks. That with luck will involve setting up the environment, formulating the plan, reviewing online resources, getting the app past Apple QA and documenting lots and lots of error messages.

As I once said at the start of what turned out to be the worst project of my career "It's going to be great!".

Monday, August 11, 2008

Getting to grips with Apache on the Mac - Part 1

First off, lets explain that this is only a first attempt at getting a full Web Service up and running on Leopard (OSX 10.5). Don't expect this to be an in depth explanation of the process, it is only a step by step guide and notes as I do it myself. Being a .Net developer I can find my way around IIS without any problem and moving to Unix is a bit of leap. With luck by the time I've completed this post, both you and I will know a little more than before.

1) Starting the Apache web server on 10.5 is the easy bit, as the software is already included in your installation. Simply go to the System Preferences options from the Apple icon. Select "Sharing" from the "Internet and Network" section. Click the "Web Sharing" on and you should get a HTTP link on the right showing the IP Address of the server.

2) Clicking this link will bring up a default Apache webpage showing
"If you can see this, it means that the installation of the Apache web server software on this system was successful. You may now add content to this directory and replace this page."
Clicking the site http://ipaddress/~username
brings up
"Forbidden You don't have permission to access /~username/ on this server."

3) "So where are all the files then?", I asked myself. Well, after some searching I found the configuration files in the /etc/apache2 directory on the System volume. Get there using the Terminal application and edit the "httpd.conf" file. This contains all the main information information. hostname, ServerAdmin email address, IP Address, etc.
The information in this file indicated that the root directory "INETPUB/wwwroot" in Windows is located in /Library/Webserver/Documents.

4) Editing the DocumentRoot setting in the httpd.conf file moves the home directory to a new location brings up a "readonly" error in vi. The only way around this is to make the editable is to do it with increased permissions. First off I tried to do a "su root" command, only to find that this is disabled by default on my version of OSX. However, you can use the "sudo vi httpd.conf" command to open the config file with read/write access.

5) Using XCode I create a new index.html file with some text and placed it in the required directory.

6) Start/Stop the webservice and browse to the IPAddress of your server and you won't get the correct file displayed, No, for some reason you get the default Apache not configured message. After much head scratching I discovered that it will work fine if you use Local host (i.e. 127.0.0.1) as the IP address in the browser. Why? Who knows, guess its something to do with the configuration in the .conf file. However accessing the file from another browser on the LAN worked fine!

More next time ....

Friday, August 1, 2008

Connecting BDC to SQL WebService

Recently I've been experimenting with the Business Data Connector (BDC) technology available within MOSS. Resources on the Net are generally supportive and I've found that in general it works most of the time. Connecting to Database tables is well supported and although limited, connecting to WebServices is also possible. I have however come up with a limitation that I was unable to get around, that is connecting BDC to a SQL WebService or WCF Service as opposed to a standard ASPX.

Full WCF is simply not supported, however using the basicHTTP protocol can be accomplished with a little bit of text editing of the XML. I was however surprised that the SQL WebSerice caused such an issue for the BDC Editor.

Creating the WebService within SQL is a simple matter of running the following SQL against the NorthWind database:

IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[GetProductsProc]') AND type in (N'P', N'PC'))
DROP PROCEDURE [dbo].[GetProductsProc]
GO
CREATE PROC dbo.GetProductsProc
AS
SELECT
ProductID,
ProductName,
UnitPrice
FROM
Products
GO

DROP ENDPOINT GetProducts
GO

CREATE ENDPOINT GetProducts
STATE = STARTED
AS HTTP
(
PATH = '/Store',
AUTHENTICATION = (INTEGRATED),
PORTS = (CLEAR),
CLEAR_PORT = 8045,
SITE = '*'
)
FOR SOAP
(
WEBMETHOD 'ProductsList'
(NAME='Northwind.dbo.GetProductsProc'),
BATCHES = DISABLED,
WSDL = DEFAULT,
LOGIN_TYPE = WINDOWS,
DATABASE = 'Northwind',
NAMESPACE = 'http://Northwind/Store'
)
GO


This provides an endpoint on the URL HTTP://localhost:8045/Store?wsdl.

Opening up the BCD Editor (available in the SharePoint SDK) and connecting to the WebService by clicking Add LOB System, Connect to WebService and type in the URL to your SQL WebService.





Click Add WebMethod from the right toolbar and drag the "ProductList" method on to the canvans and click OK. Choose the standard 'WebServiceLobSystem' System Name.






At this point we would create a Finder Instance within the /Entities/Entity0/Method/ProductList/Instances node and click Execute.








However unlike a standard webservice we get the error: AdapterObject is not of RootTypeDescriptor Type. Parameter name: adapterObject.















Following a long investigation I was unable to find a resolution to this issue. It is however all related to the fact that SQL is publishing the return values as an array of System.Objects and not an array of System.String. I'm hoping that the next version of the BCD Editor will work correctly.

Full Microsoft documentation for the BDC is available from :http://msdn.microsoft.com/en-us/library/ms563661.aspx

Wednesday, July 30, 2008

BDC to query a WebService using LINQ

In this post I'll describe how to connect your BDC definition to an external WebService. I'm assuming that you are using Visual Studio 2008, a SQL 2005 database hosting the Northwind database on the local server, MOSS 2007 and you've got the SharePoint SDK installed. Also this is a quick demo so I'll be connecting to debug instance of the webservice and not creating a permanent site as that's not really the point of this blog.

Stage 1: Building the WebService



Open Visual Studio 2008 and create a new WebSite Project of type "ASP.NET Web Service" called "Store". It should be noted at this stage that WCF is not fully supported in BDC although basicHTTP protocol is allowed. This will create a standard Service.asmx file with a default HelloWorld method.


Add a LINQ connection to your database using "Tools/Connect to Database" setting the server name to "(local)", Log on to the server as "Use Windows Authentication" and Database Name as "NORTHWIND". Test that the connection is working fine and Click OK.


Within your project Visual Studio, right click the Project file and select "Add New Item" and select a "Linq to SQL Classes" called Products.dbml. If you get the message regarding placing the file within the APP_Code folder, click Yes.


Within Visual Studio drag the "Product" table from the ServerExplorer to the Products design window. This should display a list of all fields in the table. This is DBML file contains all the code needed to connect to the database and query the table. Save the file and close the designer window.


Now we need to create a simple structure to pass this information back over the HTTP protocol. We do this by building a class containing only those fields we want to pass. Within your project Visual Studio, right click the Project file and select "Add New Item" and select a "Class" called ProductProxy.cs. If you get the message regarding placing the file within the APP_Code folder, click Yes. Replace the default code with the following:

using System;
using System.Data;
using System.Configuration;
using System.Linq;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.HtmlControls;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Xml.Linq;

/// <summary>
/// Summary description for Product
/// </summary>
public class ProductProxy
{
private int productIDField;
private string productNameField;
private decimal unitPriceField;
private double unitSalesPriceField;
private int unitsInStockField;

/// <remarks/>
public int ProductID {
get {
return this.productIDField;
}
set {
this.productIDField = value;
}
}

/// <remarks/>
public string ProductName {
get {
return this.productNameField;
}
set {
this.productNameField = value;
}
}
/// <remarks/>
public decimal UnitPrice
{
get
{
return this.unitPriceField;
}
set
{
this.unitPriceField = value;
}
}

/// <remarks/>
public int UnitsInStock
{
get
{
return this.unitsInStockField;
}
set
{
this.unitsInStockField = value;
}
}

/// <remarks/>
public double UnitSalePrice
{
get
{
return this.unitSalesPriceField;
}
set
{
this.unitSalesPriceField = value;
}
}

}

Now we create the web method that connects everything together. Open the Service.cs file and replace the existing code with the following:

using System;
using System.Linq;
using System.Web;
using System.Web.Services;
using System.Web.Services.Protocols;
using System.Xml.Linq;

[WebService(Namespace = "http://tempuri.org/")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
public class Service : System.Web.Services.WebService
{
public Service () {}

[WebMethod]
public ProductProxy[] ProductList()
{
ProductDataContext db = new ProductDataContext();
var query = from p in db.Products
select p;

ProductProxy[] prods = new ProductProxy[query.Count()];
int record = 0;

foreach (var q in query)
{
ProductProxy prod = new ProductProxy();
prod.ProductID = q.ProductID;
prod.ProductName = q.ProductName;
prod.UnitPrice = q.UnitPrice.Value;
prod.UnitsInStock = q.UnitsInStock.Value;
prod.UnitSalePrice = (double)q.UnitPrice.Value * 1.10;
prods[record] = prod;
record++ ;
}

return prods;
}

}

At this point we test the WebMethod by running the application in Visual Studio 2008 debug mode. Choose Debug/Start Debugging, if you get a message about modifying the web.config to allow debugging select "OK" or that ScriptDebugging in InternetExporer select "Yes". Once the HTML page appears, click the Service.asmx link, note the URL displayed as we'll use this later, then click the ProductList link and then the "Invoke" button. The results of the query should be displayed on screen in XML format.



Stage 2: Developing the BDC Mapping


Start the Business Data Catalog Definition Editor which is available when you install the SharePoint SDK. From the application tool bar click the "Add LOB System" then click the "Connect to Webservice" and type the URL of the debug instance noted previously (in my case it was "http://localhost:3534/Store/Service.asmx", however your port number could be different. Clicking Connect should present you with a blank deisng surface with a toolbar on the right hand side of the screen. From this toolbar click "Add Web Method" and drag the "ProductList" method onto the design surface. Finally click the OK button and choose the standard "WebServiceLobSystem" as the name and click OK.



We need to create a Method Instance in order to test that or generated mapping is operating correctly. Browse to the Entities/Entity0/Methods/ProductList/Instances node and click "Add Method Instance" on the toolbar. Choose "Finder" as the Method Instance Type and click OK. While the MethodInstance0 is selected choose "Execute" from the tool bar and click the "Execute" button from the dialogue box displayed, should show a set of results. Then close the window.


Select the top node "WebServiceLobSystem" on teh left hand side of the screen. Click "Export" from the tool bar and save the XML file to your desktop as "BDC_WS_Example".

Stage 3: Importing to SharePoint


Open SharePoint 3.0 Central Administration from the Administrative Tools folder on your sever. Browse to the Share Services Administration window and select the SharedServices that runs your site, in my case it was the default SharedService1. Then select the "Import application definition" link.

Click "Browse" and navigate to the BDC xml file we created in the previous stage and click "Import". All going well you should be told that the application was imported successfully. You will see a warning message saying "No method instance of type SpecificFinder defined for for application 'WebServiceLobSystem', entity 'Entity0'. Profile page creation skipped", which is just saying that we have not defined a method, but for it will not effect this demo. Just click OK and you should see the application details shown on screen. You are now ready to implement the BDC object inside your SharePoint site.

Browse to your SharePoint website, in my case http://sharepoint1/Pages/Default.aspx, and create a new "Blank Web Part Page" called Products. Add a new "Business Data List" webpart In any of holding areas on the page.


Open the tool pane by clicking on the link within the new webpart. Browse to the Type you wish by clicking the book icon shown. Select the "WebServiceLobSystem_Instance" which is the name provided in the previous stage and click OK. Then click OK on the SharePoint WebPart tool bar shown on the right of teh screen.


At this point the details can be seen on screen.


Now you have a working demo feel free to play around with the various settings and options within SharePoint and the BDC Editor.

The End.

Sunday, June 15, 2008

Ghostdoc

Had a very nice developer tell me about an automated documention tool called Ghost Doc. Being a sceptical sole I doubted it would live up to the hype, however after a few minutes I was hooked.

Easy to download and install, assign a quick key and away you go. It defaults to D, which I kept .. yes I was that sceptical !

Roland Weigelt's G h o s t D o c (http://www.roland-weigelt.de/ghostdoc/) gets five gold stars from me.

Thursday, April 24, 2008

Error with SharePoint BDC

I recently got the error :
"Application definition import failed. The following error occurred: The IdentifierName Attribute of the TypeDescriptor named 'FirstName' of the Parameter named '@FirstName' of the Method named 'GetCustomers' of the Entity named 'Customers' does not match any of the defined Identifiers for the Entity. Error was encountered at or just before Line: '57' and Position: '16'."
When trying to import a BDC schema into MOSS 2007.

It turned out that I'd forgotten to add the following line to the <Identifiers>
<Identifier Name="FirstName" TypeName="System.String" />

Following that fix I get the next error:
"Application definition import failed. The following error occurred: The AssociatedFilter Attribute of the TypeDescriptor named 'FirstdName' of the Parameter named '@FirstName' of the Method named 'GetCustomers' of the Entity named 'Customers' does not match any of the defined FilterDescriptors for the Method. Error was encountered at or just before Line: '57' and Position: '16'."

So I added the lines to the <FilterDescriptors> tag:
<FilterDescriptor Type="Wildcard" Name="FirstName" >
<Properties>
<Property Name="UsedForDisambiguation" Type="System.Boolean">true</Property>
</Properties>
</FilterDescriptor>

Following that, I got another error after successful installation of the schema into SharePoint when the User attempted to run the webpart. "The Business Data Catalog is configured incorrectly. Administrators, see the server log for more information.". That error is found within the Servers Event Log:
"A Metadata Exception was constructed in App Domain '/LM/W3SVC/1959965621/Root-2-128667808042048704'. The full exception text is: Could not find appropriate places on the root TypeDescriptor to read sub-identifier values for the Instance of Entity 'Customers'"

The fix for this was because I forgot to add the "IdentifierName="FirstName" to the TypeDescriptor tag.
<TypeDescriptor TypeName="System.String" IdentifierName="FirstName" Name="FirstName" >
<LocalizedDisplayNames>
<LocalizedDisplayName LCID="1033">FirstName</LocalizedDisplayName>
</LocalizedDisplayNames>
<Properties>
<Property Name="DisplayByDefault" Type="System.Boolean">true</Property>
</Properties>
</TypeDescriptor>

So the rule of the game when using the SharePoint BDC is watch your XML when adding new fields to a query..