Building a real-life web-experience : Don´t trip over the threads.
By Peter van Ooijen
Published: 2/19/2003
Reader Level: Expert
Rated: 4.00 by 1 member(s).
Tell a Friend
Rate this Article
Printable Version
Discuss in the Forums

In my previous contribution I took a look at calling web-services. There I showed how to asynchronously invoke a web-service from a Windows form. Behind the scenes the project involved a lot of threading issues. The .NET framework is very good at handling these but there is one thing I overlooked. In this paper I will dive deeper into the matter and build build a more complicated service which serves jpeg pictures. A windows form consumer will make asynchronous invocations of the web-service to display the pictures on a form. I will show you how to handle binary data in a web-service without tripping over the threads.

What was the problem ?

When invoking a web-service by just calling the web-method the calling application will freeze until the web-service returns a result. As web-services not always return that fast, if they return at all, your application will be more responsive if it makes an asynchronous call to the service. This is done using delegate objects, objects which wrap up a callback-function or event-handler. The web-service gets invoked and the application can continue with whatever it wants to do. When the web-service is finished the callback will invoke the delegate object to actually execute the method originally passed in its constructor. In my implementation the delegates' method added the result string of the web-service to a listbox on the windows form.

public void WsCallback(IAsyncResult ar)
{
      localhost.Service1 ws = ar.AsyncState as localhost.Service1;
      listBox1.Items.Add(ws.EndDoSomething(ar));
}

I overlooked the fact that this callback function is executed in a worker thread of the web-service wrapper. Which is not the same thread as the UI-thread on which the listbox was created. This is not thread-safe, accessing windows controls (to be precise everything which has a windows handle) is only thread-safe from the thread on which the control was created. If you clicked fast enough in the previous demo project it could be possible to get an asynchronous execution error.

The control can be safely accessed from another thread by calling the control's Invoke method. You don't have to know at coding time if you have to use this method, the Controls class in the .NET framework has an InvokeRequired property which will tell you at run time. The invoke method takes another delegate object to wrap up the code to execute. In the second parameter to Invoke the arguments to the delegate method are passed. Invoke will make the thread switch and call the method. To solve my problem I can even reuse the callback method:

public void WsCallback(IAsyncResult ar)
{
   if (this.InvokeRequired)
      {
         object[] args = new object[1];
         args[0] = ar;
         this.Invoke( new AsyncCallback(WsCallback), args );
      }
   else
      {
         localhost.Service1 ws = ar.AsyncState as localhost.Service1;
         listBox1.Items.Add(ws.EndDoSomething(ar));
      }
}

The method first checks the InvokeRequired property of this. This being the form itself. When the callback method is called from the web-service wrapper, from a worker thread, the property will read true. The arguments to the method to invoke is an array of objects. I construct an array with one item to pass through the ar parameter. Now I call the Invoke method of the form passing it a new delegate based on this same method and its arguments. The result will be my code re-entering this method but now from the UI-thread. The InvokeRequired property will read false and I can safely access the listbox.

A web service which serves pictures

In the remainder of this paper I will build another web service and a consumer which will asynchronously invoke it. In the process we will learn more about delegates and about transporting binary data in a web service's result. The new web service is  a picture server. It has two web-methods, one provides a list of pictures available and the other provides the image data. The pictures are published in a virtual directory on my localhost web server, named /galleries/Family. The actual files are located in a subdirectory (Portret) of My Pictures. When browsing //localhost//galleries/Family with IE you see a listing of the files, by clicking them you can view them one at a time in IE. When a web service wants to access the picture files it will not be IE accessing them but the aspnet worker process. This process does not have any rights outside of IIS, I explicitely have to give the aspnet account reading rights in the directory where the files themselves are stored.

Now the web service's implementation can open the picture files.

Publishing the pictures as an XML dataset

The nice thing about an XML dataset is that it provides metadata on the data it contains, it describes it's own content. In the VS.NET schema editor I design a dataset to store a collection of pictures. In the editor I create a new Pictures element and add a name and graphics element to this.

The name is just a string, the graphics element is of type base64Binary. In this element you can store any binary object, like a jpg - file, as a serializable array of bytes.

The typed dataset DataSetPictures has a table Pictures which can store a number of pictures. Any dataset can be serialized, so it can be sent over the HTTPprotocol as result of a web service. Now it is time to create this web service. For the details of doing that I refer to another paper. Having the design surface of the service at hand I drag a dataset from the data tab of the toolbox. VS will propose the typed dataset I had just created.

Which is exactly what I need, a DataSetPictures component available to all web-methods.

A webmethod which returns a list of pictures

