Search code examples
c#asynchronousdata-bindinguwp

x:Bind data not showing up for async / await loaded data set in UWP app


In my project, I recently made a change in how I was loading data in my application (UWP app) and in the process it has broken data binding (what was previously showing up in the bound list is now showing up as empty) but I am not fully understanding why. I have seen a bunch of posts here that discuss async data binding and I thought that I had followed everything fine.

I had previously been loading a json file using File.OpenRead() but in order to fix a different issue I was running into, I changed it over to using the StorageFile stream. Because I was now using await StorageFile.CreateFileAsync(), I saw that my OnNavigatedTo() was now running the next line of code before my object creation was completed. So I tried to convert it over and make it await the async object, which works fine in terms of timing except my bindings no longer show the data (setting the breakpoint does show that the ObservableCollection has data).

I think I boiled it down to just the relative code pieces here:

MainPage.xaml:

<ListView x:Name="lstAudioFX" ItemsSource="{x:Bind AudioFXModel.FXList}">
     <ListView.ItemTemplate>
          <DataTemplate x:DataType="models:CheckBoxData">
               <StackPanel Orientation="Horizontal" HorizontalAlignment="Left">
                    <CheckBox Content="{x:Bind Name}" IsChecked="{x:Bind Selected, Mode=TwoWay}"/>
               </StackPanel>
          </DataTemplate>
      </ListView.ItemTemplate>
</ListView>

MainPage.xaml.cs:

public AudioFXViewModel AudioFXModel;

protected async override void OnNavigatedTo(NavigationEventArgs e)
{
    // Originally was just AudioFXViewModel.Create(
    AudioFXModel = await AudioFXViewModel.CreateAsync();

    // Breakpoint here was initially hitting before Create() finished prior to modifying to await / async
    if (AudioFXModel.NeedsProcessing)
    {
        ...
    }

    base.OnNavigatedTo(e);
}

AudioFXViewModel.cs

public class CheckBoxData
{
    public string Name { get; set; }
    public bool Selected { get; set; }
}

public class AudioFXViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    public void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(propertyName)));
    }

    private bool _fNeedsProcessing = false;
    public bool NeedsProcessing { get => _fNeedsProcessing; }

    private ObservableCollection<CheckBoxData> _FXList;
    public ObservableCollection<CheckBoxData> FXList { 
        get => this._FXList; 

        set
        {
            _FXList = value;
            this.OnPropertyChanged("FXList");
        } 
    }

    private AudioFXViewModel(){}

    // Originally public static AudioFXViewModel Create()
    public static async Task<AudioFXViewModel> CreateAsync()
    {
        AudioFXViewModel audioFXViewModel = new AudioFXViewModel();

        // Originally audioFXViewModel.LoadFXContent();
        await audioFXViewModel.LoadFXContentAsync();        

        return audioFXViewModel;
    }

    // Originally private void LoadFXContent()
    private async Task LoadFXContentAsync()
    {
        // Originally using File.OpenText()
        StorageFile jsonFile = await StorageFile.GetFileFromApplicationUriAsync(new Uri($"ms-appx:///Data/Settings.json"));

        using (StreamReader sr = new StreamReader(await jsonFile.OpenStreamForReadAsync()))
        {
            var json = sr.ReadToEnd();

            // The relevant info here is that FXData has a serialized list (FXList) of applicable FX
            var data = JsonConvert.DeserializeObject<FXData>(json);

            ObservableCollection<CheckBoxData> FXs = new ObservableCollection<CheckBoxData>();
            foreach (string s in data.FXList)
            {
                FXs.Add(new CheckBoxData() {Name = s, Selected = true });
            }

            FXList = new ObservableCollection<CheckBoxData>(FXs);

            _fNeedsProcessing = true;
        }
    }
}

I hadn't previously needed to call the OnPropertyChanged when I set the ObservableCollection, but even that doesn't seem to make a difference. If I remove the await from AudioFXModel = await AudioFXViewModel.CreateAsync();, then the data will bind / display fine, except I am back at my original issue where NeedsProcessing hasn't been set yet because it didn't wait.

** UPDATE ** So in doing some additional debugging here, it seems that the call to GetFileFromApplicationUriAsync() is what is causing the scenario to break


Solution

  • I agree with the above comments that you should be setting the Mode=OneWay attribute in your markup:

    ItemsSource="{x:Bind AudioFXModel.FXList, Mode=OneWay}"
    

    Because of the async nature of your calls, you are potentially running into a timing issue where x:Bind is trying to bind against the property that doesn't exist yet and likely is not getting notified when it does get created. If your MainPage implements INotifyPropertyChanged as well and raises the OnPropertyChanged event for the AudioFXModel it should resolve the problem.

    So add something like this to your MainPage.xaml.cs:

    public sealed partial class MainPage : Page, INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;
        public void OnPropertyChanged([CallerMemberName] string propertyName = null)
        {
            this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    
        private AudioFXViewModel _audioFXModel;
        private AudioFXViewModel AudioFXModel
        {
            get
            {
                return _audioFXModel;
            }
            set
            {
                _audioFXModel = value;
                OnPropertyChanged();
            }
        }
    // REST OF YOUR CODE HERE
    }
    

    Note too that the way your PropertyChangedEventArgs are created, they are always going to pass "propertyName" instead of the name of the property that is actually making the call. You will want to remove the nameof() so it looks like above.