首页 文章

Xamarin表单ListView SelectedItem绑定问题

提问于
浏览
0

ListView遵循UI控件的ItemPicker / Selector模式 . 一般来说,这些类型的控件,无论其平台如何,都将具有SelectedItem和ItemsSource . 基本思想是ItemsSource中有一个项目列表,SelectedItem可以设置为其中一个项目 . 其他一些例子是ComboBoxes(Silverlight / UWP / WPF)和Pickers(Xamarin Forms) .

在某些情况下,这些控件是异步准备的,在其他情况下,需要编写代码以处理ItemsSource的填充晚于SelectedItem的情况 . 在我们的例子中,大多数情况下,BindingContext(包含绑定到SelectedItem的属性)将在ItemsSource之前设置 . 因此,我们需要编写代码以使其正常运行 . 例如,我们在Silverlight中为ComboBoxes做过这个 .

在Xamarin Forms中,ListView控件不是异步准备好的,即如果在设置SelectedItem之前未填充ItemsSource,则所选项目将永远不会在控件上突出显示 . 这可能是设计的,这没关系 . The point of this thread is to find a way make the ListView async ready so that the ItemsSource can be populated after the SelectedItem is set.

应该有可以在其他平台上实现的直接解决方案来实现这一点,但是Xamarin Forms列表视图中存在一些错误,使得它似乎无法解决该问题 . 我创建的示例应用程序在WPF和Xamarin Forms之间共享,以显示ListView在每个平台上的行为方式 . 例如,WPF ListView是异步准备好的 . 如果在WPF ListView上设置DataContext后填充ItemsSource,则SelectedItem将绑定到列表中的项目 .

在Xamarin Forms中,我不能始终如一地在ListView上使用SelectedItem双向绑定来工作 . 如果我在ListView中选择一个项目,它会在我的模型上设置属性,但是如果我在模型上设置属性,则应该选择的项目不会反映为在ListView中被选中 . 加载项目后,当我在模型上设置属性时,不会显示SelectedItem . 这种情况发生在UWP和Android上 . iOS仍未经过测试 .

您可以在此Git仓库中查看示例问题:https://ChristianFindlay@bitbucket.org/ChristianFindlay/xamarin-forms-scratch.git . 只需运行UWP或Android示例,然后单击Async ListView . 您还可以运行XamarinFormsWPFComparison示例以查看WPF版本的行为方式 .

当您运行Xamarin Forms示例时,您将看到在加载项目后没有选择任何项目 . 但是在WPF版本中,它被选中 . 注意:它没有突出显示为蓝色,但略呈灰色,表示已选中 . 这是我的问题所在,也是我无法解决异步问题的原因 .

这是我的代码( clone repo for absolute latest code ):

public class AsyncListViewModel : INotifyPropertyChanged
{
    #region Fields
    private ItemModel _ItemModel;
    #endregion

    #region Events
    public event PropertyChangedEventHandler PropertyChanged;
    #endregion

    #region Public Properties
    public ItemModel ItemModel
    {
        get
        {
            return _ItemModel;
        }

        set
        {
            _ItemModel = value;
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ItemModel)));
        }
    }
    #endregion
}


public class ItemModel : INotifyPropertyChanged
{
    #region Fields
    private int _Name;
    private string _Description;
    #endregion

    #region Events
    public event PropertyChangedEventHandler PropertyChanged;
    #endregion

    #region Public Properties
    public int Name
    {
        get
        {
            return _Name;
        }

        set
        {
            _Name = value;
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Name)));
        }
    }

    public string Description
    {
        get
        {
            return _Description;
        }

        set
        {
            _Description = value;
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Description)));
        }
    }
    #endregion

    #region Public Methods
    public override bool Equals(object obj)
    {
        var itemModel = obj as ItemModel;
        if (itemModel == null)
        {
            return false;
        }

        var returnValue = Name.Equals(itemModel.Name);

        Debug.WriteLine($"An {nameof(ItemModel)} was tested for equality. Equal: {returnValue}");

        return returnValue;
    }

    public override int GetHashCode()
    {
        Debug.WriteLine($"{nameof(GetHashCode)} was called on an {nameof(ItemModel)}");
        return Name;
    }

    #endregion
}

public class ItemModelProvider : ObservableCollection<ItemModel>
{
    #region Events
    public event EventHandler ItemsLoaded;
    #endregion

    #region Constructor
    public ItemModelProvider()
    {
        var timer = new Timer(TimerCallback, null, 3000, 0);
    }
    #endregion