The PicturesList webmethod constructs a list of files found in the directory. The method returns this list as a typed DataSetPictures dataset. The filenames are stored in the name field. To find the files the method needs the actual directory, which is computed using the Context.Server property of the web-service.

private string PictureDir
{
   get
   {
      return Context.Server.MapPath("/galleries/Family");
   }
}

This computation is implemented as a private property of the web-service, making it easy to use in the WebMethod's. The DirectoryInfo class is used to find out what can be be found in a directory. It takes the physical path, as provided by the property, in its constructor. The GetFiles method returns an array of files found which are enumerated in a foreach loop:

[WebMethod]

public DataSetPictures PictureList()
   {
      DirectoryInfo pictDir = new DirectoryInfo(PictureDir);
      FileInfo[] pictFiles = pictDir.GetFiles();
      foreach (FileInfo pictFile in pictFiles)
      {
         dataSetPictures1.Pictures.AddPicturesRow(pictFile.Name, null);
      }
return dataSetPictures1;}

dataSetPictures1 is the typed dataset component. Is has a Pictures property which is the typed table of files. This has an AddPicturesRow method which adds a new row, taking the field values in the parameter list. For now I only add the name of the file. You could add a thumbnail preview of the picture in the second parameter, for the sake of demo I will just pass a null.

Consuming the picturelist

The consumer of the web-service is a Windows application. It is a form with a listbox and the webservice is in it's references list. I drag a dataset on the form and again VS proposes the typed dataset DataSetPictures found in the web service referenced. This component houses the picture data. The listbox is bound to the dataset, it will show a list of pictures the web service has to offer.

When the form is opened the dataset is filled. In the form_load event the webservices is invoked. As I perform an asynchronous invocation the loading of the form will not be paused waiting for the web service to return the actual list.

private void Form1_Load(object sender, System.EventArgs e)
   {
      localhost.PictureServices ws = new localhost.PictureServices();
      ws.BeginPictureList(new AsyncCallback(ReceivePictureList), ws);
   }

When the webservice is finished it will call-back the ReceivePictureList method.

private void ReceivePictureList(IAsyncResult ar)
   {
      localhost.PictureServices ws = ar.AsyncState as localhost.PictureServices;
      lock(this)
      {
         dataSetPictures1.Merge(ws.EndPictureList(ar).Pictures);
      }
   }

The method receives the dataset. Before merging the Pictures with the dataSetPictures1 component it has to lock the current form object. During the merge the state of the dataset component is undefined. No other threads (like a callback from another web service invocation) can access the current object until the end of the lock. The merge statement is in a so called critical section, threads pass these one at a time. For more info threading you are invited to my threading article.

A webmethod which returns graphics

The second web-method Picture, does return the actual picture. It wraps it up in a dataset of the same type as we met in PictureList , this time the graphics field will contain the image. The method's parameter indicates which file(s) to return.

[WebMethod]

public DataSetPictures Picture(string FileName)
   {
      DirectoryInfo pictDir = new DirectoryInfo(PictureDir);
      FileInfo[] pictFiles = pictDir.GetFiles(FileName);

      foreach (FileInfo pictFile in pictFiles)
      {
         long pictSize = pictFile.Length;
         byte[] buffer = new byte[pictSize];
         System.IO.FileStream fs = new FileStream(pictFile.FullName, FileMode.Open, FileAccess.Read);
         fs.Read(buffer, 0, (int) pictSize);

         dataSetPictures1.Pictures.AddPicturesRow(pictFile.Name, buffer);

      }

return dataSetPictures1;

}

Again a DirectoryInfo object gets me to the files. This time the GetFiles method is passed the name of the desired file(s). You can use wildcards in this name, like  picture?.jpg. The result of the web-method is a dataset so it can return multiple files in multiple rows. All files are treated one by one in the foreach loop. The graphics field is an array of bytes, to determine the array size I read the length property of the FileInfo object. To read the data in the file I create a FileStream object based on the file's name. The full path to the file is also in the FileInfo object. Respect the flags when opening the file, I had given the aspnet process only read  permission. If I don't explicitly open the stream as read-only I will receive an "access denied" error. Having opened the filestream it is read into the buffer. Together with the name this buffer is added to the dataset.

To get an idea how the graphics data is transported in the webservice you can take a look with your browser. Browse to http://localhost/WebServices/WebPictureService/PictureServices.asmx?op=Picture and invoke the picture method with parameter MyPicture1?.jpg (or whatever you have in your pictures directory). You will receive a nice bunch of XML as a result :

Between the <Graphics> tags there is a very long string of characters. They are the encoded graphics. Take some care when playing with this, when your directory contains a lot of big files you will receive an XML response of many many megabytes; larger then IE or your PC will like.

