Dale Preston's Web Log
  
Wednesday, January 18, 2006
 

Returning Custom Classes from a Web Service part III


Returning custom classes from a web service, the series:

Part 1 — A public data-only class on the web service

Part 2 — A shared class library

Part 3 — Advanced methods

Part 4 — .Net 2.0 Update

I know it has been a year since I started this series but then Stephen King went over 10 years between volumes of the Dark Tower series and people still read it. Ok, I'm no Stephen King but I do think this series is worth continuing.

In this article I want to talk about some more advanced techniques for passing objects to and from web services, specifically:

Let's start with data-only classes:

Data-only classes are the back bone of passing objects to and from web services. When you pass a custom class to or from a web service, it has to be a data only class - it is not possible to pass operations or methods via XML. Even if the class you're passing has operations in it, only the public data members (properties and fields) will be passed.

Using data-only classes was demonstrated in the first article in this series. In the code below, copied from that article, the web service client creates a NewsItem instance using the class definition in the NewsService web service:

// Create the connection to the web service proxy

NewsService news = new NewsService();
// Set security for the web service - may not be required in all environments
news.Credentials = System.Net.CredentialCache.DefaultCredentials;
// Create the news item
NewsService.NewsItem item = news.GetItem();
// Display something from the NewsItem to prove we got the item returned
Response.Write("News item summary: "
+ item.NewsSummary
+ "<BR><BR>");


In the simplest of client applications, this works and works quite well. If you are only passing data back and forth and you don't have to extend the data class on the client by adding additional properties or operations, this could be all you need to do to share classes between the web service and the client. For simple data sharing, there are only three caveats that I have had to deal with in using this solution (your own mileage may vary):
  1. The shared class has to be on the web service side, not on the client. This is because the web service has no knowledge of the client but, since the client has a web reference to the web service, the client is aware of the capabilities of the web service.

  2. The shared class must be used in the signature of a WebMethod (as a return type or as a parameter) in order for the Microsoft tools to automatically expose the class in the WSDL.

  3. The caveat most likely to be a show stopper in a situation where this simple model would otherwise work is that you cannot by default, use the class for data binding at the client. There are third party tools and VS2003 add-ins that will create proper proxy classes but the tool shipped with VS2003,used either manually or by adding/updating web references in visual studio, is WSDL.exe. WSDL.exe creates a proxy that represents all public properties of the web service class as public fields in the proxy. Databinding in ASP.Net will not work with fields; it must be done with properties. You can manually edit the proxy class, refactoring the public fields as public properties by encapsulating the fields in properties. Just remember that any time you update the web reference, you will have to perform the edit again!


Enhancing the shared data-only class

There are two primary limitations of using data-only classes for passing objects between web services and clients:
  1. They're data-only. Classes represent objects and objects typically have behaviors or operations implemented as methods.
  2. The Visual Studio tools generate proxies that cannot be used for data binding because properties are converted to fields.

I will demonstrate four solutions to these limitations: delegation, wrapper classes, inheritance, and conversion. Each of these solve one or both of these problems to varying degrees in the appropriate circumstances. Most of these solutions eliminate the need for manually editing the proxy class file, Reference.cs, generated by Visual Studio. All of the solutions I have presented in this series, including those discussed in the first two articles and the simple data-only solution above, become the best solution when faced with the set of conditions best suited to them. You'll want them all in your toolkit.

Delegation

You can add functionality to the web service class, NewsService.NewsItem, by creating a delegate class to handle the NewsItemData classes properties and then add the methods you need to the delegate class:
// A NewsItem to return and display
public class NewsItemDelegate
{
private NewsService.NewsItem item;
private string region;

private NewsItemDelgate()
{
}

public NewsItemDelegate(NewsService.NewsItem item)
{
this.item = item;
}

public NewsService.NewsItem Item
{
get{ return item; }
set{ item = value; }
}

public string Region
{
get{ return region; }
set{ region = value; }
}

public string FormatToHtmlString()
{
return "<B>" + item.NewsSummary + "</B>";
}

}


To call the NewsItem through the delegate, we might do this:
// A NewsItem to return and display
private NewsItemDelegate nid = new NewsItemDelegate(item);
Response.Write(nid.Item.NewsDate + "<BR>");
Response.Write(nid.FormatToHtmlString());


In this example, I've added functionality, overcoming the data-only obstacle by adding the method FormatToHtmlString(). I created a delegate for the NewsService.NewsItem class but haven't overcome the databinding problem. The Item property of the NewsItemDelegate class still has public fields instead of public properties. Where the NewsItem is not used in data binding, this is a very workable, and easy to code solution. Just like the data-only class solution, I can manually edit the proxy to encapsulate the fields with properties and overcome the databinding obstacle.

Wrapper

There's an easy solution to the problem with delegation that prevents us from using it for data binding: we turn our delegate class into a wrapper class, providing properties to encapsulate the Item property's public fields. A partial list of the properties we would add to the NewsItemDelegate class are shown below:
public string NewsTitle
{
get{ return item.NewsTitle; }
set{ item.NewsTitle = value; }
}

public string NewsDate
{
get{ return item.NewsDate; }
set{ item.NewsDate = value; }
}

public string PicName
{
get{ return item.PicName; }
set{ item.PicName = value; }
}


There's one problem with this model. It works great in your code behind classes but it won't work in declarative code in your aspx pages. I don't know why it is but the DataBinder will not navigate the class structure to return the proper value. There may be a way to overcome this flaw but I quit looking pretty quickly and moved on to inheritance.

Inheritance

One way to overcome the data-only obstacle is to simply create a new class extending the web service class:
public class ClientNewsItem : NewsService.NewsItem


The ClientNewsItem class doesn't immediately solve the data binding issue because all of the public fields of the base class, NewsService.NewsItem, are still public fields in the inherited class. You can now encapsulate those public fields and the data binding problem is solved without having to edit the web service proxy, Reference.cs, again.

ClientNewsItem also solves the data-only limitation. You can add any methods that you need to add to the ClientNewsItem class, allowing you to perform operations on the object.

According to Liskov's substitution principle, a subclass, ClientNewsItem, can always be passed to a method expecting the base class, NewsService.NewsItem. If you have a web method that expects a NewsService.NewsItem, such as:
[WebMethod]
public Void SaveNewsItem(NewsItem item)
{
// Code here to save a NewsItem
}


We can call that method, passing a ClientNewsItem without a problem:
// Create the connection to the web service proxy
NewsService news = new NewsService();
// Set security for the web service - may not be required in all environments
news.Credentials = System.Net.CredentialCache.DefaultCredentials;
ClientNewsItem cni = new ClientNewsItem();

// Code here to set the properties of the ClientNewsItem, then

//Save the news item
news.SaveNewsItem(cni);


We have converted the inherited ClientNewsItem back into a form that the web service, NewsService could understand.

In the example above, I created the ClientNewsItem from scratch. A more likely scenario, though, is to get the data for the ClientNewsItem from the web service, edit the property values, and send it back to be persisted in a database.

I covered how to send it back above, but how do I get the NewsItem values into our ClientNewsItem in the first place? Well, there is nothing to do to get around the need to convert from a NewsService.NewsItem to a ClientNewsItem. I discuss how to do that conversion more fully in the Conversion section below.

One thing I don't like about the inheritance solution is that, for example in our discussion case, it leaves the Reporters property/field of the ClientNewsItem defective on at least three ways:
  1. The NewsService.NewsItem.Reporters property is of type NewsService.Reporter[]. Therefore, in our inherited class, we only halfway achieved our goal of separation from the web service NewsItem class because ClientNewsItem still has properties in the NewsService namespace.

  2. The inherited Reporters property is, by necessity when coming from a web service, an array. I don't like arrays. I like collections. I want my Reporters property to be a collection, perhaps something like a ReportersCollection (not defined in our sample code). Collections inherited from System.Collections.CollectionBase have too many great features to pass up: Indexers, methods such as IList.Add, IList.Insert, IList.Contains, IList.IndexOf, and much more. Did I mention that I like collections?

  3. To solve the first two defects, you have to hide the inherited Reporters property. I don't like hiding base class members. Consumers of your class expect certain characteristics based upon the base class of your class. Altering or hiding those characteristics should not be done without good reason.


Conversions

What is the goal of passing custom classes between web services and clients? If we remember that the goal is to get work done and not to protect the sanctity of some brilliant piece of code, then this section will really make sense. The advantage to passing a class back and forth between client and web service is not the satisfaction of knowing our thoughtfully designed class is flowing around on some wire. The advantage is programming simplicity. So here's a way we can achieve that programming simplicity, sharing the class between the web service and client, without sacrificing functionality and without having to remember to manually edit the proxy file

I will create three classes. The public data-only NewsItemData class on the web service that has only private fields and public properties to encapsulate those fields.

The client has an internal NewsItem class that has properties matching all of the properties of the data-only class and may extend that list for functionality that applies only to the client. Perhaps the client NewsItem has a Display property to indicate whether or not the item should be presented in the user interface based on some filter selection by the user. The client NewsItem class may have some methods as well, such as the FormatToHtmlString method.

