0
votes

I have a ListView that is bound to an ItemsSource of Album instances. Because there can be many (>2,000) Album instances, I use a (horizontally scrolling) VirtualizingStackPanel as the ItemPanel:

<ListView.ItemsPanel>
    <ItemsPanelTemplate>
        <VirtualizingStackPanel Orientation="Horizontal" />
    </ItemsPanelTemplate>
</ListView.ItemsPanel>

As ItemTemplate, I use a custom UserControl, which is bound to the Albums in the ItemsSource:

<DataTemplate x:Name="albumItem" x:DataType="local2:Album">
    <local3:AlbumControl x:Name="albumControl" Album="{x:Bind}" />
</DataTemplate>

The DependencyProperty Album on the AlbumControl, carries out some actions when an new binding occurs:

/// <summary>
/// Gets or sets the Album assigned to the control.
/// </summary>
public Album Album
{
    get { return (Album)GetValue(AlbumProperty); }
    set { SetValue(AlbumProperty, value); }
}

/// <summary>
/// Identifies the Album dependency property.
/// </summary>
public static readonly DependencyProperty AlbumProperty = DependencyProperty.Register(nameof(Album), typeof(Album), typeof(AlbumControl), new PropertyMetadata(null, HandleAlbumChange));

private static async void HandleAlbumChange(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
    if (d is AlbumControl control)
    {
        if (e.NewValue is Album album)
        {
            control.DoStuff(album);
            await control.DoStuffAsync(album);
            control.DoMoreStuff(album);
        }
    }
}

Because the ItemsPanel is virtualized, during scrolling, it is possible that a new Album is bound to the AlbumControl after DoStuff() and before the DoStuffAsync() is finished. This causes problems.

I assume I need:

  • some kind of locking (DoStuff() contains a lock, but you cannot lock async calls), or
  • some kind of cancelling processing the old Album and just process the new Album, or
  • maybe it goes wrong at the awaited call, or
  • maybe my setup is entirely wrong.

The main question is: How to handle binding events that occur quicker than processing the binding?

========================================

In response to a comment, I'll add some more details of what I observed.

control.DoStuff(album) cleared an ObservableCollection-property of the AlbumControl. The AlbumControl shows this collection in a ListView.

await control.DoStuffAsync(album) was used to set the Source of an Image-control in the AlbumControl. An Album has the information for this source as byte[]. I need to convert this to a RandomAccessStream in order to be able to call await bmp.SetSourceAsync(stream) and then I can use bmp (a BitmapImage) as Source. The byte[] are not backed by a file on disk, so I cannot use a Uri to set the source of the Image-control.

After this awaited call, I repopulated the ObservableCollection (control.DoMoreStuff()). If I did it this way, the collection of the last AlbumControl contained listitems from multiple Albums. Indicating that during the awaited call a new Album was set. When the thread returns after the async of the first Album ends, it repopulates the collection, next the thread for the second Album returns and it also repopulates the collection. Resulting in listitems for both Albums to be in the collection. I "solved" this by moving the Clear() after the async method call, right before the repopulation step. But I was still wondering if I did something dubious.

I couldn't find a way to precompute the BitmapImage, so that I wouldn't need the async call. I kept getting the "Marshalled for a different thread"-errors meaning that I needed to create the RandomAccessStream on the UI thread.

=========================================================

Demo code available at https://antamista.visualstudio.com/_git/TestAlbumControl

2
Hi, Do you mean that the last DoStuffAsync method is not completed, the control have to start a new asynchronous task, which may cause data interference? Has it actually happened? If this is the case, you might consider performing asynchronous operations for everything before data binding, and loading the data in advance. In addition, the binding is not affected by the internal processing method of the control, and we cannot directly interfere with the binding process. - Richard Zhang - MSFT
Sadly, I don't understand your comment entirely. I understand that you mean that I need to precompute the data produced by my asynchronous call instead of doing it in the HandleAlbumChange-handler. Maybe I should try harder for a way to do that. Nevertheless, I added more details and thanks for your comment. - vvasch
Maybe you can make a judgment first when filling the list items, if the current item is in the collection, skip it, otherwise add it, so you don’t have to care about the order. It is not clear how your data structures and methods are executed. If you can provide a minimal runnable demo, this will help us analyze your problem. - Richard Zhang - MSFT
Ok, it took some time the get the demo, but the source code should be publicly available here: antamista.visualstudio.com/TestAlbumControl If you clone the code and build the solution, you should be able to see the issue in action. - vvasch

2 Answers

1
votes

I think we can start to change this in two ways:

  1. Let the Album and Song classes inherit the INotifyPropertyChanged interface to respond to data changes in a timely manner.
public class Album : List<Song>,INotifyPropertyChanged
{
    // ...

    private string _title;
    /// <summary>
    /// The title of Album
    /// </summary>
    public string Title
    {
        get => _title;
        set
        {
            _title = value;
            OnPropertyChanged();
        }
    }

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

    private string _title;
    /// <summary>
    /// The song title
    /// </summary>
    public string Title
    {
        get => _title;
        set
        {
            _title = value;
            OnPropertyChanged();
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;
    public void OnPropertyChanged([CallerMemberName]string propertyName = "")
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}
  1. Block multiple assignments inside AlbumControl by identifier.
private bool _isLoading = false;

private static async void HandleAlbumChange(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
    if (d is AlbumControl control)
    {
        if (control._isLoading)
            return;
        if (e.NewValue is Album album)
        {
            control._isLoading = true;
            // handle code
            control._isLoading = false;
        }
        else
        {
            control.Songs.Clear();
            control.albumSleeve.Source = null;
        }
    }
}

DataTemplate

<DataTemplate x:DataType="local:Song">
    <StackPanel Orientation="Horizontal">
        <TextBlock Text="{x:Bind Track}" FontSize="24" Margin="8,0,16,0" VerticalAlignment="Center"/>
        <StackPanel>
            <TextBlock Text="{x:Bind Title,Mode=OneWay}" FontWeight="SemiBold" TextWrapping="Wrap"/>
            <TextBlock Text="{x:Bind Album}" FontSize="12" TextWrapping="Wrap"/>
        </StackPanel>
    </StackPanel>
</DataTemplate>

Thanks.

0
votes

The issue seems to be that I use the album that is boxed in e.NewValue of the DependencyPropertyChangedEventArgs e:

if (e.NewValue is Album album)
   {
       control.DoStuff(album);
       await control.DoStuffAsync(album);
       control.DoMoreStuff(album);
   }

If I use control.Album the issues no longer show up. Apparently, when using the DependencyPropertyChangedEventArgs the synchorisation is off. Changing the code to:

control.DoStuff(control.Album);
await control.DoStuffAsync(control.Album);
control.DoMoreStuff(control.Album);

solves the synchronisation issues for the Album/Songs combination.

If you go to the demo code, you see that await control.DoStuffAsync(album) sets an image on the control. Sadly, the synchronisation between album/songs and image is still off. I'll leave it for now, since this is not explained in the original question. Still, it seems that using an async method inside the DependencyProperty eventhandler, combined with VirtualizingStackPanel is not a good idea.