Xamarin.Forms: Get all cells/items of a listview

2019-02-14 21:21发布

问题:

I want to make some changes to all of the cells shown in the ListView. Therefore I want to get all cells or items. E.g.

this.listView.ItemSource.Items

Items in the above code doesn't exist. Furthermore I didn't found any options on ListView or ItemSource, which could do this. Is it possible to get all cells/items? If not what other options do I have to change the appearence of the cells after they were loaded? E.g. for changing text or layout.

回答1:

Normally, you don't touch your cells directly. Instead, you try to do everything via data binding if it is possible.

Let's use the following Page defined in XAML:

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="App6.Page1">
  <ListView x:Name="myList"
            BackgroundColor="Gray"
            ItemsSource="{Binding MyItems}"
            HasUnevenRows="true"
            RowHeight="-1">
    <ListView.ItemTemplate>
      <DataTemplate>
        <ViewCell>
          <ViewCell.View>
            <StackLayout
                        HorizontalOptions="FillAndExpand"
                        VerticalOptions="StartAndExpand"
                        Padding="15, 10, 10, 10"
                        BackgroundColor="White">

              <Label  Text="{Binding Title}"
                      FontSize="18"
                      TextColor="Black"
                      VerticalOptions="StartAndExpand"/>
            </StackLayout>
          </ViewCell.View>
        </ViewCell>
      </DataTemplate>
    </ListView.ItemTemplate>
  </ListView>
</ContentPage>

In your Page, you set your ViewModel as BindingContext.

public partial class Page1 : ContentPage
{
    public Page1()
    {
        InitializeComponent();
        BindingContext = new Page1ViewModel();
    }
}

And your ViewModel contains your Items MyItems in an ObservableCollection, which means that your view updates, if you add or remove Items. This property is bound as ItemsSource of you List (see XAML: ItemsSource="{Binding MyItems}").

class Page1ViewModel : INotifyPropertyChanged
{
    private ObservableCollection<MyItem> _myItems = new ObservableCollection<MyItem>();
    public event PropertyChangedEventHandler PropertyChanged;

    public ObservableCollection<MyItem> MyItems
    {
        get { return _myItems; }
        set
        {
            _myItems = value;
            OnPropertyChanged();
        }
    }

    [NotifyPropertyChangedInvocator]
    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }

    public Page1ViewModel()
    {
        MyItems.Add(new MyItem { Title = "Test" });
        MyItems.Add(new MyItem { Title = "Test2" });
        MyItems.Add(new MyItem { Title = "Test3" });
        MyItems.Add(new MyItem { Title = "Test4" });
        MyItems.Add(new MyItem { Title = "Test5" });
    }

    public void ChangeItem()
    {
        MyItems[1].Title = "Hello World";
    }
}

For each Item, you have an object that represents the data of one cell. The type of this item implements INotifyPropertyChanged. That means, that it notifies which property has been changed. The data binding mechanism registers to this event and will update the view, when it is raised.

public class MyItem : INotifyPropertyChanged
{
    private string _title;

    public string Title
    {
        get { return _title; }
        set
        {
            _title = value;
            OnPropertyChanged(); // Notify, that Title has been changed
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;

    [NotifyPropertyChangedInvocator]
    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

If you now change one of the items, e.g. by calling ChangeItem(). Then the Label of the second cell will update to "Hello World", because it is bound to Title (see XAML Text="{Binding Title}").

The general idea behind all this is called MvvM pattern, where you want to separate your view from the view logic and the model (data/services, ...).



回答2:

I am probably late to the party, but here's another approach using System.Reflection. Anyways, I'd suggest using it only in cases where databinding isn't possible and as a last resort option.

For a given ListView

<ListView x:Name="connectionsListView" HorizontalOptions="FillAndExpand" VerticalOptions="FillAndExpand" ItemSelected="OnConnectionSelect">
    <ListView.ItemTemplate>
        <DataTemplate>
            <ViewCell Height="40">
                <Grid>
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="*"/>
                        <ColumnDefinition Width="50"/>
                    </Grid.ColumnDefinitions>
                    <customs:MyViewClass Grid.Column="0"/>
                </Grid>
            </ViewCell>
        </DataTemplate>
    </ListView.ItemTemplate>
    <ListView.Footer>
        <ContentView />
    </ListView.Footer>
</ListView>

You can crawl through the ListView by grabbing the "TemplatedItems" property, casting it to ITemplatedItemsList<Cell> and iterating through your (View)cells.

IEnumerable<PropertyInfo> pInfos = (connectionsListView as ItemsView<Cell>).GetType().GetRuntimeProperties();
var templatedItems = pInfos.FirstOrDefault(info => info.Name == "TemplatedItems");
if (templatedItems != null)
{
  var cells = templatedItems.GetValue(connectionsListView);
    foreach (ViewCell cell in cells as Xamarin.Forms.ITemplatedItemsList<Xamarin.Forms.Cell>)
    {
        if (cell.BindingContext != null && cell.BindingContext is MyModelClass)
        {
            MyViewClass target = (cell.View as Grid).Children.OfType<MyViewClass>().FirstOrDefault();
            if (target != null)
            {
               //do whatever you want to do with your item... 
            }
        }
    }
}

Update: Since it was asked, if (cell.BindingContext != null && cell.BindingContext is MyModelClass) is basically a safeguard to prevent access to non-existing views, for instance if the ListView hasn't been populated yet.

If a check for the BindingContext being a particular model class isn't necessary, it would be sufficient to reduce the line to

if (cell.BindingContext != null)