Prevent static property being accessed before async function initializes it

Viewed 247

I have a function that asynchronously loads a xml file, parses it, and adds certain values to a list. I'm using async and await for this. The issue I've run into is that after calling await the program moves on to executing code that accesses that list before the async function has finished adding all items.

My static class with async function:

using System;
using System.Threading.Tasks;
using System.Collections.Generic;
using System.Xml.Linq;

using UnityEngine;
using UnityEngine.ResourceManagement.AsyncOperations;
using UnityEngine.AddressableAssets;

namespace Drok.Localization
{
    public static class Localization
    {
        /// <summary>
        /// The currently available languages.
        /// </summary>
        public static List<string> Available { get; private set; } = new List<string>();
        /// <summary>
        /// The currently selected language.
        /// </summary>
        public static string Current { get; private set; } = null;

        public static async Task Initialize()
        {
            await LoadMetaData();
        }

        private static async Task LoadMetaData()
        {
            AsyncOperationHandle<TextAsset> handle = Addressables.LoadAssetAsync<TextAsset>("Localization/meta.xml");
            TextAsset metaDataFile = await handle.Task;
            XDocument metaXMLData = XDocument.Parse(metaDataFile.text);
            IEnumerable<XElement> elements = metaXMLData.Element("LangMeta").Elements();
            foreach (XElement e in elements)
            {
                string lang = e.Attribute("lang").Value;
                int id = Int32.Parse(e.Attribute("id").Value);
                Debug.LogFormat("Language {0} is availible with id {1}.", lang, id);
                Available.Add(lang);
            }
        }

        public static void LoadLanguage(string lang)
        {
            Current = lang;
            throw new NotImplementedException();
        }

        public static string GetString(string key)
        {
            return key;
        }
    }
}

The class that initializes it and accesses the list:

using Drok.Localization;

using UnityEngine;

namespace Spellbound.Menu
{
    public class LanguageMenu : MonoBehaviour
    {
        private async void Awake()
        {
            await Localization.Initialize();
        }

        private void Start()
        {
            Debug.Log(Localization.Available.Count);
        }

        private void Update()
        {

        }
    }
}

I have no idea how to prevent access to that list until after all items have been added. The code I posted just collects info on what languages are available so that only the one language being used can be loaded later.

5 Answers

A Task<T> represents some value (of type T) that will be determined in the future. If you make your property this type, then it will force all callers to await for it to be loaded:

public static class Localization
{
  public static Task<List<string>> Available { get; private set; }

  static Localization() => Available = LoadMetaDataAsync();

  private static async Task<List<string>> LoadMetaDataAsync()
  {
    var results = new List<string>();
    ...
      results.Add(lang);
    return results;
  }
}

Usage:

private async Task StartAsync()
{
  var languages = await Localization.Available;
  Debug.Log(languages.Available.Count);
}

One possibility might be to add some logic to wait for the metadata to be loaded when returning the list from the get accessor.

One way to do this is to have a bool field that is set to true when the list is ready, and then we either return a private backing List<string> or null, depending on the value of our bool field:

public static class Localization
{
    private static bool metadataLoaded = false;
    private static List<string> available = new List<string>();

    // The 'Available' property returns null until the private list is ready
    public static List<string> Available => metadataLoaded ? available : null;

    private static async Task LoadMetaData()
    {
        // Add items to private 'available' list here

        // When the list is ready, set our field to 'true'
        metadataLoaded = true;
    }
}

The Awake method is async void, so there is no way for the caller to guarantee that it finishes before moving on to something else.

However, you could preserve the task and await it in the Start method to ensure that it is completed. Awaiting it twice does not harm anything.

public class LanguageMenu : MonoBehaviour
{
    private Task _task;

    private async void Awake()
    {
        _task = Localization.Initialize();
        await _task;
    }

    private async void Start()
    {
        await _task;
        Debug.Log(Localization.Available.Count);
    }

    private void Update()
    {

    }
}

Expanding on Rufus' comment:

