Dale Preston's Web Log
  
Thursday, February 24, 2005
 

Passing Classes from WebServices Part II


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

After a couple false starts, here is my sample of how to share a class between a web service and the web service's client. In my previous article on the topic I demonstrated defining a class in the web service and creating an instance of that class on the client by pointing to the web service.

Key points to remember about that model are that:
  • Only public fields (which should almost never exist) and public properties are available to the client.
  • The ArrayList defined in the class was converted to an array of objects at the client.
The second point above is important in the model I am describing today for reasons that will be more clear later. For now, suffice it to say that, for this demonstration, I have replaced the ArrayList with an array of Reporter objects. I could have come up with a way to use the ArrayList internally and use the array of Reporter objects as the public property exposing the ArrayList contents but that functionality would confuse this description and be outside the scope of this article. See my first article in this series for a simple example of passing the contents of an ArrayList from a web service to a client.

There are a couple other differences from the first article that were required to share the classes including methods, between the web service and its client:
  • The NewsItem class was moved to a separate class library rather than being included inside the NewsService web service class. This allows us to set a reference to the NewsItem class from both the web service and the web form client. For simplicity, I included the Person and Reporter classes within the NewsItem class library.
  • I have added a couple methods to demonstrate that the methods are available at the web service and at the client.
To compile the examples below, make sure that you set a reference to the NewsService class library in both the web service and in the ASP.Net web application that is the client to the web service.

In the web form class, WSReturnDemo, I created a call to the web service that returns a NewsItem object, item. In the first article, the item was an instance of the NewsItem class from the web service. In this case, item is an instance of the NewsService class defined in the NewsService class library referenced locally.

If you compile the project at this point, you get the infamous Cannot implicitly convert type 'WebServiceReturnTypeDemo.NewsServiceWS.NewsItem' to 'NewsService.NewsItem' exception because the NewsServiceWS.NewsService.NewsItem, while identical to it, is not the same item as the WSReturnDemo.NewsService.NewsItem. We have to do a little tweaking in the web reference Resource.cs file.

In Resource.cs there is a proxy class NewsServiceWS as well as proxy class definitions for Person, Reporter, and NewsItem classes. In the NewsServiceWS class, there are four references to NewsItem. Each reference must be changed to NewsService.NewsItem so the compiler will know to refer to the local reference rather than to the web service reference.

If you compile again, you will get another error, Types NewsService.Reporter and WebServiceReturnTypeDemo.NewsServiceWS.Reporter both use the XML type name, Reporter, from namespace http://DalePreston.com/NewsServiceDemo/. Use XML attributes to specify a unique XML name and/or namespace for the type.

(September 3, 2005)This is an updated solution to the above problem. My original solution was in error and a question by a blog reader, Andrei Matei, led me to a better solution. My original solution was to just change the XML namespace declaration in the auto-generated file, reference.cs. This question from Andrei:
Why isn't this the case for the NewsItem class? The compiler doesn't complain about that, even though you haven't modified the XMLnamespace for it.
made me look closer at the solution and I came up with this solution:
Your question raises a good point that I had overlooked. The answer is that we have redirected all references to NewsItem to the local copy of the NewsService library but are, mistakenly, still getting the Person and Reporter classes from the web service. We need to get them from the local copy of the NewsService library as well.

The proper solution is to just remove the following attribute declaration from the proxy class:

[System.Xml.Serialization.XmlIncludeAttribute(typeof(Person))]

With that removed, you could also delete the Reporter, NewsItem, and Person classes from the reference.cs file. They are no longer referenced in the client.

With these changes to the web reference to the NewsServiceWS proxy on the ASP.Net web form application, we have completed the tasks necessary to use common class definitions, including public fields, members, and methods. The last point to remember is:
  • If you update the web reference using the functionality built into Visual Studio 2003, you will have to repeat the modifications to the Reference.cs file. The file will have been re-created by the update process!
So there you have it. We have created a web service and client that pass a custom class between them using local copies of the class definition so that the same set of methods can be applied to the objects on either side of the wire. The class instance is passed as a value and not a reference (do not confuse this with how it is stored in memory, on the stack or on the heap, on either side of the wire) so changes made locally on either side are not seen on the other side unless the item is passed back.

Remember that, at the start of this article, I said I removed the ArrayList for the Reporters collection in deference to the Reporters[] array? Now the reason why should be more easily understood.

In the first article, the web service consumer only knew of the Reporters collections by virtue of the Web service proxy as defined in Reference.cs. When the web service returned the ArrayList as an array of objects, and the web service proxy defined Reporters as an array of objects, it all worked well.

