Thursday, February 12, 2009

Asynchronous calls to a WebService

Figured out how to call a WebService asynchronously using a Callback in .NET. The process is fairly easy and straightforward.

This article Professional ASP.NET Web Services : Asynchronous Programming provided me with most of the information that I needed, and even had a few solutions discussed, of which I chose the one which suits me best

First of all, of course, I need a WebService

namespace AsyncService
{
///
/// Summary description for Service1
///

[WebService(Namespace = "http://tempuri.org/")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
[ToolboxItem(false)]
// To allow this Web Service to be called from script, using ASP.NET AJAX,
// uncomment the following line.
// [System.Web.Script.Services.ScriptService]
public class AsyncService : System.Web.Services.WebService
{

[WebMethod(Description="Returna a random value under 1000")]
public int[] GetRandomValue(int id, int delay)
{
Random random = new Random();
int randomValue = random.Next(1000);
int[] returnValue = new int[] { id, randomValue };
Thread.Sleep(delay);
return returnValue;
}
}
}

This service only returns a random number. It also returns the number after some delay to imitate that it is actually does something useful.

A Web Service proxy class provides a wrapper that lets me communicate to a WebService.

namespace AsyncCaller
{
[WebServiceBindingAttribute(Name = "AsyncRequestSoap", Namespace
= "http://tempuri.org/")]
public class AsyncCallerProxy : SoapHttpClientProtocol
{
public AsyncCallerProxy()
{
this.Url = "http://localhost/MyAsyncService/AsyncService.asmx";
}

[SoapDocumentMethodAttribute("http://tempuri.org/GetRandomValue",
Use = SoapBindingUse.Literal, ParameterStyle = SoapParameterStyle.Wrapped)]
public int[] GetRandomValue(int id, int delay)
{
object[] results = this.Invoke("GetRandomValue", new object[]
{ id, delay });
return ((int[])results[0]);
}

public IAsyncResult BeginGetRandomValue(int id, int delay,
AsyncCallback callback, object asyncState)
{
return this.BeginInvoke("GetRandomValue", new object[] {
id, delay}, callback, asyncState);
}

public int[] EndGetRandomValue(IAsyncResult asyncResult)
{
object[] results = this.EndInvoke(asyncResult);
return ((int[])(results[0]));
}
}
}

Now I want to make a small demonstration of asynchronous communication to the WebService.

Before that I would need a very simple helper class to make it easy.

public class AsyncHelper
{
public AsyncHelper(int id)
{
this.HelperID = id;
Random random = new Random();
this.RandomDelay = random.Next(10000);
}

int _helperID = 0;

public int HelperID
{
get { return _helperID; }
set { _helperID = value; }
}

int _randomDelay = 0;

public int RandomDelay
{
get { return _randomDelay; }
set { _randomDelay = value; }
}

int _randomResult = 0;

public int RandomResult
{
get { return _randomResult; }
set { _randomResult = value; }
}
}

When an instatnce of the class is created, it is assigned a random delay value. I will pass it to the WebService and will get a response after a number of milliseconds defined by RandomDelay value.

On my demo application form I have a button and two DataGridViews.

When a button is pressed, an instance of the AsyncHelper class is created and added to the list of currently running requests, which is bound to the first DataGridView. The ID of the class instance and the delay value are passed to the WebService.

After the delay, the WebService returns the ID and the random ‘Result’. An instance of the AsyncHelper is found by ID, the result is assigned and the instance is removed from the current requests list and added to the processed requests list. If the button is pressed multiple times, a user can see multiple requests being added to the list and being returned by the WebService after the delays specified.

public partial class Form1 : Form
{
AsyncCallerProxy objWebService = new AsyncCallerProxy();

//counter for number of requests sent
private int _requestCounter = 0;
private List _asyncRequests = new List();
private List _asyncRequestsProcessed =
new List();

public Form1()
{
InitializeComponent();
bindingSourceRequests.DataSource = _asyncRequests;
dataGridViewRequests.DataSource = bindingSourceRequests;

bindingSourceRequestsProcessed.DataSource = _asyncRequestsProcessed;
dataGridViewRequestsProcessed.DataSource =
bindingSourceRequestsProcessed;

dataGridViewRequests.Columns[0].DataPropertyName = "HelperID";
dataGridViewRequests.Columns[1].DataPropertyName = "RandomDelay";

dataGridViewRequestsProcessed.Columns[0].DataPropertyName =
"HelperID";
dataGridViewRequestsProcessed.Columns[1].DataPropertyName =
"RandomDelay";
dataGridViewRequestsProcessed.Columns[2].DataPropertyName =
"RandomResult";
}

private void buttonRequest_Click(object sender, EventArgs e)
{
//create a new request and add it to request queue
AsyncHelper newRequest = new AsyncHelper(_requestCounter);
_requestCounter++;

_asyncRequests.Add(newRequest);

AsyncCallback asyncCallback = new AsyncCallback(MyCallBack);

IAsyncResult asyncResult;

asyncResult = objWebService.BeginGetRandomValue
(newRequest.HelperID, newRequest.RandomDelay, asyncCallback, null);

UpdateRequestQueueDisplay();
}

private void MyCallBack(IAsyncResult asyncResult)
{
int[] returnValue = objWebService.EndGetRandomValue(asyncResult);
int id = returnValue[0];
int result = returnValue[1];

AsyncHelper currentRequest =
_asyncRequests.Find(delegate(AsyncHelper testRequest)
{return testRequest.HelperID == id;});

//request is processed, remove it from the queue and add to processed
//requests list
if (currentRequest != null)
{
currentRequest.RandomResult = result;
_asyncRequestsProcessed.Add(currentRequest);
_asyncRequests.Remove(currentRequest);

UpdateRequestQueueDisplay();
}
}

private void UpdateRequestQueueDisplay()
{
//fixes the cross-thread issue while accessing the form controls
this.BeginInvoke(new MethodInvoker(delegate()
{
this.bindingSourceRequests.ResetBindings(false);
this.bindingSourceRequestsProcessed.ResetBindings(false);
}));

}
}
by . Also posted on my website

No comments: