Silverlight 2 Web Service Part II - User Defined Type

PART I 

Building a Web Service

We are now going to have a look at how you can build and call a Web Service using a user defined type (a class called Products). We will display the returned data in TextBlocks and TextBoxes when the user clicks a button, both trough binding and code. We will also check binding validation errors to handle faulty values in the text boxes. We will use a Silverlight-enabled WCF Service as the Web Service.

webservice1.gif

The Silverlight application before the user clicks the button

webservice2.gif

The Silverlight application after the user has clicked the button

webservice3.gif

The Silverlight application after the user has given a faulty value

Creating the Silverlight application

Let's start by creating a new Silverlight application that will host our control.
  1. Open Visual Studio 8 and create a new Silverlight project called WebService.
  2. Make sure that the Add a new ASP.NET Web project to the solution to host Silverlight option is selected. Make sure the Project Type dropdown is set to ASP.NET Web Application Project, then click OK.

Two projects will be added to the solution. The WebService project is the Silverlight application project and the WebService.Web project is the ASP.NET application hosting the Silverlight application. The WebService.Web project contains one .aspx and one .html page, which one you choose to use as the container is up to you; in this example however we will use the WebServiceTestPage.aspx page, so make sure it is set as the start page.

The page.axml page is the main page in the Silverlight application, and it should contain the basic framework for hosting Silverlight controls. We will later use this page to test our Web Service.

Adding the WCF Service to the Silverlight application

The first step in creating a Web Service is to add the Silverlight-enabled WCF Service to the WebService.Web project.

  1. Right click on the WebService.Web project name and select Add-New Item.
  2. Select Silverlight-enabled WCF Service in the Add-New Item dialog box.
  3. Name the Web Service wsData in the Name field and click Add.
    This will add the required references and the two Web Service files; wsData.svc and wsData.svc.cs.
    wsData.svc doesn't contain any code; it contains one line of mark-up code that tells ASP.NET where to find the code behind file for the Web Service.

    wsData.svc.cs is the code behind file for the Web Service; here you write the logic.

    webservice4.gif

Adding the Product class to the Web Service project

Before we write the method that returns a product we have to define what a product is. We do this by adding a class with the desired properties to store values.

  1. Right click on the WebService.Web project in the Solution Explorer.
  2. Select Add-Class in the context menu.
  3. Name the class Product.cs and click OK.

Open the Product.cs class and add an empty constructor and the following properties. The properties will hold the data for a product. The DataContract attribute makes the class serializable and the DataMember makes the method serializable and it also makes it visible to the Silverlight application. We will later modify the properties of the class to add more functionality.

[DataContract]
public class Product
{
  public Product() { }

  [DataMember()]
  public string Name
  {
     get;
     set;
  }

  [DataMember()]
  public string ProductNumber
  {
     get;
     set;
  }

  [DataMember()]
  public double Price
  {
     get;
     set;
  }

  [DataMember()]
  public double Size
  {
     get;
     set;
  }
 
  [DataMember()]
  public double Weight
  {
     get;
     set;
  }
}

Adding a method to return a product from the Web Service

We are now going to add a method that we can call from the client. Start by opening the wsData.svc.cs file. Notice the two attributes on the wsData class. The ServiceContract attribute states that you intend to add methods to the class that remote callers can call access as part of a service. The AspNetCompatibilityRequirements attribute indicate that the class will have access to ASP.NET platform features such as session state, file authorization and url authorization similar to an ASMX service.

Each Web Service method needs to be decorated with the OperationContract attribute when adding it to the WCF class.

Let's add a method called GetProduct that returns an instance of the Product class. Start by deleting the default DoWork method.

[OperationContract]
public Product GetProduct()
{
  Product product = new Product();
  product.ProductNumber = "1";
  product.Name = "The product description";
  product.Price = 10.25;
  product.Size = 0;
  product.Weight = 0.5;
  return product;
}

Build the solution by pressing Ctrl+Shift+B on the keyboard.

Adding the Controls

Open the Page.xaml page in the WebService project. To get the desired output we need to add three columns and eight rows to the Grid control. Add a button in the first column and first row; a TextBlock control with the text Original Values in the second column of the second row, and a TextBlock control with the text Changeable values in the third column of the second row. Add TextBlocks with the headings for each row and corresponding TextBlocks for the static values displayed by code; give the TextBlocks the following names (lblProductNumber, lblName, lblPrice, lblSize, lblWeight). Add TextBox controls to display the changeable values and give them the following names (txtName, txtPrice, txtSize, txtWeight). And the last control we will add is a TextBlock to display error messages, name it lblErrorMessage. When the user clicks the button the Product object will be returned from the Web Service method and will be displayed in the controls. Add the following xaml code to the Page.xaml page.