If, in the application as described in this article, the web service returned the ArrayList as an array of objects, the compiler would object because now the compiler on the web page client knows that Reporters is supposed to be an ArrayList. If we offer an array of objects instead, we get an error.

The only solution, in this model, is to define all public fields and properties as XML serializable types or, if you really have to do it, to write custom serializers for your classes. Do I sense another article coming on?


You can download the code for this series or see it here. Remember that, if you download it, you'll have to re-create some of the project information to work on your PC.

First the NewsService class library:

using System;
using System.Collections;

namespace NewsService
{
// A Reporter is a Person who works for a news Agency.
public class Reporter : Person
{
private string agency;

public string Agency
{
get { return agency; }
set { agency = value; }
}
}


// A base Person class to describe all humans.
public class Person
{
private string name;
private int age;
private string city;


public string Name
{
get { return name; }
set { name = value; }
}

public int Age
{
get { return age; }
set { age = value; }
}

public string City
{
get { return city; }
set { city = value; }
}

// A method created just to demonstrate that the methods
// are available at the WebService and at the client.
public string PersonInfo()
{
return Name + ", " + Age + ", " + City;
}
}

// A NewsItem to return and display
public class NewsItem
{
private int _NewsId;
private string _NewsTitle;
private string _NewsDate;
private string _PicName;
private string _NewsSummary;
private Reporter[] reporters;

public int NewsId
{
get{return _NewsId;}
set{_NewsId=value;}
}

public string NewsTitle
{
get{return _NewsTitle;}
set{_NewsTitle=value;}
}

public string NewsDate
{
get{return _NewsDate;}
set{_NewsDate = value;}
}
public string PicName
{
get{return _PicName;}
set{_PicName = value;}
}
public string NewsSummary
{
get{return _NewsSummary;}
set{_NewsSummary = value;}
}

// An array of Reporters that worked on this NewsItem
public Reporter[] Reporters
{
get { return reporters; }
set { reporters = value; }
}

// A method to add to the Reporters array
public void AddReporter(Reporter reporter)
{
int newLength = 0;
Reporter[] newList;
if (reporters != null)
{
newLength = reporters.Length;
newList = new Reporter[newLength + 1];
reporters.CopyTo(newList, 0);
}
else
{
newList = new Reporter[1];
}
newList[newList.GetUpperBound(0)] = reporter;
reporters = newList;
}
}
}




And now the NewsServiceWS WebService:



using System;
using System.Collections;
using System.ComponentModel;
using System.Data;
using System.Diagnostics;
using System.Web;
using System.Web.Services;
using NewsService;

// included to add the XmlInclude attribute
using System.Xml.Serialization;

namespace NewsServiceDemo
{
/// <summary>
/// Demonstrates returning custom classes from a web service
/// </summary>
[WebService(Namespace="http://DalePreston.com/NewsServiceDemo/",
Description="Demonstrates passing custom classes from web services.")]
public class NewsServiceWS : System.Web.Services.WebService
{
public NewsServiceWS()
{
//CODEGEN: This call is required by the ASP.NET Web Services Designer
InitializeComponent();
}

#region Component Designer generated code

//Required by the Web Services Designer
private IContainer components = null;

/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
}

/// <summary>
/// Clean up any resources being used.
/// </summary>
protected override void Dispose( bool disposing )
{
if(disposing && components != null)
{
components.Dispose();
}
base.Dispose(disposing);
}

#endregion


// Without the XmlInclude attributes, the XmlSerializer would not be
// able to serialize the Reporters ArrayList and it would return null
// for the Reporters property.
[WebMethod]
[XmlInclude(typeof(Reporter)), XmlInclude(typeof(Person))]
public NewsItem GetItem()
{
// Create the NewsItem to return
NewsItem item = new NewsItem();

// Populate the properties with some sample data. This could
// easily be done by getting the values from a database
item.NewsDate = DateTime.Now.ToShortDateString();
item.NewsId = 1;
item.NewsSummary = "No one was killed today.";
item.NewsTitle = "Special Report!";
item.PicName = "NewBornBaby.jpg";

// Create two Reporter objects to store in the Reporters ArrayList
// The first Reporter
Reporter reporter = new Reporter();
reporter.Age = 49;
reporter.Name = "Dale";
reporter.City = "Park Hill";
reporter.Agency = "UPI";
item.AddReporter(reporter);

// The second Reporter
reporter = new Reporter();
reporter.Age = 50;
reporter.Name = "Jim";
reporter.City = "Tulsa";
reporter.Agency = "CNN";
item.AddReporter(reporter);

// Return the NewsItem class
return item;
}
}
}




And the WebForm code:



using System;
using System.Collections;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Web;
using System.Web.SessionState;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.HtmlControls;

