Before I start, I just want to mention that this is a very old "flame war".
On the official Silverlight Forums, there was a thread that went on and on, becoming
quite ugly with various ad-hominem attacks, and it eventually got so ugly that
--thankfully -- the moderators took the whole thread down. However, not being
averse to controversy, I of course had my say there and was duly accused by various
characters of being arrogant, an idiot, and what not. My position then was exactly
as it remains now; basically, Microsoft is not going to give you native synchronous
web requests in Silverlight because there is too much room for abuse by developers
who will freeze the UI Thread, the browser then locks up, and of course Microsoft
gets blamed for a "crappy product".
Furthermore, in order to remain truly "cross browser", Silverlight needs
to use the NPAPI architecture that almost all modern browsers provide. This only
allows for asynchronous web requests.
Flash developers, having had to use async-only for a lot longer than Silverlight
developers, have exactly the same situation except that they are by now a bit
more gentlemanly about accepting it. The advice I gave then (and now) was, "Get
over it."
However any discussion of this situation would not be complete without mention of
different ways that you actually can "fake" the async runtime into "emulating" a synchronous call.
My position on this, and the way I've been programming Silverlight from the start,
is not to use this - instead, go with the flow, learn to use and take advantage
of the async model, and 99% of what you need to do will be eminently do-able.
The other 1% might even be the result of poor design by -- gasp -- you, the developer!
There have been a number of successful attempts by various developers to "fake"
a synchronous Web Request, some of them extremely complex and unwieldy to use,
but what I'll illustrate here is actually very simple - both to understand, and
to use.
I'll give you an example of both a GET and a POST using the handy WebClient Class.
For the GET, we'll just retrieve an RSS feed and display it in a TextBlock. I
didn't even bother formatting it from the raw XML as that is not the purpose
of the demo code.
For the POST, we'll post a string to an ASPX page on localhost, take the posted string,
prepend "You posted:" to it, and send it back to the client for display.
You can use the same technique for posting XML to a WebService or to a REST-enabled
service endpoint.
And finally, though I do not show it here, the download contains a complete implementation
of the same technique doing multiple items in parallel using the Silverlight
Threadpool.
That should cover 90 percent or better of all the developer scenarios you may run
into. Once you see the simple technique I show, it should be pretty easy to adapt
it to HttpWebRequest / Response or any other call you need to make.
The class that handles the "heavy lifting" for both the GET and the POST
is pretty simple:
namespace SyncWebClient
{
public static class SyncWc
{
private static ManualResetEvent mre = new ManualResetEvent(false);
public static string GetData(string url)
{
string s = "";
WebClient wc = new WebClient();
wc.DownloadStringCompleted += (sender, e) =>
{
s = e.Result;
mre.Set();
};
wc.DownloadStringAsync(new Uri(url));
mre.WaitOne(1000);
return s;
}
public static string PostData(string url, string dataToPost)
{
string s = "";
WebClient wc = new WebClient();
wc.UploadStringCompleted += (sender, e) =>
{
s = e.Result;
mre.Set();
};
wc.UploadStringAsync(new Uri(url),dataToPost);
mre.WaitOne(1000);
return s;
}
}
}
So when you want to do a GET Request for the results of a Uri, you would use code
like this:
private void Button1_Click(object sender, RoutedEventArgs e)
{
Thread thd = new Thread(new ParameterizedThreadStart(GetPage));
thd.IsBackground = true;
thd.Start("http://feeds.feedburner.com/blogspot/lGrQ");
}
private void GetPage(object url)
{
string s= SyncWc.GetData((string)url);
Dispatcher.BeginInvoke(() => text1.Text = s);
}
To make a POST, the code would resemble the following:
private void Button2_Click(object sender, RoutedEventArgs e)
{
Thread thd = new Thread(new ParameterizedThreadStart(PostPage));
thd.IsBackground = true;
//format the 2 items needed as a semicolon-delimited string so you can split to an
array for use.
thd.Start("http://localhost:2067/PostToMe.aspx;this is the data to post");
}
private void PostPage(object stuff)
{
Dispatcher.BeginInvoke(() => text1.Text = "");
string[] guys = ((string) stuff).Split(";".ToCharArray() );
string url = guys[0];
string data = guys[1];
string s = SyncWc.PostData((string)url,data);
Dispatcher.BeginInvoke(() => text1.Text = s);
}
The "PostToMe.aspx" page just does this:
protected void Page_Load(object sender, EventArgs e)
{
byte[] bData = Request.BinaryRead(Request.ContentLength);
string s = System.Text.Encoding.UTF8.GetString(bData);
string returnString = "You posted:" + s;
Response.Write(returnString);
}
Note in all cases that using the timeout overload on the ManualResetEvent's WaitOne
method is important. If something bad happens, you "DO" want your call
to timeout and return, otherwise you have simply accomplished exactly what Microsoft
is trying to avoid - locking up the UI thread.
All you are doing is making your call on a background thread, and using the ManualResetEvent's
WaitOne call to "hold up the operation" until either your result is
ready to send back or the operation times out. In either case, it looks and acts
"just like" a synchronous, blocking method call. The rest of the code
is simply required Dispatcher code to update the UI from a background thread.
Again, the reason why this approach does not interfere with the UI thread is
because it is not being done there - it's being done on a background thread.
If you want to get more sophisticated, Silverlight supports the ThreadPool where
you can use QueueUserWorkItem to do the same thing and achieve a degree of parallelization
with multiple requests. You can use a counter variable to tell when to call the
ManualResetEvent's Set method after all of the requests have completed, and even
aggregate the results as if it were a single blocking call. Or, you could use
the WaitHandle.WaitAll method overloads with an array of ManualResetEvent, which
is what I've used in the download. Just don't do it on the UI thread.
You can download the full working Visual Studio 2008 sample solution here. The download includes a full implementation of doing the operation with multiple
items using the Silverlight Threadpool with WaitHandle.WaitAll.