Xamarin表单ListView SelectedItem绑定问题

时间:2017-07-28 02:52:26

标签: c# android listview uwp xamarin.forms

ListViews遵循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,则所选项目将永远不会在控件上突出显示。这可能是设计的,这是可以的。 此线程的目的是找到一种方法使ListView异步准备就绪,以便在设置SelectedItem之后填充ItemsSource。

应该有可以在其他平台上实现的直接解决方案来实现这一点,但是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版本中,它被选中。注意:它没有突出显示为蓝色,但略呈灰色,表示已选中。这就是我的问题所在,以及我可以解决异步问题的原因。

这是我的代码(绝对最新代码的克隆代表):

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应该会产生相同的结果。

现在很明显,有几个错误是Xamarin Forms。我在这里记录了repro步骤: https://bugzilla.xamarin.com/show_bug.cgi?id=58451

2 个答案:

答案 0 :(得分:0)

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

答案 1 :(得分:0)

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

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