namespace WebServiceReturnTypeDemo
{
/// <summary>
/// Demonstrates returning custom classes from a web service.
/// </summary>
public class WSReturnDemo : System.Web.UI.Page
{
protected System.Web.UI.WebControls.Literal Literal1;

private void Page_Load(object sender, System.EventArgs e)
{
// Create the connection to the web service proxy
WebServiceReturnTypeDemo.NewsServiceWS.NewsServiceWS news =
new WebServiceReturnTypeDemo.NewsServiceWS.NewsServiceWS();
// 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>");

// Create a new Reporter object to demonstrate
// using the NewsItem.AddReporter() method
NewsService.Reporter newReporter = new NewsService.Reporter();
newReporter.Age = 85;
newReporter.Agency = "AP";
newReporter.City = "Seattle";
newReporter.Name = "John";

// Prove that we have shared methods between the
// WebService and the service consumer by calling
// the NewsService.AddReporter method.
item.AddReporter(newReporter);

// Loop through the Reporters array. The original ArrayList from the
// web service is translated into an object[] by the XML Serializer but we
// did get the values!!!
for(int count = 0; count < item.Reporters.Length; count++)
{
NewsService.Reporter reporter =
(NewsService.Reporter)item.Reporters[count];
Response.Write("Reporter "
+ count.ToString()
+ " name: "
+ reporter.Name
+ "<BR>");
Response.Write("News agency: "
+ reporter.Agency + "<BR>");

// Prove that we can access even methods of base classes.
Response.Write("Results of Person.PersonInfo() method: "
+ reporter.PersonInfo()
+ "<BR><BR>");
}
}

#region Web Form Designer generated code
override protected void OnInit(EventArgs e)
{
//
// CODEGEN: This call is required by the ASP.NET Web Form Designer.
//
InitializeComponent();
base.OnInit(e);
}

/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
this.Load += new System.EventHandler(this.Page_Load);

}
#endregion

}
}




And finally, the modified Reference.cs file for the NewsServiceWS reference in the ASP.Net WebForm:



//------------------------------------------------------------------------------
// <autogenerated>
// This code was generated by a tool.
// Runtime Version: 1.1.4322.573
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </autogenerated>
//------------------------------------------------------------------------------

//
// This source code was auto-generated by Microsoft.VSDesigner, Version 1.1.4322.573.
//
namespace WebServiceReturnTypeDemo.NewsServiceWS {
using System.Diagnostics;
using System.Xml.Serialization;
using System;
using System.Web.Services.Protocols;
using System.ComponentModel;
using System.Web.Services;


/// <remarks/>
[System.Diagnostics.DebuggerStepThroughAttribute()]
[System.ComponentModel.DesignerCategoryAttribute("code")]
[System.Web.Services.WebServiceBindingAttribute(Name="NewsServiceWSSoap", Namespace="http://DalePreston.com/NewsServiceDemo/")]
[System.Xml.Serialization.XmlIncludeAttribute(typeof(Person))]
public class NewsServiceWS : System.Web.Services.Protocols.SoapHttpClientProtocol {

/// <remarks/>
public NewsServiceWS() {
this.Url = "http://localhost/NewsServiceDemo/NewsServiceWS.asmx";
}

/// <remarks/>
[System.Web.Services.Protocols.SoapDocumentMethodAttribute("http://DalePreston.com/NewsServiceDemo/GetItem", RequestNamespace="http://DalePreston.com/NewsServiceDemo/", ResponseNamespace="http://DalePreston.com/NewsServiceDemo/", Use=System.Web.Services.Description.SoapBindingUse.Literal, ParameterStyle=System.Web.Services.Protocols.SoapParameterStyle.Wrapped)]
public NewsService.NewsItem GetItem() {
object[] results = this.Invoke("GetItem", new object[0]);
return ((NewsService.NewsItem)(results[0]));
}

/// <remarks/>
public System.IAsyncResult BeginGetItem(System.AsyncCallback callback, object asyncState) {
return this.BeginInvoke("GetItem", new object[0], callback, asyncState);
}

/// <remarks/>
public NewsService.NewsItem EndGetItem(System.IAsyncResult asyncResult) {
object[] results = this.EndInvoke(asyncResult);
return ((NewsService.NewsItem)(results[0]));
}
}

/// <remarks/>
[System.Xml.Serialization.XmlTypeAttribute(Namespace="http://DalePreston.com/NewsServiceDemo/")]
public class NewsItem {

/// <remarks/>
public int NewsId;

/// <remarks/>
public string NewsTitle;

/// <remarks/>
public string NewsDate;

/// <remarks/>
public string PicName;

/// <remarks/>
public string NewsSummary;

/// <remarks/>
public Reporter[] Reporters;
}

/// <remarks/>
[System.Xml.Serialization.XmlTypeAttribute(Namespace="http://DalePreston1.com/NewsServiceDemo/")]
public class Reporter : Person {

/// <remarks/>
public string Agency;
}

/// <remarks/>
[System.Xml.Serialization.XmlTypeAttribute(Namespace="http://DalePreston1.com/NewsServiceDemo/")]
[System.Xml.Serialization.XmlIncludeAttribute(typeof(Reporter))]
public class Person {

/// <remarks/>
public string Name;

/// <remarks/>
public int Age;

/// <remarks/>
public string City;
}
}