The Grid

Let's have a look at the changes to the grid that we need to make. First we change the width of the page to 700 to accommodate the controls. Rename the grid ProductDetails and add the BindingValidationError event. The event will be triggered when an erroneous value is typed in one of the TextBox controls, we will get back to this event later. Add a ColumnDefinitions tag to the Grid control and add three ColumnDefinition tags to it; give them the following widths: 150, 210 and *. The * means that that column will get the remaining space defined for the page. Next add a RowDefinitions tag to the Grid control and add eight RowDefinition tags with the Height property set to Auto. Auto means that the row height will depend on the height of the controls placed in the row.

<UserControl x:Class="WebService.Page"
|  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  Width="700" Height="300">
  <Grid x:Name="ProductDetails" Background="White"
     BindingValidationError="ProductDetails_BindingValidationError">
     <Grid.ColumnDefinitions>
       <ColumnDefinition Width="150"></ColumnDefinition>
       <ColumnDefinition Width="210"></ColumnDefinition>
       <ColumnDefinition Width="*"></ColumnDefinition>
     </Grid.ColumnDefinitions>
     <Grid.RowDefinitions>
       <RowDefinition Height="Auto"></RowDefinition>
       <RowDefinition Height="Auto"></RowDefinition>
       <RowDefinition Height="Auto"></RowDefinition>
       <RowDefinition Height="Auto"></RowDefinition>
       <RowDefinition Height="Auto"></RowDefinition>
       <RowDefinition Height="Auto"></RowDefinition>
       <RowDefinition Height="Auto"></RowDefinition>
       <RowDefinition Height="Auto"></RowDefinition>
                                </Grid.RowDefinitions>

The Button

We need to add a button that will trigger the call to the Web Service. Place the button in the first column of the first row. Add the button below the RowDefinitions tag in Page.xaml. Be sure to add the Click event; we will get back this event later.

</Grid.RowDefinitions>
<Button Content="Call WebService" Grid.Column="0" Grid.Row="0"
  Width="120" Height="30" Margin="5" Click="Button_Click"></Button>

The TextBlocks

We will require thirteen TextBlock controls placed in the rows and columns of the Grid control. Add the controls below the Button control in Page.xaml. The TextBlock controls with names will be filled with data when the user clicks the button.

<Button Content="Call WebService" Grid.Column="0" Grid.Row="0"
  Width="120" Height="30" Margin="5" Click="Button_Click"></Button>
<!-- Headers -->
<TextBlock Grid.Column="0" Grid.Row="2" Margin="20, 0, 0, 10">
 
Product number</TextBlock>
<
TextBlock Grid.Column="0" Grid.Row="3" Margin="20, 0"
  VerticalAlignment
="Center">Name</TextBlock>
<
TextBlock Grid.Column="0" Grid.Row="4" Margin="20, 0"
  VerticalAlignment
="Center">Price</TextBlock>
<
TextBlock Grid.Column="0" Grid.Row="5" Margin="20, 0"
  VerticalAlignment
="Center">Size</TextBlock>
<
TextBlock Grid.Column="0" Grid.Row="6" Margin="20, 0"
  VerticalAlignment
="Center">Weight</TextBlock>
<!-- Data bound through code -->
<TextBlock Name="lblProductNumber" Grid.Column="1" Grid.Row="2"
  Margin
="20,0" HorizontalAlignment="Left" VerticalAlignment="Center">
 
Product number through code</TextBlock>
<
TextBlock Name="lblName" Grid.Column="1" Grid.Row="3"
  Margin
="20, 0" HorizontalAlignment="Left" VerticalAlignment="Center">
 
Name through code</TextBlock>
<
TextBlock Name="lblPrice" Grid.Column="1" Grid.Row="4"
  Margin
="20, 0" HorizontalAlignment="Left" VerticalAlignment="Center">
 
Price through code</TextBlock>
<
TextBlock Name="lblSize" Grid.Column="1" Grid.Row="5"
  Margin
="20, 0" HorizontalAlignment="Left" VerticalAlignment="Center">
 
Size through code</TextBlock>
<
TextBlock Name="lblWeight" Grid.Column="1" Grid.Row="6"
  Margin
="20, 0" HorizontalAlignment="Left" VerticalAlignment="Center">
 