Consuming the graphics

The windows form has two ways of requesting an image from the web service. Double clicking an item in the listbox request the item pointed at :

private void listBox1_DoubleClick(object sender, System.EventArgs e)
   {
      localhost.DataSetPictures.PicturesRow fileName = dataSetPictures1.Pictures[listBox1.SelectedIndex];
      localhost.PictureServices ws = new localhost.PictureServices();
      ws.BeginPicture(fileName.Name, new AsyncCallback(ReceivePicture), ws);
}

The filename is read from the dataset the listbox is bound to. The SelectedIndex property of the listbox will get me to the right row.

As a second option a button requests all files matching the pattern in a textbox

private void button1_Click(object sender, System.EventArgs e)
   {
       localhost.PictureServices ws = new localhost.PictureServices();
       ws.BeginPicture(textBox1.Text, new AsyncCallback(ReceivePicture), ws);
   }

Both methods perform an asynchronous invocation. When the web-service is finished it invokes the ReceivePicture method to show the result. This method may not touch any of the visual controls, to do that it will Invoke another delegate. The method does some of the non-visual pre-processing

private void ReceivePicture(IAsyncResult ar)
   {
      localhost.PictureServices ws = ar.AsyncState as localhost.PictureServices;
      localhost.DataSetPictures dsp = ws.EndPicture(ar);

      object[] invokeArgs = new Object[1];

      for (int i =0; i < dsp.Pictures.Count; i++ )
         {
            invokeArgs[0] = dsp.Pictures[i];
            this.Invoke(new ShowPictureDelegate(ShowPicture), invokeArgs);
         }

   }

The method catches the dataset returned by the web service and takes it apart row by row. Each row is handled by the delegate method ShowPicture which will show the actual contents on the form. The parameter to this method is typed as a DataSetPictures.PicturesRow . If I want to create a delegate object for this method I have to declare a delegate of its signature

delegate void ShowPictureDelegate(localhost.DataSetPictures.PicturesRow pr);

For a wider explanation on delegates you are invited to another of my dotnetjunkies stories. Here I create a parameter list containing the row and pass it with the delegate to the Invoke method. The delegate is created on the ShowPicture method. In there the real display work is done. On the form is an empty tabControl. For every picture it adds a new tabPage to the control. On the tabpage will be a PictureBox control to house the image. During the process, the tabControl will be in an undetermined state. By locking the form it's access is serialized, different threads will execute one after the other.

private void ShowPicture(localhost.DataSetPictures.PicturesRow pr)
   {
      lock(this)

         {
            TabPage newTabPage = new System.Windows.Forms.TabPage();
            newTabPage.Text = pr.Name;
            tabControl1.Controls.Add(newTabPage);

            PictureBox newPictureBox = new System.Windows.Forms.PictureBox();
            newTabPage.Controls.Add(newPictureBox);

            byte[] buffer = new Byte[pr.Graphics.Length];
            MemoryStream ms = new MemoryStream(buffer);
            ms.Write(pr.Graphics, 0, pr.Graphics.Length);

            newPictureBox.SizeMode = PictureBoxSizeMode.AutoSize;
            newPictureBox.Image = Image.FromStream(ms);

            tabControl1.SelectedIndex = tabControl1.TabPages.Count -1;

         }

}

The method creates a new TabPage. The title is set to the name of the picture. The tabpage becomes visible by adding it to the Controls collection of the tabControl. To display the picture a PictureBox is created and added to the Controls collection of the tabpage.

To catch the graphics I set up an array of bytes, the size of the buffer is in the Length property of the graphics field. I create a MemoryStream on this buffer and fill it with the contents of the Graphics field. This stream is used to fill the Image property of the PictureBox. This filling is done using the static FromStream method of the Image class. You use static methods (of the Image class) without referring to an actual image object.

The result is a tabbed picture album whose content is supplied by a web service.

To conclude

In this paper I have explored the world of web-services a little farther. Working with strongly typed datasets to communicate between service and consumer has lead to plain strongly typed OOP code. In these datasets multiple binary large objects (BLOB's) are transferred in one go. Transferring these large amounts of data could lead to a freezing consumer. The consuming application stays responsive by invoking the web-service asynchronously. Which does result in some threading issues, all of these can be solved in an elegant manner using locks and delegates.



Marketplace
(Sponsored Links)
What are the green links?
   



 
Copyright © 2007 CMP Tech LLC |
Privacy Policy (4/10/06) | Your California Privacy Rights (4/10/06) | Terms of Service | Advertising Info | About Us | Help