简体   繁体   中英

What is the best way to fix “A method was called at an unexpected time” error

First of all, I've already seen this question , and I (kind of) understand why I'm getting this exception, I would like to know what is the best way to fix it. My code looks somewhat like this (this is a WinRT application):

//Here is my App constructor:
public App()
{
    this.InitializeComponent();
    this.Suspending += this.OnSuspending;

    //Initializing the model
    _model = new Model();
    _model.LoadData();
}

//the LoadData method looks like this:
public async void LoadData()
{
    StorageFolder folder = Windows.ApplicationModel.Package.Current.InstalledLocation;
    StorageFile file = await folder.GetFileAsync(@"Assets\Data.json");
    string data = await FileIO.ReadTextAsync(file);

    var dataList = JsonConvert.DeserializeObject<List<MyDataClass>>(data);
    // From time to time (pretty rarely, honestly) this line causes the
    // "A method was called at an unexpected time" thing:
    var dispatcher = CoreApplication.MainView.CoreWindow.Dispatcher;
    foreach (var item in dataList)
    {
        //do some stuff
        //<...>
        await dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal,
        () => {
            //do some stuff in the UI thread
        });                
    }
}

Obviously, having the LoadData method async void is not the best solution. However, as you can see, I have to do some async function calls (to read data from a file) inside of it. Right now I can think of two possible solutions:

  1. Change LoadData to public async Task LoadData() and change it's call in the application constructor to _model.LoadData().GetAwaiter().GetResult(); in order to run it synchronously;
  2. Change LoadData to public void LoadData() , and change all the await calls inside of it to use the awaiter, eg StorageFile file = folder.GetFileAsync(@"Assets\\Data.json").GetAwaiter().GetResult() .

Which of these is a better solution, or, better yet, is there any other proper way to run async code on the application startup? Also, why is the "A method was called at an unexpected time" error happening on the dispatcher line?

Which of these is a better solution

Both of the prospective solutions block - specifically, they block on I/O . Let's think about blocking for a bit, but this time instead of the app's perspective of "I need this data before I can display what I want", consider it from the runtime's perspective.

The runtime absolutely, positively should not ever block the UI. Blocking the UI blocks the user, and that is simply an unacceptable user experience. This is why the entire mobile world is async-first; this comes as a shock to many desktop developers. Desktop apps should be async-first, but mobile apps must be async-first.

So, from the runtime's perspective, when it starts an app, the app must show something. Immediately. Now. I/O is not an option. It doesn't have to be the perfect app home screen, but the app must show something synchronously.

This is why blocking is not going to work. The automated app store code analysis should reject both blocking approaches.

is there any other proper way to run async code on the application startup?

Yes, but the app must compromise. It cannot block the UI while it does its I/O before showing its pretty, full-formed first screen to the user. Some desktop apps can get away with this (even though they shouldn't either), but it's just not an option for mobile apps.

Instead, the app should (synchronously) load and show a "loading..." state. This can be a splash screen, or the actual main screen just with a "loading..." message or spinner in place of the data. This satisfies the runtime's requirements. Of course, before returning, the app should also start the asynchronous operation to retrieve the data, and later update the UI to show what it really wants to show (the regular view complete with the data).

This can be as simple as using an asynchronous event (as @marcinax commented):

protected override async void OnLaunched(...)
{
  ... // Initialize UI to "Loading" state.

  _model = new Model();
  await _model.LoadData();

  ... // Update UI with data in _model.
}

Personally, I prefer to keep all the UI code in the UI layer (not part of Model ), so if you have any explicit (non-data-bound) UI initialization to do with the data, it should go after // Update UI .

However, if you find yourself doing this several times in your app (eg, each window/page needs to load data), then you may prefer an asynchronous data-binding solution like the one described in my MSDN article.

Also, why is the "A method was called at an unexpected time" error happening on the dispatcher line?

I'm not entirely sure, but I suspect the dispatcher was not running yet.

One of the very nice side effects of keeping the UI code out of the model/VM layer is that explicit CoreDispatcher code is no longer necessary. There is always a better solution.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM