0

I'm downloading two JSON files from the webs, after which I want to allow loading two pages, but not before. However, the ManualResetEvent that is required to be set in order to load the page never "fires". Even though I know that it gets set, WaitOne never returns.

Method that launches the downloads:

private void Application_Launching(object sender, LaunchingEventArgs e)
{
    PhoneApplicationService.Current.State["doneList"] = new List<int>();
    PhoneApplicationService.Current.State["manualResetEvent"] = new ManualResetEvent(false);

    Helpers.DownloadAndStoreJsonObject<ArticleList>("http://arkad.tlth.se/api/get_posts/", "articleList");
    Helpers.DownloadAndStoreJsonObject<CompanyList>("http://arkad.tlth.se/api/get_posts/?postType=webbkatalog", "catalog");
}

The downloading method, that sets the ManualResetEvent

public static void DownloadAndStoreJsonObject<T>(string url, string objName)
{
    var webClient = new WebClient();
    webClient.DownloadStringCompleted += (sender, e) => 
    {
        if (!string.IsNullOrEmpty(e.Result))
        {
            var obj = ProcessJson<T>(e.Result);
            PhoneApplicationService.Current.State[objName] = obj;


            var doneList = PhoneApplicationService.Current.State["doneList"] as List<int>;
            doneList.Add(0);

            if (doneList.Count == 2)    // Two items loaded
            {
                (PhoneApplicationService.Current.State["manualResetEvent"] as ManualResetEvent).Set();  // Signal that it's done
            }
        }
    };

    webClient.DownloadStringAsync(new Uri(url));
}

The waiting method (constructor in this case)

public SenastePage()
{
    InitializeComponent();

    if ((PhoneApplicationService.Current.State["doneList"] as List<int>).Count < 2)
    {
        (PhoneApplicationService.Current.State["manualResetEvent"] as ManualResetEvent).WaitOne();
    }
    SenasteArticleList.ItemsSource =  (PhoneApplicationService.Current.State["articleList"] as ArticleList).posts;
}

If I wait before trying to access that constructor, it easily passes the if-statement and doesn't get caught in the WaitOne, but if I call it immediately, I get stuck, and it never returns...

Any ideas?

Soner Gönül
  • 97,193
  • 102
  • 206
  • 364
SamiHuutoniemi
  • 1,576
  • 2
  • 16
  • 35
  • 2
    That's a deadlock. The `DownloadStringCompleted` event of the WebClient is executed in the UI thread, the very same thread that you are blocking with the `WaitOne`. In any case, you should avoid blocking the UI thread at all cost. It's better to display a loading animation while you're waiting for the data. – Kevin Gosse Oct 20 '13 at 18:45
  • Are you able to drop into a debugger while it is in this stuck state and inspect the doneList.Count and manualResetEvent variables to see what they are set to at the time? – Micah Zoltu Oct 20 '13 at 18:55
  • @KooKiz: Dispaying a splash screen or not, at some point I must know when both downloads have completed. Do I not still need to have a ManualResetEvent? – SamiHuutoniemi Oct 20 '13 at 18:57
  • @MicahCaldwell: I can. – SamiHuutoniemi Oct 20 '13 at 18:58
  • @SamiHuutoniemi Display the loading animation, then notify the UI with a callback function when the downloads are done. You can even use tasks for this kind of scenario. – Kevin Gosse Oct 20 '13 at 18:59
  • Also, note that you forgot to handle error cases. If one the download fails, the `DownloadStringCompleted` won't be triggered, and the wait event will never be set. – Kevin Gosse Oct 20 '13 at 19:09

2 Answers2

1

Blocking the UI thread must be prevented at all costs. Especially when downloading data: don't forget that your application is executing on a phone, which has a very instable network. If the data takes two minutes to load, then the UI will be freezed for two minutes. It would be an awful user experience.

There's many ways to prevent that. For instance, you can keep the same logic but waiting in a background thread instead of the UI thread:

public SenastePage()
{
    // Write the XAML of your page to display the loading animation per default
    InitializeComponent();

    Task.Factory.StartNew(LoadData);
}

private void LoadData()
{
    ((ManualResetEvent)PhoneApplicationService.Current.State["manualResetEvent"]).WaitOne();

    Dispatcher.BeginInvoke(() =>
    {
        SenasteArticleList.ItemsSource = ((ArticleList)PhoneApplicationService.Current.State["articleList"]).posts;

        // Hide the loading animation
    }
}

That's just a quick and dirty way to reach the result you want. You could also rewrite your code using tasks, and using Task.WhenAll to trigger an action when they're all finished.

Kevin Gosse
  • 38,392
  • 3
  • 78
  • 94
0

Perhaps there is a logic problem. In the SenastePage() constructor you are waiting for the set event only if the doneList count is less than two. However, you don't fire the set event until the doneList count is equal to two. You are listening for the set event before it can ever fire.

Phillip Ngan
  • 15,482
  • 8
  • 63
  • 79
  • Isn't that the point? If the count is 2 or more, we are done downloading, and I don't have to Wait. If it's less than 2, I begin waiting. – SamiHuutoniemi Oct 20 '13 at 18:59