Declare a bool property that's initialized to false. And in your list's getter, return the list only if the said bool property is true, and return maybe null (this depends on your requirements) if false.

public static bool IsAvailable { get; set; } = false;

private static List<string> _available;
public static List<string> Available
{
    get
    {
        if (IsAvailable)
            return _available;
        else
            return null;
    }
    set { _available = value; }
}

Finally, in your async function, when the work is done set the above property to true.

Latest when there is an Update method involved that should also wait with its execution using async and await might not be enough anyway.

Usually there is always one big alternative to using async for the Unity messages: an event system like e.g.

public static class Localization
{
    public static event Action OnLocalizationReady;

    public static async void Initialize()
    {
        await LoadMetaData();

        OnLocalizationReady?.Invoke();
    }

    ...
}

And wait for that event in any class using it like e.g.

public class LanguageMenu : MonoBehaviour
{
    private bool locaIsReady;

    private void Awake()
    {
        Localization.OnLocalizationReady -= OnLocalizationReady;
        Localization.OnLocalizationReady += OnLocalizationReady;

        Localization.Initialize();
    }

    private void OnDestroy ()
    {
        Localization.OnLocalizationReady -= OnLocalizationReady;
    }

    // This now replaces whatever you wanted to do in Start originally
    private void OnLocalizationReady ()
    {
        locaIsReady = true;

        Debug.Log(Localization.Available.Count);
    }

    private void Update()
    {
        // Block execution until locaIsReady
        if(!locaIsReady) return;

        ...
    }
}

Or for minimal better performance you could also set enabled = false in Awake and set it to true in OnLocalizationReady then you could get rid of the locaIsReady flag.


No async and await needed.


If you would move the Localization.Initialize(); instead to Start you would give other classes the chance to also add some callbacks before to Localization.OnLocalizationReady in Awake ;)


And you can extend this in multiple ways! You could e.g. together with firering the event directly also pass in the reference to Availables so listeners can directly use it like e.g.

public static class Localization
{
    public static event Action<List<string>> OnLocalizationReady;

    ...
}

and then in LanguageMenu change the signiture of OnLocalizationReady to

public class LanguageMenu : MonoBehaviour
{
    ...

    // This now replaces whatever you wanted to do in Start originally
    private void OnLocalizationReady(List<string> available)
    {
        locaIsReady = true;

        Debug.Log(available.Count);
    }
}

If anyway LanguageMenu will be the only listener then you could even pass the callback directly as parameter to Initialize like

public static class Localization
{
    public static async void Initialize(Action<List<string>> onSuccess)
    {
        await LoadMetaData();

        onSuccess?.Invoke();
    }

    ...
}

and then use it like

private void Awake()
{
    Localization.Initialize(OnLocalizationReady);
}

private void OnLocalizationReady(List<string>> available)
{
    locaIsReady = true;

    Debug.Log(available.Count);
}

or as lambda expression

private void Awake()
{
    Localization.Initialize(available => 
    {
        locaIsReady = true;

        Debug.Log(available .Count);
    }
}

Update

As to your question about later Initialization: Yes there is a simple fix as well

public static class Localization
{
    public static event Action OnLocalizationReady;

    public static bool isInitialized;

    public static async void Initialize()
    {
        await LoadMetaData();

        isInitialized = true;
        OnLocalizationReady?.Invoke();
    }

    ...
}

Then in other classes you can do it conditional either use callbacks or Initialize right away:

private void Awake()
{
    if(Localization.isInitialized)
    {
        OnLocaInitialized();
    }
    else
    {
        Localization.OnInitialized -= OnLocaInitialized;
        Localization.OnInitialized += OnLocaInitialized;
    }
}

private void OnDestroy ()
{
    Localization.OnInitialized -= OnLocaInitialized;
}

private void OnLocaInitialized()
{
    var available = Localization.Available;

    ...
}

private void Update()
{
    if(!Localization.isInitialized) return;

    ...
}
Related