    #region Private Methods
    private void TimerCallback(object state)
    {
        Device.BeginInvokeOnMainThread(() => 
        {
            Add(new ItemModel { Name = 1, Description = "First" });
            Add(new ItemModel { Name = 2, Description = "Second" });
            Add(new ItemModel { Name = 3, Description = "Third" });
            ItemsLoaded?.Invoke(this, new EventArgs());
        });
    }
    #endregion
}

这是XAML:

<Grid x:Name="TheGrid">

        <Grid.Resources>
            <ResourceDictionary>
                <local:ItemModelProvider x:Key="items" />
            </ResourceDictionary>
        </Grid.Resources>

        <Grid.RowDefinitions>
            <RowDefinition />
            <RowDefinition Height="100" />
        </Grid.RowDefinitions>

        <ListView x:Name="TheListView" Margin="4" SelectedItem="{Binding ItemModel, Mode=TwoWay}" ItemsSource="{StaticResource items}" HorizontalOptions="Center" VerticalOptions="Center" BackgroundColor="#EEEEEE" >

            <ListView.ItemTemplate>
                <DataTemplate>

                    <ViewCell>

                        <Grid >

                            <Grid.RowDefinitions>
                                <RowDefinition Height="20" />
                                <RowDefinition Height="20" />
                            </Grid.RowDefinitions>

                            <Label Text="{Binding Name}" TextColor="#FF0000EE" VerticalOptions="Center"  />
                            <Label Text="{Binding Description}" Grid.Row="1"  VerticalOptions="Center" />

                        </Grid>


                    </ViewCell>

                </DataTemplate>
            </ListView.ItemTemplate>

        </ListView>

        <ActivityIndicator x:Name="TheActivityIndicator" IsRunning="True" IsVisible="True" Margin="100" />

        <StackLayout Grid.Row="1" Orientation="Horizontal">
            <Label Text="Name: " />
            <Label Text="{Binding ItemModel.Name}" />
            <Label Text="Description: " />
            <Label Text="{Binding ItemModel.Description}" />
            <Button Text="New Model" x:Name="NewModelButton" />
            <Button Text="Set To 2" x:Name="SetToTwoButton" />
        </StackLayout>

    </Grid>

代码背后:

public partial class AsyncListViewPage : ContentPage
{
    ItemModelProvider items;
    ItemModel two;

    private AsyncListViewModel CurrentAsyncListViewModel => BindingContext as AsyncListViewModel;

    public AsyncListViewPage()
    {
        InitializeComponent();

        CreateNewModel();

        items = (ItemModelProvider)TheGrid.Resources["items"];
        items.ItemsLoaded += Items_ItemsLoaded;

        NewModelButton.Clicked += NewModelButton_Clicked;
        SetToTwoButton.Clicked += SetToTwoButton_Clicked;
    }

    private void SetToTwoButton_Clicked(object sender, System.EventArgs e)
    {
        if (two == null)
        {
            DisplayAlert("Wait for the items to load", "Wait for the items to load", "OK");
            return;
        }

        CurrentAsyncListViewModel.ItemModel = two;
    }

    private void NewModelButton_Clicked(object sender, System.EventArgs e)
    {
        CreateNewModel();
    }

    private void CreateNewModel()
    {
        //Note: if you replace the line below with this, the behaviour works:
        //BindingContext = new AsyncListViewModel { ItemModel = two };

        BindingContext = new AsyncListViewModel { ItemModel = GetNewTwo() };
    }

    private static ItemModel GetNewTwo()
    {
        return new ItemModel { Name = 2, Description = "Second" };
    }

    private void Items_ItemsLoaded(object sender, System.EventArgs e)
    {
        TheActivityIndicator.IsRunning = false;
        TheActivityIndicator.IsVisible = false;
        two = items[1];
    }
}

注意:如果我将方法CreateNewModel更改为:

private void CreateNewModel()
    {
        BindingContext = new AsyncListViewModel { ItemModel = two };
    }

SelectedItem反映在屏幕上 . 这似乎表明ListView正在基于对象引用比较项目,而不是在对象上使用Equals方法 . 我倾向于认为这是一个错误 . 但是,这不是唯一的问题,因为如果这是唯一的问题,那么单击SetToTwoButton应该会产生相同的结果 .

It is now clear that there are several bugs around this is Xamarin Forms. I have documented the repro steps here: https://bugzilla.xamarin.com/show_bug.cgi?id=58451

2 回答

  • 0

    AdaptListView是ListView控件的合适替代方案,不受这些问题的影响 .

  • 0

    Xamarin Forms团队创建了一个pull请求来解决这里的一些问题:https://github.com/xamarin/Xamarin.Forms/pull/1152

    但是,我不相信这个拉取请求曾经被Xamarin Forms的主分支所接受 .

相关问题