Weight through code</TextBlock>
<!-- Error message -->
<TextBlock Name="lblErrorMessage" Grid.Column="0" Grid.Row="7"
  Grid.ColumnSpan
="3" Margin="20, 20, 2, 2" Foreground="Red"
  HorizontalAlignment
="Left"></TextBlock>

The TextBoxes

When it comes to the TextBox controls there are a few things to mention. We want to bind to the data when the DataContext is set to the Product object returned from the Web Service; we can achieve this by assigning the DataContext to each of the controls, but this makes it hard to read the code, so instead we set the DataContext on the Grid and let the data propagate down to the intrinsic controls. There are three binding modes that we can use:

  • OneWay
    This mode makes it possible to read values but not to alter them in the underlying object holding the data.
     
  • TwoWay
    This mode makes it possible to read and update values in the underlying object that holds the data.
     
  • OneTime
    This mode sets the controls' value once and ignores any potential value changes to the object. This mode reduces overhead because the value will not be read more than one time.

In this example we will use the TwoWay mode because we want to be able to change the value of the underlying object. To get feedback when errors occur in the data binding, for instance if you type in an erroneous value in a TextBox control and we want to display a message to the user, we need to activate two properties that handle errors. The two properties we need to set are ValidatesOnExceptions and NotifyOnValidationError. We also want to use the TextChanged event. We will get back to these events shortly.

<!-- TwoWay binding -->
<TextBox Name="txtName" Grid.Column="2" Grid.Row="3"
  Margin
="20, 2, 2, 2" Height="Auto" Width="200"
  HorizontalAlignment
="Left" Text="{Binding Name, Mode=TwoWay,
  ValidatesOnExceptions
=true, NotifyOnValidationError=true}"
  TextChanged
="TextBox_TextChanged"></TextBox>
<
TextBox Name="txtPrice" Grid.Column="2" Grid.Row="4"
  Margin
="20, 2, 2, 2" Height="Auto" Width="70"
  HorizontalAlignment
="Left" TextAlignment="Right" Text="{Binding Price,
  Mode=TwoWay, ValidatesOnExceptions=true, NotifyOnValidationError=true}"
  TextChanged
="TextBox_TextChanged"></TextBox>
<
TextBox Name="txtSize" Grid.Column="2" Grid.Row="5"
  Margin
="20, 2, 2, 2" Height="Auto" Width="70"
  HorizontalAlignment
="Left" TextAlignment="Right" Text="{Binding Size,
  Mode
=TwoWay, ValidatesOnExceptions=true, NotifyOnValidationError=true}"
  TextChanged
="TextBox_TextChanged" ></TextBox>
<
TextBox Name="txtWeight" Grid.Column="2" Grid.Row="6"
  Margin
="20, 2, 2, 2" Height="Auto" Width="70"
  HorizontalAlignment
="Left" TextAlignment="Right" Text="{Binding Weight,
  Mode
=TwoWay, ValidatesOnExceptions=true, NotifyOnValidationError=true}"
  TextChanged
="TextBox_TextChanged" ></TextBox>

Add a reference to the Web Service

The first thing we need to do is to add a reference to the Web Service.

  1. Right click on the References folder in the WebService project.
  2. Select Add Service Reference in the context menu.
  3. Click on the Discover button. This searches the solution for available services. Alternatively you can write the address to the service in the Address field.
  4. Select the wsData service in the list of services.
  5. Name the namespace DataService in the Namespace field.
  6. Click on the OK button to crate the necessary proxy classes for the Web Service.

A proxy to the service should appear under the Service References folder in the Solution Explorer. Click on the Show All Files button in the Solution Explorer to see all the files added to the proxy. There will also be a ServiceReferences.ClientConfig file added to the project root folder. This file configures the connection to the Web Service.

The event methods

Now that we have a proxy to the Web Service we can begin to write the logic to handle the calls to the Web Service methods. The first thing we need to do is to add a couple of using statement to the Page.xaml.cs page. The following using statements are needed:

DataService: Makes it easier to access the Web Service classes.

System.Windows.Browser: To gain access to the HtmlPage class needed to get the current port.

System.ServiceModel: To gain access to the EndpointAddress and the BasicHttpBinding classes needed when creating the Web Service proxy object in code. We will override the settings in the ServiceReferences.ClientConfig file. We need to do that to prevent the Web Service from stop working when the port changes.

using WebService.DataService;
using System.Windows.Browser;
using System.ServiceModel;

