Search code examples
windows-store-apps

Windows Run Time GetFileFromPathAsync Not Returning


I am hoping someone can shed some light on a problem I am having. I am writing a Windows Store app which on startup needs to copy a "seed" database from the App installation folder Windows.ApplicationModel.Package.Current.InstalledLocation.Path to the Local data folder Windows.Storage.ApplicationData.Current.LocalFolder. To do this I have used the following called from my OnNavigatedTo method in Main().

protected override void OnNavigatedTo(NavigationEventArgs e)
{
    //Calls  method in Dal Class which ensures database is in correct location and if not copies it
    Dal.LoadData();  

    RefreshScenarioList();
    ScenarioList.SelectedIndex = 0;  //this is the Default Item             
}

public static async void LoadData()
{
    await CopyIfNotExists(dbName);
}

private static async Task CopyIfNotExists(string dbName)
{
    if (await GetIfFileExistsAsync(dbName) == null)
    {

        StorageFile seedFile = await StorageFile.GetFileFromPathAsync(
        Path.Combine(Windows.ApplicationModel.Package.Current.InstalledLocation.Path,
        dbName));

        await seedFile.CopyAsync(Windows.Storage.ApplicationData.Current.LocalFolder);
    }
}

private static async Task<StorageFile> GetIfFileExistsAsync(string key)
{
    try
    {
        return await ApplicationData.Current.LocalFolder.GetFileAsync(key);

    }
    catch (FileNotFoundException) { return default(StorageFile); }
}

Please also note that I have also used GetFileAsync and also tried using a uri to obtain the seed file (to see if there was any difference). With the combination here I seem to be having the highest success rate. This seems to work 2-3 times out of every 4 tries but I would be much more comfortable with something that works 100% of the time. It only needs to copy the database once on startup.

When it doesn't work it "seems" to never return (ie CopyIfNotExists never seems to complete). The issue appears to be in the CopyIfNotExists method. The problem is that virtually the next line of code in my OnNavigatedTo method is a query on the database and it falls over when the database doesn't copy.

Any insights that anyone may have would be greatly appreciated.


Solution

  • From what I can see, the problem here is that your use of async and await does not go as far up the call stack as it can.

    When writing async code, there are a couple of general rules to follow -

    1. If a function Foo uses the async keyword, then any other function that calls Foo must also use the async keyword.
    2. Never use async void in a function signature - use async Task instead. The exceptions are:
      • your function is an event handler (e.g. button press)
      • or you are overriding a method that happens to return void

    Let's take a look at the method signatures on your call stack. OnNavigatedTo is shown here at the top:

          override void              OnNavigatedTo()
    async          void              LoadData()
    async          Task              CopyIfNotExists()
    async          Task<StorageFile> GetIfFileExistsAsync()
    

    The bottom two methods of the call stack uses async Task in the method signatures. Above those on the call stack is LoadData, which follows rule 1 above, but breaks rule 2. LoadData should be changed to this:

    public static async Task LoadData()
    {
        await CopyIfNotExists(dbName);
    }
    

    Next we try to make OnNavigatedTo follow the above rules:

    async protected override void OnNavigatedTo(NavigationEventArgs e)
    

    Note that we cannot use async Task, since your method is an override. We have to stick to async void.

    Now you just need to add await to your LoadData call since it is an async method. We do this so that your program waits for LoadData to complete prior to calling RefreshScenarioList:

    async protected override void OnNavigatedTo(NavigationEventArgs e)
    {
        //Calls  method in Dal Class which ensures database is in correct location and if not copies it
        await Dal.LoadData();  
    
        RefreshScenarioList();
        ScenarioList.SelectedIndex = 0;  //this is the Default Item             
    }
    

    Now you will want to also ensure that the call stack for RefreshScenarioList follows the async rules above.


    One more thing that I noticed with your code. In Windows 8.1 or later, you should use TryGetItemAsync instead of GetFileAsync:

    private static async Task<StorageFile> GetIfFileExistsAsync(string key)
    {
        return await ApplicationData.Current.LocalFolder.TryGetItemAsync(key) as StorageFile;
    }
    

    The new method returns null when it cannot find a file. The old method throws an exception. Throwing exceptions on a UI thread can cause your UI to stall, so it is advantageous to use the new method.