Comments:
Thanks a lot Mr. Dale Preston, this article has helped me a lot..
 
Thanks a lot Mr Dale for the wonderful article. This was the first time I was creating a webservice that returns the object. And I found your article that really helped me.

The only issue that I have is that the proxy class that I am creating does not include the definition of public members of custom class. It is just an empty class. I have tried several times but unable to achive this. I have followed the steps that you have mentioned in your article.
 
Cheers Dale. I've been banging my head against a brick wall with this all day.

Thank you, great informative article!

G
 
On Compiling the project WebServiceReturnTypeDemo, I am getting following error:

Error 1 Cannot implicitly convert type 'WebServiceReturnTypeDemo.NewsServiceWS.NewsItem' to 'NewsService.NewsItem' D:\Projects\InterAmerica\Downloads\WebServiceClassesII\WebServiceClassesII\WebServiceReturnTypeDemo\WSReturnDemo.aspx.cs 30 32 D:\...\WebServiceReturnTypeDemo\


Am I missing something??
 
Ashish,

The error you're getting is exactly the point of the article. Please re-read carefully, paying attention about halfway through the text portion of the article to the instructions on how to modify your reference.cs file. That is the point of this portion of the article: how to modify the reference.cs to use your local class.

I recommend that you look at the SchemaImporterExtension description in part 4, though. It is a much better solution to passing objects from a web service in .Net 2.0.

Dale
 
I'm not sure what the point of this is! You code services for a contract first approach allowing the only dependencies between service and consumer to be the contract. By doing this you completely break this tenant. Also, what happens when the "shared" class library becomes out of sync between service and some clients?!? Boom goes the dynomite!
 
Is there a way to specify which classes will be described in the WSDL document so some of them won't be in the generated proxy class.
If there are numerious custom classes in a shared library used by the web service, the process of generating the proxy takes a lot of time, and it will take even more if You have to change the namespaces of all of this classes.
 
Not a really good idea modifying references.cs I may say.
 
mvp - That's a rather broad and unsupported statement.

Why isn't it a good idea? The article discusses the risks of editing reference.cs and I have only ever done it that way in a couple unique situations but, in the end, reference.cs is just a code file like any other code file.

There are multiple ways to generate a web reference. You can let Visual Studio do it for you, the most common way, or you can create the web reference classes using WSDL.exe, or you could code the whole thing from scratch.

You could use Visual Studio's generated classes as the starting point for coding from scratch. In which case it might make perfect sense to modify reference.cs.

The only downside of doing so is that you become responsible for the code; in other words, you can't update the web reference in Visual Studio.
 
Wow, thanks Dale! I think this article spells the end of months of head-scratching.

One question, though:

In your discussion of modifying the Reference.cs file you state that

Each reference must be changed to NewsService.NewsItem so the compiler will know to refer to the local reference rather than to the web service reference.

Would it work just as well to add

using NewsService;

to the beginning of Reference.cs and then wipe out the NewsItem, Person and Reporter proxies?
 
One more thing, Matt. I will have to go along with mvp to the point that using the solution where you modify the reference.cs is probably one of the least desirable solutions. It is good to have the knowledge that you can in your toolbelt but remember that you will have to make that modification every time you update your web reference.
 
I misunderstood your post originally, Matt. You are correct, that you could remove the proxy altogether by adding the using statement to the class or page where those objects are used.
 
Hi Dale,

I'm new to web service. After i added the web reference to my web service in my web application using ASP.NET 3.5, i couldn't see the Reference.map/.cs file. How could i get the Reference.map/.cs files. I need to change it according to your tutorial in order to pass the custom object from my web service, but i couldn't find these file. Help from you will be very appreciated.

Thanks,
Chris Lian
 
You can create a web application project rather than a web site project to see reference.cs. reference.cs is created when Visual Studio runs WSDL.exe when the web reference is added or update.

For your .Net 3.5 project, use the SchemaImporterExtension described in part 4 of this series.
 
Post a Comment

<< Home

Powered by Blogger