Code in the Constructor

Because we use numeric values in the controls we might want to get a uniform presentation of such values, therefore we will set the current culture of the application to en-US. When I wrote the application I realized that the decimal delimiter was different when displaying values through code and when using binding. This happened because I have Swedish settings in Windows, and to counter for that and set a culture in code I use the CurrentCulture property of the Thread object.

public Page()
{
  InitializeComponent();
  System.Threading.Thread.CurrentThread.CurrentCulture = new
     System.Globalization.CultureInfo("en-US");
}

Code in the Button_Click event

Next we will create an EndpointAddress object to define where the Web Service is located. We do this to ensure that we can call the service even if the port changes. You could change this endpoint address and base the URL on the current Silverlight page so that it will work wherever you deploy it.

private void Button_Click(object sender, RoutedEventArgs e)
{
  System.ServiceModel.EndpointAddress endpoint = new
    EndpointAddress("http://localhost:" +
    HtmlPage.Document.DocumentUri.Port + "/wsData.svc");
}

Next we need to create a proxy object that will handle the call to the service. On the proxy object we will create an event handler for the GetProductCompleted event that will fire when the call returns from the service method call. It is named after the GetText method we created earlier in the Web Service class.

The default time out is 60 seconds, which might be a tad long, so we will change that to 30 seconds using the OperationTimeout property of the proxy object.

All calls to Web Services need to be done asynchronously so we will call the GetTextAsync method on the proxy object. When calling a method asynchronously we need to have a callback method present, in our case it is the proxy_GetProductCompleted method.

private void Button_Click(object sender, RoutedEventArgs e)
{
  System.ServiceModel.EndpointAddress endpoint = new
     EndpointAddress("http://localhost:" +
    HtmlPage.Document.DocumentUri.Port + "/wsData.svc");

   wsDataClient proxy = new wsDataClient(new BasicHttpBinding(), endpoint);
  proxy.GetProductCompleted += new EventHandler
     <GetProductCompletedEventArgs>(proxy_GetProductCompleted);  
  proxy.InnerChannel.OperationTimeout = TimeSpan.FromSeconds(30);
  proxy.GetProductAsync();
}

Callback method code

We need to write the code that updates the page controls in the callback method. We will use the controls we defined earlier in the Page.xaml page. Note that we use the DataContext property of the Grid to assign the values to the bound controls. We also use the Product objects' properties directly when assigning values to the labels. The product object is passed in to the event method through the GetProductCompletedEventArgs parameter. We also set the error message lable to reflect if any Web Service connection error has occurred.

void proxy_GetProductCompleted(object sender, GetProductCompletedEventArgs e)
{
  Product product = (Product)e.Result;
  lblErrorMessage.Text = "";
  try
  {
     //Data bound through binding expression in Page.xaml
     ProductDetails.DataContext = product;

     //Data bound through through code
     lblProductNumber.Text = product.ProductNumber;
     lblName.Text = product.Name;
     lblPrice.Text = product.Price.ToString();
     lblSize.Text = product.Size.ToString();
     lblWeight.Text = product.Weight.ToString();
  }
  catch { lblErrorMessage.Text = "Error calling Web Service"; }
}

The BindingValidationError event method code

We need to write some code to reflect if any binding error has occurred while changing values in the textboxes, to achieve this we activate the BindingValidationError event that we created earlier for the grid. We will cast the ValidationErrorEventArgs parameter back to a Product object that we can use to extract information. We will also let the textbox retain focus. Remember that the binding evaluation occur at different stages for different controls; for instance it will be fired when a user checks a checkbox, but only after a textbox loses its focus.

private void ProductDetails_BindingValidationError(object sender,
  ValidationErrorEventArgs e)
{
  lblErrorMessage.Text =
     "The value has not changed \nThe stored value is still: " +
     ((Product)ProductDetails.DataContext).Price.ToString();
  txtPrice.Focus();
}

The TextChanged event method code

The last thing we need to do is to clear the error label when the text changes in the textbox. To achieve this we activate the TextBox_TextChanged event and write some code in its method.

private void TextBox_TextChanged(object sender, TextChangedEventArgs e)
{
 
lblErrorMessage.Text = "";
}

Testing the WCF Service

Let's call the GetProduct method from the Silverlight application. Press F5 on the keyboard to run the application and click on the button test the Web Service. Also try to alter the values of the textboxes; if you for instance write a non-numeric value in the Price textbox and leave the control an error should be displayed for you below the other controls.


Similar Articles