1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201
|
<html>
<head>
<title>Using multithreaded operations</title>
<link rel="stylesheet" href="styles/presentation.css">
</head>
<h1>Principals</h1>
The .NET SDK uses the event-based Asynchronous Pattern, as described in MSDN. It uses the AsyncOperationManager to ensure that the implemented events and delegates are called back on the same thread that started the operation.
<h1>Example</h1>
A complete example implementing query/update/insert/download is provided in the Photobrowser example application, that you can find in the /cs/samples/PhotoBrowser directory of the .NET SDK installation.
<h2>Walkthrough</h2>
The following code snippets are taken directly from the sample application.
<h3>Setting it up</h3>
If you spawn an operation in a different thread, you need to be able to get notifications about the progress and the state of the operation in that thread. To enable this, the .NET SDK uses an event system on the service object. To set up events for progress report and completion of the operation, you just need to set 2 events, after you created your service, like this:
<p>
<code><pre>
this.picasaService.AcynOperationCompleted += new AsyncOperationCompletedEventHandler(this.OnDone);
this.picasaService.AcynOperationProgress += new AsyncOperationProgressEventHandler(this.OnProgress);
</pre></code>
</p>
With those 2 lines you will now receive events from asyncronous operations executed by this particular service object.
Let's assume now, you want to query a feed in the background. The service object has a new method for this, called <code>QueryFeedAsync</code>, and it is used in the Photobrowser.cs file in the following method:
<p>
<code><pre>
public void StartQuery(string uri, string albumTitle)
{
UserState us = new UserState();
this.states.Add(us);
us.opType = UserState.OperationType.query;
us.filename = albumTitle;
this.picasaService.QueryFeedAync(new Uri(uri), DateTime.MinValue, us);
}
</pre>
</code>
</p>
So what does this code do? The QueryFeedAsync method needs a URI as a target, a DateTime value for the If-Modified-Since header (if you do not care, just use the MinValue as in the sample), and a unique identifier for this background task. This identifier will be used again in the events to associate a given event notification with what it was that you were trying to do.
As the PhotoBrowser application can issue several different background requests, we use a <code>UserState</code> object to remember what kind of operation it was (in the above sample this is indicated with the UserState.OperationType.query), as well to realize if the callback was actually meant for the object that received it.
This is due to the fact that there are several forms open that all share the same service. Each of these forms adds their event handlers as in the sample code above. This means that the SDK will notify all open forms of progress reports. To distinguish that a given event is arriving at the correct form, the forms create <code>UserState</code> objects, and remember those in the states variable.
An alternative solution would have been to just create a new service object per form, that would avoid this complication, but use more resources.
<h3>Handling progress report</h3>
Below you see the first event handler:
<code><pre>
private void OnProgress(object sender, AsyncOperationProgressEventArgs e)
{
if (this.states.Contains(e.UserState as UserState) == true)
{
this.progressBar.Value = e.ProgressPercentage;
}
}
</pre></code>
All we do here is that we set a UI element on the form, the progress bar control to the given percentage value of the event, and we are done.
Note though, we will not be able to get reasonable numbers for all requests. There are situations where you donwload a resource and the server does not return a content-length header. In this case the complete size of the download is not known before it is completed, therefore having an exact progress report is not possible. The SDK will still send you notifications, and let you know how much data is already downloaded, but the number for the ProgressPercentage will be constant at 100. You can determine this situation by evaluating the CompleteSize property of the event arguments, if that property is -1, the length of the data transfer is unknown.
<h3>Handling background task completion</h3>
When a background task finishes, the SDK fires the CompletedEvent handler. This can be implemented as simple as taken the resulting object and putting it into a listbox, but in case of the PhotoBrowser and it's several different background operations, this is a bit more involved.
Therefore, we look at the code in chunks. The first thing the Photobrowser does is this:
<code><pre>
private void OnDone(object sender, AsyncOperationCompletedEventArgs e)
{
UserState ut = e.UserState as UserState;
if (this.states.Contains(ut) == false)
return;
this.states.Remove(ut);
if (e.Error == null)
{
</pre></code>
So first of all, we check if the event happens to be interesting to us. If it is not, the code returns. If it is, we remove the object from our list. Then we check if the error object of the argument is null (the object might hold an exception thrown on the background thread), if it is, we continue processing.
To handle our simple query-a-feed case, we need to check the operation type:
<code><pre>
if (ut.opType == UserState.OperationType.query ||
ut.opType == UserState.OperationType.queryForBackup)
{
if (e.Feed != null)
{
this.photoFeed = e.Feed as PicasaFeed;
this.InitializeList(ut.filename);
}
}
</pre></code>
If the operationtype is OperationType.query, we check if the arguments feed property is not null, if that is the case, we got a feed back, and can initialize our listbox with that feed. Pretty simple, even in the case of several operations at once.
<p>
For the case that downloads a picture, this is not much different:
<code><pre>
if (ut.opType == UserState.OperationType.download ||
ut.opType == UserState.OperationType.downloadList)
{
if (e.ResponseStream != null)
{
WriteFile(ut.filename, e.ResponseStream);
this.FileInfo.Text = "Saved file: " + ut.filename;
}
}
</pre></code>
We check the operationtype, if it is a download operation, we get the response stream property and use this to write the data to a file.
<p>
Where it get's a little more complicated is the "Backup an album" functionallity. An album can have a lot of pictures, and just doing something like this:
<code><pre>
foreach (Entry e in album.Entries)
Service.QueryStreamAync(e.Uri, DateTime.MinValue, myObject);
</pre></code>
would create one new background thread per picture in the album, something that can tax even the more powerful machines. So what is implemented is a queue system. Whenever one picture is downloaded, we spawn the next download.
Therefore, for a backup job, we create a complete new photobrowser dialog, fill that dialog with the initial list of photos. Then we start downloading the first photo to the target folder, remove it from the dialog, and then spawn the download of the next photo. This is done in the following piece of code, also in the OnDone event handler:
<code><pre>
if (ut.opType == UserState.OperationType.downloadList)
{
// we need to create a new object for uniqueness
UserState u = new UserState();
u.counter = ut.counter + 1;
u.feed = ut.feed;
u.foldername = ut.foldername;
u.opType = UserState.OperationType.downloadList;
if (u.feed.Entries.Count > 0)
{
u.feed.Entries.RemoveAt(0);
this.PhotoList.Items.RemoveAt(0);
}
this.states.Add(u);
SaveAnotherPictureDelegate d = new SaveAnotherPictureDelegate(this.CreateAnotherSaveFile);
this.BeginInvoke(d, u);
}
</pre></code>
So, we check the operation type, if it is the OperationType.downloadList, we create a new, unique user object, increment a counter, add this object to the list of userobjects, remove the photo from the photolist, and spawn another delegate by invoking the CreateAnotherSaveFile() method of the photobrowser.
<code><pre>
private void CreateAnotherSaveFile(UserState us)
{
if (us.feed.Entries.Count > 0)
{
PicasaEntry p = us.feed.Entries[0] as PicasaEntry;
us.filename = us.foldername + "\\image" + us.counter.ToString() + ".jpg";
this.picasaService.QueryStreamAync(new Uri(p.Media.Content.Attributes["url"] as string), DateTime.MinValue, us);
}
else if (us.feed.Entries.Count == 0 && us.feed.NextChunk != null)
{
}
else
{
this.Close();
}
}
</pre></code>
What happens here is: if in our user state object, we have a feed with entries left in it, we take the first entry, and create another background thread to query for that picture. And so the cycle continues.
The only unhandled case is chunking of the feed. If a feed of photos would arrive in chunks, we would need to handle it here to load the next set of photos. This is just a placeholder, as i do not believe picasa creates this situation.
When we are done backing up the album, the dialog closes itself.
</html>
|