Listbox - inability to select the first element af

2019-09-01 01:57发布

问题:

I'm working on WP8.1 and I've found strage behaviour of my Listbox - to make it easier to reproduce I've put the code here. The problem occurs both on device and emulator.

I've a Listbox which is bound to ObservableCollection and it is filled with items upon button click:

public sealed partial class MainPage : Page
{
    List<string> populationOfItems = new List<string>();
    ObservableCollection<string> itemsToView = new ObservableCollection<string>();

    public MainPage()
    {
        this.InitializeComponent();

        second.Click += second_Click;
        myList.ItemsSource = itemsToView;
        // populate list of items to be copied
        for (int i = 0; i < 6; i++) populationOfItems.Add("Item " + i.ToString());
        BottomAppBar = new CommandBar();
    }

    private async void second_Click(object sender, RoutedEventArgs e)
    {
        itemsToView.Clear();
        await PopulateList();
    }

    private async Task PopulateList()
    {
        await Task.Delay(100); // without this line code seems to work ok
        itemsToView.Add("FIRSTELEMENT"); // add first differet element
        foreach (var item in populationOfItems)
            itemsToView.Add(item);
    }
}

The first time I fill the list, everthing is ok (pic. 1). But when I hit the button second time and, I can see elements not from first but from second (pic. 2). Ok - it is above, but I'm anable to scroll to it, when I hold my finger (mouse) I can scroll list and see that it exists, but when I stop scrolling the list hides (scrolls to second element) the first element. Also when you select any item - the list seems to look ok while you hold your finger (pic. 3), when you release it hides the first element again. When you move the list few time up/down, it repairs and works normal.

The way to reproduce the issue:

  • click the scond button once - fill the list
  • scroll list down, so that you hide the first elements (this is important)
  • hit the second button once more
  • try to scroll the list up and see first element, release finger

Pic.1

Pic.2

Pic.3

The problem seems to be concerned with asynchronous Task (that is why I also tagged this question asynchronous) - without the line await Task.Delay(100);, the code seems to work ok.

Does anybody have an idea what can be wrong?

EDIT - some other attempts

I've also tried running populating process via Dispatcher, without success - the problem exists.

I've also made an attempt to populate a temporary List (not ObservableCollection) and after returning from async Task, populate ObservableCollection - the problem persists.

List<string> temporaryList = new List<string>();
private async Task PopulateList()
{
    await Task.Delay(100); // without this line code seems to work ok
    temporaryList.Clear();
    temporaryList.Add("FIRSTELEMENT"); // add first differet element
    foreach (var item in populationOfItems)
        temporaryList.Add(item);
}

private async void second_Click(object sender, RoutedEventArgs e)
{
    itemsToView.Clear();
    await PopulateList();
    foreach (var item in temporaryList)
       itemsToView.Add(item);
}

Edit 2 - returnig List created in acync Task also doesn't help much:

private async void second_Click(object sender, RoutedEventArgs e)
{
    itemsToView.Clear();
    var items = await PopulateList();
    foreach (var item in items)
        itemsToView.Add(item);
}

private async Task<IEnumerable<string>> PopulateList()
{
    await Task.Delay(100); // without this line code seems to work ok
    List<string> temporaryList = new List<string>();
    temporaryList.Add("FIRSTELEMENT"); // add first differet element
    foreach (var item in populationOfItems)
        temporaryList.Add(item);
    return temporaryList;
}

EDIT 3 - as I've checked the same code run under Windows Phone 8.1 Silverlight works without problems.

回答1:

You should not mix UI handling with data retrieval because they are now happening concurrently.

Be also aware that when you call await PopulateList() the execution flow goes back to the UI thread and is ready to accept clicks and fire click events.

Try this instead:

private async void second_Click(object sender, RoutedEventArgs e)
{
    // UI thread
    var items = await PopulateListAsync(); // -> return to UI thread
    // back to UI thread
    itemsToView.Clear();
    itemsToView.Add("FIRSTELEMENT"); // add first differet element
    foreach (var item in items)
    {
        itemsToView.Add(item);
    }
}

private async Task<IEnumerable<string>> PopulateListAsync()
{
    // caller thread - UI thread
    await Task.Delay(100)
        .ConfigureAwait(continueOnCapturedContext: false);
    // some other thread
    return populationOfItems;
}

You might want to read this:

  • Asynchronous Programming with Async and Await (C# and Visual Basic)
  • Task-based Asynchronous Pattern (TAP)
  • Best Practices in Asynchronous Programming

EDIT:

I believe this demonstrates what you're trying to do. I've added a few more delays for you to see it happening on the phone.

MainPage.xaml

<phone:PhoneApplicationPage
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:phone="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone"
    xmlns:shell="clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:System="clr-namespace:System;assembly=mscorlib"
    x:Class="PhoneApp1.MainPage"
    mc:Ignorable="d"
    FontFamily="{StaticResource PhoneFontFamilyNormal}"
    FontSize="{StaticResource PhoneFontSizeNormal}"
    Foreground="{StaticResource PhoneForegroundBrush}"
    SupportedOrientations="Portrait" Orientation="Portrait"
    shell:SystemTray.IsVisible="True">

    <Grid x:Name="LayoutRoot" Background="Transparent">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>

        <StackPanel x:Name="TitlePanel" Grid.Row="0" Margin="12,17,0,28">
            <TextBlock Text="MY APPLICATION" Style="{StaticResource PhoneTextNormalStyle}" Margin="12,0"/>
            <TextBlock Text="page name" Margin="9,-7,0,0" Style="{StaticResource PhoneTextTitle1Style}"/>
        </StackPanel>

        <Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
            <Grid.RowDefinitions>
                <RowDefinition Height="*"/>
                <RowDefinition Height="Auto"/>
            </Grid.RowDefinitions>
            <phone:LongListSelector x:Name="List" HorizontalAlignment="Stretch" Margin="0" VerticalAlignment="Stretch" ItemsSource="{Binding}" LayoutMode="List">
                <phone:LongListSelector.ItemTemplate>
                    <DataTemplate>
                        <TextBlock Text="{Binding}" Style="{StaticResource PhoneTextExtraLargeStyle}" />
                    </DataTemplate>
                </phone:LongListSelector.ItemTemplate>
            </phone:LongListSelector>
            <Button Content="Button" Margin="0" Grid.Row="1" Click="Button_Click" x:Name="Button1"/>

        </Grid>
    </Grid>

</phone:PhoneApplicationPage>

MainPage.xaml.cs

using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Threading.Tasks;
using System.Windows;
using Microsoft.Phone.Controls;

namespace PhoneApp1
{
    public partial class MainPage : PhoneApplicationPage
    {
        private List<string> populationOfItems = new List<string>
        {
            "one",
            "two",
            "three",
            "four",
            "five"
        };
        private ObservableCollection<string> itemsToView = new ObservableCollection<string>();

        public MainPage()
        {
            InitializeComponent();
            this.DataContext = this.itemsToView;
        }

        private async void Button_Click(object sender, RoutedEventArgs e)
        {
            this.Button1.IsEnabled = false;
            var items = await PopulateListAsync();
            itemsToView.Clear();
            await Task.Delay(100);
            itemsToView.Add("FIRSTELEMENT");
            foreach (var item in items)
            {
                await Task.Delay(10);
                itemsToView.Add(item);
            }
            this.Button1.IsEnabled = true;
        }
        private async Task<IEnumerable<string>> PopulateListAsync()
        {
            await Task.Delay(100)
                .ConfigureAwait(continueOnCapturedContext: false);
            return populationOfItems;
        }
    }
}