1. Problem
Your Silverlight application needs to exchange POX messages with an HTTP endpoint.
2. Solution
Use the HttpWebRequest/HttpWebResponse pair of types in System.Net to exchange POX messages with an HTTP endpoint.
3. How It Works
POX-style message
exchange can be an attractive alternative to the more structured
SOAP-based message exchange. It does not impose any of the specific
format requirements of SOAP, and there is much more freedom regarding
how messages are structured. Consequently, it requires fewer
infrastructural requirements, benefits from more implementation
options, and can be consumed by almost any XML-aware runtime
environment.
The downside of such
loose-format messaging, however, is that very often, client frameworks
do not have the luxury of tool-based assistance like Visual Studio's
service proxy-generation features. Also, client APIs that consume such
services are somewhat lower level—in most cases, they implement some
sort of request/response mechanism over HTTP, with support for
HTTP-related features, like choice of verbs or Multipurpose Internet
Mail Extensions (MIME) types.
3.1. Using HttpWebRequest/HttpWebResponse in Silverlight
The HttpWebRequest/HttpWebResponse
types implement an API that allows Silverlight clients to send requests
and receive responses from HTTP endpoints in an asynchronous fashion.
HttpWebRequest and HttpWebResponse are abstract classes and hence are not directly constructable. To use the API, you start by invoking the static Create()HttpWebRequest type, supplying the URL of the endpoint you wish to interact with. What is returned to you is an instance of WebRequest—the base class for HttpWebRequest. You have the option of setting the desired HTTP verb to use through the HttpWebRequest.Method property—HttpWebRequestMethod property on a newly created web request is GET. You really only need to set it if you are going to use POST. method on the supports GET and POST. The default value of the
You also have the option of setting the MIME type using the ContentType property.
3.1.1. Using GET
The GET verb is typically
used to request a web resource from an endpoint. The request is
represented as the URI of the resource, with optional additional query
string parameters. You invoke a GET request using the BeginGetResponse() method on the WebRequest instance. Pass a delegate of the form AsyncResult
around a handler that you implement. This handler gets called back when
the async request operation completes. In the handler, call EndGetResponse() to access any response information returned in the form of a WebResponse instance. You can then call WebResponse.GetResponseStream() to access the returned content.
3.1.2. Using POST
If you need to submit content
back to an HTTP endpoint for processing, and you want to include the
data in the body of the request, you must use the POST verb. To POST
content, you need to write the content to be posted into the request
stream. To do this, first call BeginGetRequestStream(), again passing in an AsyncResultEndGetRequestStream() to acquire a stream to the request's body, and write the content you intend to POST to that stream. Then, call BeginGetResponse() using the same pattern outlined earlier. delegate. In the handler, call
3.1.3. Handling Asynchronous Invocation
The methods discussed here follow an asynchronous invocation pattern. The BeginGetResponse() and BeginGetRequestStream()
methods dispatch the execution to a randomly allocated background
thread, returning control to your main application thread right away.
The AsyncResult handlers that you pass
in as callbacks are invoked on these background threads. If you want to
access any parts of your object model created on the main thread—such
as the controls on the page or any types that you instantiate elsewhere
in your code—from one of these handlers, you cannot do it in the normal
fashion, because doing so causes a cross-thread access violation. You
need to first switch context to the thread that owns the object you are
trying to access. To do this, you must use a type called Dispatcher.
The Dispatcher type is designed to manage work items for a specific thread. More specifically, in this context, a Dispatcher exposes methods that allow you to execute a piece of code in the context of the thread that owns the Dispatcher. The DependencyObject type, and hence all derived types, exposes a DispatcherPage itself.
instance, which is associated with the thread that creates the type.
One of the easiest instances you can get hold of is exposed on the
To use the Dispatcher, use the static BeginInvoke() function, passing in a delegate to the method that you want to execute on the Dispatcher's thread, regardless of which thread it is called from. Dispatcher
ensures a proper thread-context switch to execute the targeted method
on its owning thread. For instance, if you want to access some element
on the Page from a background thread, you use the Page's Dispatcher as described.
NOTE
Although we chose
POX messages as the first example of demonstrating this API, the types
are a general-purpose means of HTTP communication from Silverlight. You
can exchange other kinds of information over HTTP using these as well.
3.2. Configuring WCF to Use Non-SOAP Endpoints
Although the Silverlight
techniques demonstrated in this API can be used with any HTTP endpoint
that accepts and responds with POX messages, we have chosen to
implement the POX/HTTP endpoint using WCF.
WCF by default uses
SOAP-based message exchange, but it also enables a web programming
model that allows non-SOAP endpoints over HTTP to be exposed from WCF
services. This allows REST-style services to use formats like POX or
JSON to exchange messages with clients.
To enable web-style, URI-based invocation of operations on these services, apply one of the WebGetAttribute or WebInvokeAttribute types found in System.ServiceModel.Web to the operations. The WebGetAttribute
mandates use of the HTTP GET verb to acquire a resource; hence the only
way to pass in parameters to such an operation is through query string
parameters on the client that are mapped by the WCF runtime to
parameters in the operation. As an example, here is the declaration of
a GET-style operation:
[OperationContract]
[WebGet()]
Information GetSomeInformation(int Param);
You can invoke this operation by sending an HTTP GET request to an URI endpoint, formatted like so:
http://someserver/someservice.svc/GetSomeInformation?Param=50
WebInvokeAttribute
defaults to the use of the POST verb but can also be specified to
accept the PUT or DELETE verb. If you are using POST, the message body
is expected to be in the POST body content, whereas you can continue to
use query-string style parameters with a verb like PUT. However, keep
in mind that Silverlight only allows the use of POST, not PUT or DELETE.
In addition to using these
attributes to decorate your WCF operations, you also need to specify
the appropriate binding and behavior. To use POX messaging over HTTP,
you must use WebHttpBinding for the endpoint. Here is a snippet from a WCF config file that shows this:
<endpoint address="" binding="webHttpBinding" contract="IProductManager" />
4. The Code
To illustrate the concept, change the WCF service to use POX messages over HTTP, and implement the client using the HttpWebRequest/HttpWebResponse API.
Listing 1 shows the service contract for the WCF service adapted for POX exchange over HTTP.
Listing 1. Service contract for the POX Service in ServiceContract.cs
using System.ServiceModel; using System.ServiceModel.Web; using System.Xml;
namespace Recipe7_2.ProductsDataPOXService { [ServiceContract] public interface IProductManager { [OperationContract] [XmlSerializerFormat()] [WebGet()] XmlDocument GetProductHeaders();
[OperationContract] [XmlSerializerFormat()] [WebInvoke()] void UpdateProductHeaders(XmlDocument Updates);
[OperationContract] [XmlSerializerFormat()] [WebGet()] XmlDocument GetProductDetail(ushort ProductId);
[OperationContract] [XmlSerializerFormat()] [WebInvoke()] void UpdateProductDetail(XmlDocument Update); } }
|
POX messages are just blocks of well-formed XML. Consequently, you use the System.Xml.XmlDocument type to represent the messages being exchanged. Because XmlDocument does not have the WCF DataContractAttribute applied to it, WCF cannot use the default data-contract serialization to serialize these messages. So, you also apply System.ServiceModel.XmlSerializerFormatAttribute() to the service operations to use XML serialization.
Listing 2 shows the implementation of the GetProductHeaders() and the UpdateProductHeaders() operations.
Listing 2. Service implementation for POX service in ProductManager.cs
using System.IO; using System.Linq; using System.ServiceModel.Activation;
using System.Web;
using System.Xml; using System.Xml.Linq;
namespace Recipe7_2.ProductsDataPOXService { [AspNetCompatibilityRequirements( RequirementsMode = AspNetCompatibilityRequirementsMode.Required)] public class ProductManager : IProductManager { public XmlDocument GetProductHeaders() { //open the local data file StreamReader stmrdrProductData = new StreamReader( new FileStream( HttpContext.Current.Request.MapPath("App_Data/XML/Products.xml"), FileMode.Open)); //create and load an XmlDocument XmlDocument xDoc = new XmlDocument(); xDoc.LoadXml(stmrdrProductData.ReadToEnd()); stmrdrProductData.Close();
//return the document HttpContext.Current.Response.Cache.SetCacheability(HttpCacheability.NoCache); return xDoc; }
public void UpdateProductHeaders(XmlDocument Updates) { //load the XmlDocument containing the updates into a LINQ XDocument XDocument xDocProductUpdates = XDocument.Parse(Updates.OuterXml); //load the local data file StreamReader stmrdrProductData = new StreamReader( new FileStream( HttpContext.Current.Request.MapPath("App_Data/XML/Products.xml"), FileMode.Open)); XDocument xDocProducts = XDocument.Load(stmrdrProductData); stmrdrProductData.Close(); //for each of the updated records, find the matching record in the local data //using a LINQ query //and update the appropriate fields foreach (XElement elemProdUpdate in xDocProductUpdates.Root.Elements()) {
XElement elemTarget = (from elemProduct in xDocProducts.Root.Elements() where elemProduct.Attribute("ProductId").Value == elemProdUpdate.Attribute("ProductId").Value select elemProduct).ToList()[0]; if (elemTarget.Attribute("Name") != null) elemTarget.Attribute("Name"). SetValue(elemProdUpdate.Attribute("Name").Value); if (elemTarget.Attribute("ListPrice") != null) elemTarget.Attribute("ListPrice"). SetValue(elemProdUpdate.Attribute("ListPrice").Value); if (elemTarget.Attribute("SellEndDate") != null) elemTarget.Attribute("SellEndDate"). SetValue(elemProdUpdate.Attribute("SellEndDate").Value); if (elemTarget.Attribute("SellStartDate") != null) elemTarget.Attribute("SellStartDate"). SetValue(elemProdUpdate.Attribute("SellStartDate").Value); } //save the changes StreamWriter stmwrtrProductData = new StreamWriter( new FileStream( HttpContext.Current.Request.MapPath("App_Data/XML/Products.xml"), FileMode.Truncate)); xDocProducts.Save(stmwrtrProductData); stmwrtrProductData.Close(); }
public XmlDocument GetProductDetail(ushort ProductId) { StreamReader stmrdrProductData = new StreamReader( new FileStream( HttpContext.Current.Request.MapPath("App_Data/XML/Products.xml"), FileMode.Open)); XDocument xDocProducts = XDocument.Load(stmrdrProductData); XDocument xDocProdDetail = new XDocument( (from xElem in xDocProducts.Root.Elements() where xElem.Attribute("ProductId").Value == ProductId.ToString() select xElem).ToList()[0]);
XmlDocument xDoc = new XmlDocument(); xDoc.LoadXml(xDocProdDetail.ToString()); stmrdrProductData.Close();
HttpContext.Current.Response.Cache.SetCacheability(HttpCacheability.NoCache); return xDoc; } public void UpdateProductDetail(XmlDocument Update) { XDocument xDocProductUpdates = XDocument.Parse(Update.OuterXml); XElement elemProdUpdate = xDocProductUpdates.Root; StreamReader stmrdrProductData = new StreamReader( new FileStream( HttpContext.Current.Request.MapPath("App_Data/XML/Products.xml"), FileMode.Open));
XDocument xDocProducts = XDocument.Load(stmrdrProductData); stmrdrProductData.Close();
XElement elemTarget = (from elemProduct in xDocProducts.Root.Elements() where elemProduct.Attribute("ProductId").Value == elemProdUpdate.Attribute("ProductId").Value select elemProduct).ToList()[0];
if (elemTarget.Attribute("Class") != null) elemTarget.Attribute("Class"). SetValue(elemProdUpdate.Attribute("Class").Value); if (elemTarget.Attribute("Color") != null) elemTarget.Attribute("Color"). SetValue(elemProdUpdate.Attribute("Color").Value); if (elemTarget.Attribute("DaysToManufacture") != null) elemTarget.Attribute("DaysToManufacture"). SetValue(elemProdUpdate.Attribute("DaysToManufacture").Value); if (elemTarget.Attribute("DiscontinuedDate") != null) elemTarget.Attribute("DiscontinuedDate"). SetValue(elemProdUpdate.Attribute("DiscontinuedDate").Value); if (elemTarget.Attribute("FinishedGoodsFlag") != null) elemTarget.Attribute("FinishedGoodsFlag"). SetValue(elemProdUpdate.Attribute("FinishedGoodsFlag").Value); if (elemTarget.Attribute("MakeFlag") != null) elemTarget.Attribute("MakeFlag"). SetValue(elemProdUpdate.Attribute("MakeFlag").Value); if (elemTarget.Attribute("ProductLine") != null) elemTarget.Attribute("ProductLine"). SetValue(elemProdUpdate.Attribute("ProductLine").Value); if (elemTarget.Attribute("ProductNumber") != null) elemTarget.Attribute("ProductNumber").
SetValue(elemProdUpdate.Attribute("ProductNumber").Value); if (elemTarget.Attribute("ReorderPoint") != null) elemTarget.Attribute("ReorderPoint"). SetValue(elemProdUpdate.Attribute("ReorderPoint").Value); if (elemTarget.Attribute("SafetyStockLevel") != null) elemTarget.Attribute("SafetyStockLevel"). SetValue(elemProdUpdate.Attribute("SafetyStockLevel").Value); if (elemTarget.Attribute("StandardCost") != null) elemTarget.Attribute("StandardCost"). SetValue(elemProdUpdate.Attribute("StandardCost").Value); if (elemTarget.Attribute("Style") != null) elemTarget.Attribute("Style"). SetValue(elemProdUpdate.Attribute("Style").Value);
StreamWriter stmwrtrProductData = new StreamWriter(new FileStream(HttpContext. Current.Request.MapPath("App_Data/XML/Products.xml"), FileMode.Truncate)); xDocProducts.Save(stmwrtrProductData); stmwrtrProductData.Close(); } } }
|
Because GetProductHeaders() returns a POX message, you open the local data file, load the XML content into an XmlDocument instance, and return the XmlDocument instance. The XmlSerializerFormatAttribute on the operation ensures that the XML content is formatted as it is on the wire.
In UpdateProductHeaders(), you receive the updates as a POX message. You parse the content of the message and load it into an instance of the XDocument
type so that it can participate in a LINQ to XML query. You use the
query to find the matching records in the local XML data, also loaded
in an XDocument, and copy over the updates before you save the local data back to its file store.
The GetProductDetail() and UpdateProductDetail() methods follow the same implementation pattern.
Note the call to SetCacheability() to set the cache policy to NoCache before you return data from the GetProductHeaders() and GetProductDetail()
methods. The Silverlight network stack relies on the browser's network
stack, and the default behavior has the browser look for the data
requested in its own cache first. Setting this in the server response
causes the browser to never cache the returned data, so that every time
the client calls the service operation, the operation is invoked and
current data is returned. This is important for data that can be
changed between requests, as in this case with the update operations.
For purely lookup data that seldom changes, you may want to leave the
browser cache on, and possibly stipulate an expiration. You can refer
to more information about controlling the browser-caching policy from
the server on MSDN at msdn.microsoft.com/en-us/library/system.web.httpresponse.cache.aspx.
Now, let's look at the
client code in the codebehind class. Because the complete code listing
is repetitive between the product header- and product detail-related
functionality, we list only the code pertaining to the acquiring and
updating product headers. You can access the book's sample code to get
the full implementation.
Listing 3 shows the product header-related functionality.
Listing 3. Partial listing of the codebehind in MainPage.xaml.cs
using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; using System.Windows; using System.Windows.Controls; using System.Xml.Linq; namespace Recipe7_2.POXProductsDataViewer { public partial class MainPage : UserControl { private const string ServiceUri = "http://localhost:9292/ProductsPOXService.svc"; bool InEdit = false;
public MainPage() { InitializeComponent();
RequestProductHeaders(); }
private List<ProductHeader> DeserializeProductHeaders(string HeaderXml) { //load into a LINQ to XML Xdocument XDocument xDocProducts = XDocument.Parse(HeaderXml); //for each Product Xelement, project a new ProductHeader List<ProductHeader> ProductList = (from elemProduct in xDocProducts.Root.Elements() select new ProductHeader { Name = elemProduct.Attribute("Name") != null ? elemProduct.Attribute("Name").Value : null, ListPrice = elemProduct.Attribute("ListPrice") != null ? new decimal?( Convert.ToDecimal(elemProduct.Attribute("ListPrice"). Value)) : null, ProductId = elemProduct.Attribute("ProductId") != null ? new ushort?(Convert.ToUInt16(elemProduct.Attribute("ProductId"). Value)) : null, SellEndDate = elemProduct.Attribute("SellEndDate") != null ?
elemProduct.Attribute("SellEndDate").Value : null, SellStartDate = elemProduct.Attribute("SellStartDate") != null ? elemProduct.Attribute("SellStartDate").Value : null
}).ToList(); //return the list return ProductList; }
private void RequestProductHeaders() { //create and initialize an HttpWebRequest WebRequest webReq = HttpWebRequest.Create( new Uri(string.Format("{0}/GetProductHeaders", ServiceUri)));
//GET a response, passing in OnProductHeadersReceived //as the completion callback, and the WebRequest as state webReq.BeginGetResponse( new AsyncCallback(OnProductHeadersReceived), webReq); }
private void OnProductHeadersReceived(IAsyncResult target) { //reacquire the WebRequest from the passed in state WebRequest webReq = target.AsyncState as WebRequest; //get the WebResponse WebResponse webResp = webReq.EndGetResponse(target);
//get the response stream, and wrap in a StreamReader for reading as text StreamReader stmReader = new StreamReader(webResp.GetResponseStream()); //read the incoming POX into a string string ProductHeadersXml = stmReader.ReadToEnd(); stmReader.Close();
//use the Dispatcher to switch context to the main thread //deserialize the POX into a Product Header collection, //and bind to the DataGrid Dispatcher.BeginInvoke(new Action(delegate { ProductHeaderDataGrid.ItemsSource = DeserializeProductHeaders(ProductHeadersXml); }), null);
} private void UpdateProductHeaders()
{ //create and initialize an HttpWebRequest WebRequest webReq = HttpWebRequest.Create( new Uri(string.Format("{0}/UpdateProductHeaders", ServiceUri))); //set the VERB to POST webReq.Method = "POST"; //set the MIME type to send POX webReq.ContentType = "text/xml"; //begin acquiring the request stream webReq.BeginGetRequestStream( new AsyncCallback(OnProdHdrUpdReqStreamAcquired), webReq); }
private void OnProdHdrUpdReqStreamAcquired(IAsyncResult target) { //get the passed in WebRequest HttpWebRequest webReq = target.AsyncState as HttpWebRequest; //get the request stream, wrap in a writer StreamWriter stmUpdates = new StreamWriter(webReq.EndGetRequestStream(target)); Dispatcher.BeginInvoke(new Action(delegate { //select all the updated records List<ProductHeader> AllItems = ProductHeaderDataGrid.ItemsSource as List<ProductHeader>; List<ProductHeader> UpdateList = new List<ProductHeader> ( from Prod in AllItems where Prod.Dirty == true select Prod );
//use LINQ to XML to transform to XML XElement Products = new XElement("Products", from Prod in UpdateList select new XElement("Product", new XAttribute("Name", Prod.Name), new XAttribute("ListPrice", Prod.ListPrice), new XAttribute("ProductId", Prod.ProductId), new XAttribute("SellEndDate", Prod.SellEndDate), new XAttribute("SellStartDate", Prod.SellStartDate)));
//write the XML into the request stream Products.Save(stmUpdates); stmUpdates.Close();
//start acquiring the response webReq.BeginGetResponse( new AsyncCallback(OnProdHdrsUpdateCompleted), webReq); }));
}
private void OnProdHdrsUpdateCompleted(IAsyncResult target) { HttpWebRequest webResp = target.AsyncState as HttpWebRequest; HttpWebResponse resp = webResp.EndGetResponse(target) as HttpWebResponse; //if response is OK, refresh the grid to //show that the changes actually happened on the server
if (resp.StatusCode == HttpStatusCode.OK) RequestProductHeaders(); } void ProductHeaderDataGrid_SelectionChanged(object sender, EventArgs e) { if (ProductHeaderDataGrid.SelectedItem != null) {
//invoke the GetProductDetails() service operation, //using the ProductId of the currently selected ProductHeader RequestProductDetail( (ProductHeaderDataGrid.SelectedItem as ProductHeader).ProductId.Value); } } void ProductHeaderDataGrid_CurrentCellChanged(object sender, EventArgs e) { //changing the dirty flag on a cell edit for the ProductHeader data grid if (InEdit && (sender as DataGrid).SelectedItem != null) { ((sender as DataGrid).SelectedItem as ProductHeader).Dirty = true; InEdit = false; } } private void ProductHeaderDataGrid_BeginningEdit(object sender, DataGridBeginningEditEventArgs e) { InEdit = true; }
void Click_Btn_SendHeaderUpdates(object Sender, RoutedEventArgs e) { UpdateProductHeaders(); } void Click_Btn_SendDetailUpdate(object Sender, RoutedEventArgs e) { UpdateProductDetail(); }
//Product detail functionality omitted – //please refer to sample code for full listing } }
|
In the RequestProductHeaders() method, you create the HttpWebRequest and submit it asynchronously using BeginGetResponse(). Note the passing of the WebRequest instance as the state parameter to BeginGetResponse(). On completion of the async call, when the supplied callback handler OnProductHeadersReceived() is called back, you need access to the WebRequest instance in order to complete the call by calling EndGetResponse()
on it. Passing it in as the state parameter provides access to it in a
thread-safe way, inside the handler executing on a background thread.
In OnProductHeadersReceived(), you obtain the WebRequest from the IAsyncResult.AsyncState parameter and then obtain the WebResponse using the EndGetResponse() method on the WebRequest. Open the response stream using WebResponse.GetResponseStream(), read the POX message from that stream, and bind the data to the ProductHeaderDataGrid after deserializing it into a suitable collection of ProductHeaders using DeserializeProductHeaders(). DeserializeProductHeaders() uses a LINQ to XML query to transform the POX message to an instance of List<ProductHeader>.
To send updates back to the service, you use the UpdateProductHeaders() method. Set the Method property of the request to POST, with the MIME type appropriately set to text/XML. Then, asynchronously acquire the request stream with a call to BeginGetRequestStream().
When BeginGetRequestStream() is completed, the OnProdHdrUpdReqStreamAcquired() callback occurs on a background thread. In the handler, switch thread context back to the main thread using Dispatcher.Invoke(). In the delegate passed to Invoke(),
filter out the updated records and transform the records to XML using
LINQ to XML, and then serialize the resulting XML to the request
stream. After closing the stream, submit the POST calling BeginGetResponse(). After the POST completes, you have the ability to check the StatusCode property to decide on your course of action. If the code is HttpStatusCode.OK, refresh the data from the server by calling RequestProductDetail() again. The only other possible value is HttpStatusCode.NotFound, which indicates a problem with the service call and can be used to display a suitable error message.