Search code examples
c#asynchronouswebclientasync-ctp

UserState using WebClient and TaskAsync download from Async CTP


I'm currently working with the Async CTP and need to convert this code into code where I can use Task.WhenAll().

What I did until now was using the UserState object and put my identifier (AID) into it and then use it in the completed event.

However the wc.DownloadFileTaskAsync methode doesn't have an overload with UserState. What can I do?

for (int i = 0; i < SortedRecommendations.Count; i++)
{
    string tempfilepath = filepath + SortedRecommendations[i].Aid + ".jpg";

    if (File.Exists(tempfilepath))
        continue;

    WebClient wc = new WebClient();
    wc.DownloadFileCompleted += (s, e) =>
        {
            var q = SortedRecommendations.Where(x => x.Aid == (int)e.UserState);
            if (q.Count() > 0)
                q.First().Image = tempfilepath;
        };
    wc.DownloadFileAsync(new Uri(SortedRecommendations[i].Image.Replace("t.jpg", ".jpg")), tempfilepath, SortedRecommendations[i].Aid);
}

This is basically with what I came up with, however I'm getting a out ouf bounds exception at y.Aid == SortedRecommendations[i].Aid because i is now obvioulsy something else then it was when the download started. Only other possibility I see is using something like TaskEx.Run( () => { // download data synchronously }; but I don't like this approach.

for (int i = 0; i < SortedRecommendations.Count; i++)
{
    string tempfilepath = filepath + SortedRecommendations[i].Aid + ".jpg";

    if (File.Exists(tempfilepath))
        continue;

    WebClient wc = new WebClient();
    wc.DownloadFileCompleted += (s, e) =>
    {
        var q = SortedRecommendations.Where(x => x.Aid == SortedRecommendations[i].Aid);
        if (q.Count() > 0)
            q.First().Image = tempfilepath;

    };
    tasks.Add(wc.DownloadFileTaskAsync(new Uri(SortedRecommendations[i].Image.Replace("t.jpg", ".jpg")), tempfilepath));
}

await TaskEx.WhenAll(tasks);
//Everything finished

Solution

  • First, I think you shouldn't base your logic on ids (unless you really have to). You should use references to the objects in the SortedRecommendations collection.

    Now, if you wanted to download only one file at a time, you could simply use await:

    for (int i = 0; i < SortedRecommendations.Count; i++)
    {
        string tempfilepath = filepath + SortedRecommendations[i].Aid + ".jpg";
    
        if (File.Exists(tempfilepath))
            continue;
    
        WebClient wc = new WebClient();
        var recommendation = SortedRecommendations[i];
        await wc.DownloadFileTaskAsync(new Uri(recommendation.Image.Replace("t.jpg", ".jpg")), tempfilepath);
        recommendation.Image = tempfilepath;
    }
    

    But, if you wanted to start all of the downloads at the same time, like your DownloadFileAsync() code does, you could use ContinueWith() instead. And you don't need user state, that's what closures are for:

    for (int i = 0; i < SortedRecommendations.Count; i++)
    {
        string tempfilepath = filepath + SortedRecommendations[i].Aid + ".jpg";
    
        if (File.Exists(tempfilepath))
            continue;
    
        WebClient wc = new WebClient();
        var recommendation = SortedRecommendations[i];
        var downloadTask = wc.DownloadFileTaskAsync(new Uri(recommendation.Image.Replace("t.jpg", ".jpg")), tempfilepath);
        var continuation = downloadTask.ContinueWith(t => recommendation.Image = tempfilepath);
        tasks.Add(continuation);
    }
    
    await Task.WhenAll(tasks);
    

    The best solution would probably be to download a limited number of files at once, not one or all of them. Doing that is more complicated and one solution would be to use ActionBlock from TPL Dataflow with MaxDegreeOfParallelism set.