The NewsService web service has an internal NewsItem class that, again, has properties matching all of the properties of the data-only class. It may add its own properties to the list, such as ExpirationDate to describe a date after which the news item will not be returned in results to the client. The web service NewsItem class may add some behavior, too. It may have a Save method that persists the properties of the NewsItem to your SQL Server database.

Let's look at a typical scenario. The user wants to see a news story and clicks a link on a web page to request the story. The front-end server web application needs to retrieve the NewsItem from the web service and then display the NewsItem on the web page. The front-end server is acting as the client of the web service and requests a NewsItemData object for the news story the user requested.

The data access layer on the web service retrieves the news story from the SQL Server database in a DataReader, creates a new NewsService.NewsItem and assigns the property values from the data in the DataReader. This is the start of our process. We have a NewsService.NewsItem object and we want to get that object back to the user's web page.

The first task we have to complete is to convert the NewsService.NewsItem into a NewsService.NewsItemData class that can be sent to the client. We have a couple options:
We can create a method ConvertNewsItemToData and call that method manually, passing the NewsService.NewsItem as a parameter. This is really not a bad solution and it is pretty common to see it:
[WebMethod]
[XmlInclude(typeof(Reporter)), XmlInclude(typeof(Person))]
public NewsItemData GetItem(int newsId)
{
// Return the NewsItemData class
return ConvertNewsItemToData(DataLayer.GetNewsItem(newsId));
}

}


I like the next solution better. We create an explicit conversion operator and then all we have to do is cast our NewsService.NewsItem to a NewsService.NewsItemData class like this:
[WebMethod]
[XmlInclude(typeof(Reporter)), XmlInclude(typeof(Person))]
public NewsItemData GetItem(int newsId)
{
// Return the NewsItemData class
return (NewsItemData)DataLayer.GetNewsItem(newsId);
}


That is clearer than the first alternative.


If I see the first example above, I am going to right click ConvertNewsItemToData and select Goto Definition to see exactly what it does. When I see the last example, I know instantly what it does. As my article on conversion operators says, the code to do an explicit or implicit cast is almost identical to the code to do a conversion. Given the greater simplicity and easier to understand code by using the cast, I always suggest the cast operator over a conversion method.

Now we have to backtrack to the front end server that requested the NewsItemData in the first place. In the handler for the LinkButton that the user clicked we find the following code:
// Create the connection to the web service proxy
NewsService news = new NewsService();
// Set security for the web service - may not be required in all environments
news.Credentials = System.Net.CredentialCache.DefaultCredentials;
// For simplicity, I hard coded our only news item NewsId value of 1.
this.NewsItem item = (this.NewsItem)news.GetItem(1);

// Display the item using the FormatToHtmlString method.
ItemLabel.Text = item.FormatToHtmlString();


Notice that we never have to handle the intermediate NewsService.NewsItemData class except to cast to it on the web service side. Once we created the explicit conversions we totally forgot about them. We just do what we do routinely in everything else we do: cast from one type to another similar type. It's so simple we don't even have to think about it.

To pass the client's NewsItem back to the web service is as easy as reversing the process, assuming that you created the conversion operator overrides in both directions:
// Create the connection to the web service proxy
NewsService news = new NewsService();
// Set security for the web service - may not be required in all environments
news.Credentials = System.Net.CredentialCache.DefaultCredentials;

// Save the NewsItem.
bool result = news.SaveItem((NewsService.NewsItemData)item);


In all of this code for conversions, I have used explicit casts so it would be easier to see when and where the casts were being performed. For the ultimate in simplicity, override the implicit operator instead. Then
this.NewsItem item = (this.NewsItem)news.GetItem(1);

becomes
this.NewsItem item = news.GetItem(1);

even though newsGetItem returns a NewsService.NewsItem and
bool result = news.SaveItem((NewsService.NewsItemData)item);

becomes
bool result = news.SaveItem(item);

even though news.SaveItem expects a NewsService.NewsItem and item is a NewsClient.NewsItem. It just doesn't get any simpler than this.

Summary

My favorite method of passing objects between a web service and client is the conversion method. In my own experience I have not run into any circumstance where it does not work. If it has any drawbacks at all, it is that it takes just a little more coding up front to create the conversion methods or conversion operators but once that is done, you forget it.

All of the other methods I have described in this article and the two earlier articles in this series work well in the right circumstances. If all you need to do is display some properties of a server side object, why create conversion methods? Just follow the steps outlined in the first article to instantiate the server defined object locally on the client. In between that ultimate simplicity and the ultimate functionality of the conversions method exist a whole range of circumstances in which the other methods for sharing objects work just as well as the two extremes.

Download the updated code for this series.

Comments: Post a Comment

<< Home

Powered by Blogger