Partilhar via


Personalizar uma ListView

Um Xamarin.Forms ListView é um modo de exibição que exibe uma coleção de dados como uma lista vertical. Este artigo demonstra como criar um renderizador personalizado que encapsula os controles de lista e layouts de célula nativa específicos a uma plataforma, permitindo mais controle sobre o desempenho do controle de lista nativo.

Cada Xamarin.Forms exibição tem um renderizador de acompanhamento para cada plataforma que cria uma instância de um controle nativo. Quando a ListView é renderizado por um Xamarin.Forms aplicativo, no iOS a classe é instanciada, o ListViewRenderer que, por sua vez, instancia um controle nativo UITableView . Na plataforma Android, a classe ListViewRenderer cria uma instância de um controle ListView nativo. Na UWP (Plataforma Universal do Windows), a classe ListViewRenderer cria uma instância de um controle ListView nativo. Para obter mais informações sobre o renderizador e as classes de controle nativas para as quais Xamarin.Forms os controles são mapeados, consulte Classes base do renderizador e controles nativos.

O diagrama a seguir ilustra a relação entre o controle ListView e os controles nativos correspondentes que o implementam:

Relação entre o controle ListView e a implementação de controles nativos

E possível aproveitar o processo de renderização para implementar personalizações específicas da plataforma criando um renderizador personalizado para um ListView em cada plataforma. O processo para fazer isso é o seguinte:

  1. Crie um Xamarin.Forms controle personalizado.
  2. Consuma o controle personalizado do Xamarin.Forms.
  3. Criar o renderizador personalizado para o controle em cada plataforma.

Agora, cada item será abordado separadamente, a fim de implementar um renderizador de NativeListView que tira proveito dos layouts de célula nativos e dos controles de lista específicos da plataforma. Esse cenário é útil ao portar um aplicativo nativo existente que contém código de lista e de célula pode ser reutilizado. Além disso, ele permite a personalização detalhada de recursos de controle de lista que podem afetar o desempenho, como a virtualização de dados.

Criando o controle ListView personalizado

É possível criar um controle ListView personalizado criando subclasses da classe ListView, conforme mostrado no exemplo de código a seguir:

public class NativeListView : ListView
{
  public static readonly BindableProperty ItemsProperty =
    BindableProperty.Create ("Items", typeof(IEnumerable<DataSource>), typeof(NativeListView), new List<DataSource> ());

  public IEnumerable<DataSource> Items {
    get { return (IEnumerable<DataSource>)GetValue (ItemsProperty); }
    set { SetValue (ItemsProperty, value); }
  }

  public event EventHandler<SelectedItemChangedEventArgs> ItemSelected;

  public void NotifyItemSelected (object item)
  {
    if (ItemSelected != null) {
      ItemSelected (this, new SelectedItemChangedEventArgs (item));
    }
  }
}

A NativeListView é criada no projeto da biblioteca .NET Standard e define a API para o controle personalizado. Esse controle expõe uma propriedade Items que é usada para popular o ListView com os dados e que pode ser associada a dados para fins de exibição. Ele também expõe um evento ItemSelected que será disparado sempre que um item for selecionado em um controle de lista nativo específico da plataforma. Para obter mais informações sobre vinculação de dados, veja Noções básicas de vinculação de dados.

Consumindo o controle personalizado

O controle personalizado NativeListView pode ser referenciado em XAML no projeto da biblioteca .NET Standard declarando um namespace para sua localização e usando o prefixo do namespace no controle. O exemplo de código a seguir mostra como o controle personalizado NativeListView pode ser consumido por uma página XAML:

<ContentPage ...
    xmlns:local="clr-namespace:CustomRenderer;assembly=CustomRenderer"
    ...>
    ...
    <ContentPage.Content>
          <Grid>
            <Grid.RowDefinitions>
              <RowDefinition Height="Auto"/>
              <RowDefinition Height="*" />
            </Grid.RowDefinitions>
          <Label Text="{x:Static local:App.Description}" HorizontalTextAlignment="Center" />
            <local:NativeListView Grid.Row="1" x:Name="nativeListView" ItemSelected="OnItemSelected" VerticalOptions="FillAndExpand" />
          </Grid>
      </ContentPage.Content>
</ContentPage>

O prefixo do namespace local pode ser qualquer nome. No entanto, os valores de clr-namespace e assembly devem corresponder aos detalhes do controle personalizado. Quando o namespace é declarado, o prefixo é usado para referenciar o controle personalizado.

O seguinte exemplo de código mostra como o controle personalizado NativeListView pode ser consumido por um página em C#:

public class MainPageCS : ContentPage
{
    NativeListView nativeListView;

    public MainPageCS()
    {
        nativeListView = new NativeListView
        {
            Items = DataSource.GetList(),
            VerticalOptions = LayoutOptions.FillAndExpand
        };

        switch (Device.RuntimePlatform)
        {
            case Device.iOS:
                Padding = new Thickness(0, 20, 0, 0);
                break;
            case Device.Android:
            case Device.UWP:
                Padding = new Thickness(0);
                break;
        }

        Content = new Grid
        {
            RowDefinitions = {
                new RowDefinition { Height = GridLength.Auto },
                new RowDefinition { Height = new GridLength (1, GridUnitType.Star) }
            },
            Children = {
                new Label { Text = App.Description, HorizontalTextAlignment = TextAlignment.Center },
                nativeListView
            }
        };
        nativeListView.ItemSelected += OnItemSelected;
    }
    ...
}

O controle personalizado NativeListView usa renderizadores personalizados específicos da plataforma para exibir uma lista de dados, que são populados por meio da propriedade Items. Cada linha na lista contém três itens de dados – um nome, uma categoria e um nome de arquivo de imagem. O layout de cada linha na lista é definido pelo renderizador personalizado específico da plataforma.

Observação

Como o controle personalizado NativeListView será renderizado usando controles de lista específicos da plataforma que incluem a capacidade de rolagem, ele não deve ser colocado em controles de layout roláveis, como ScrollView.

Agora, é possível adicionar um renderizador personalizado a cada projeto de aplicativo para criar layouts de célula nativa e controles de lista específicos da plataforma.

Criando o renderizador personalizado em cada plataforma

O processo para criar a classe do renderizador personalizado é a seguinte:

  1. Crie uma subclasse da classe ListViewRenderer que renderiza o controle personalizado.
  2. Substitua o método OnElementChanged que renderiza o controle personalizado e escreva a lógica para personalizá-lo. Esse método é chamado quando o correspondente Xamarin.FormsListView é criado.
  3. Adicione um ExportRenderer atributo à classe de renderizador personalizado para especificar que ele será usado para renderizar o Xamarin.Forms controle personalizado. Esse atributo é usado para registrar o renderizador personalizado com Xamarin.Forms.

Observação

O fornecimento de um renderizador personalizado em cada projeto de plataforma é opcional. Se um renderizador personalizado não estiver registrado, será usado o renderizador padrão da classe base da célula.

O seguinte diagrama ilustra as responsabilidades de cada projeto no aplicativo de exemplo, bem como as relações entre elas:

Responsabilidades do projeto de renderizador personalizado de NativeListView

O controle personalizado NativeListView é renderizado por classes de renderizador específicas da plataforma, que derivam da classe ListViewRenderer para cada plataforma. Isso faz com que cada controle personalizado NativeListView seja renderizado com controles de lista específicos da plataforma e layouts de célula nativos, conforme mostrado nas capturas de tela seguir:

NativeListView em cada plataforma

A ListViewRenderer classe expõe o OnElementChanged método, que é chamado quando o Xamarin.Forms controle personalizado é criado para renderizar o controle nativo correspondente. Esse método usa um parâmetro ElementChangedEventArgs, que contém as propriedades OldElement e NewElement. Essas propriedades representam o Xamarin.Forms elemento ao qual o renderizador foi anexado e o Xamarin.Forms elemento ao qual o renderizador está anexado, respectivamente. No aplicativo de exemplo, a propriedade OldElement será null e a propriedade NewElement conterá uma referência à instância de NativeListView.

Uma versão de substituição do método OnElementChanged, em cada classe de renderizador específica da plataforma, é o lugar para realização da personalização do controle nativo. Uma referência tipada ao controle nativo que está sendo usado na plataforma pode ser acessada por meio da propriedade Control. Além disso, uma referência ao Xamarin.Forms controle que está sendo renderizado pode ser obtida por meio da Element propriedade.

É necessário ter cuidado ao assinar manipuladores de eventos no método OnElementChanged, conforme demonstrado no seguinte exemplo de código:

protected override void OnElementChanged (ElementChangedEventArgs<Xamarin.Forms.ListView> e)
{
  base.OnElementChanged (e);

  if (e.OldElement != null) {
    // Unsubscribe from event handlers and cleanup any resources
  }

  if (e.NewElement != null) {
    // Configure the native control and subscribe to event handlers
  }
}

O controle nativo só deve ser configurado e os manipuladores de eventos assinados quando o renderizador personalizado estiver anexado a um novo Xamarin.Forms elemento. De forma semelhante, a inscrição de quaisquer manipuladores de evento inscritos só deve ser cancelada quando o elemento ao qual o renderizador está anexado for alterado. A adoção dessa abordagem ajudará a criar um renderizador personalizado que não sofre perdas de memória.

Uma versão substituída do OnElementPropertyChanged método, em cada classe de renderizador específica da plataforma, é o local para responder às alterações de propriedade associável no Xamarin.Forms controle personalizado. Uma verificação da propriedade alterada sempre deve ser feita, pois essa substituição pode ser chamada várias vezes.

Cada classe de renderizador personalizado é decorada com um ExportRenderer atributo que registra o renderizador com Xamarin.Forms. O atributo usa dois parâmetros: o nome do tipo do Xamarin.Forms controle personalizado que está sendo renderizado e o nome do tipo do renderizador personalizado. O prefixo assembly do atributo especifica que o atributo se aplica a todo o assembly.

As seções a seguir abordam a implementação de cada classe de renderizador personalizado específica da plataforma.

Criando o renderizador personalizado no iOS

O exemplo de código a seguir mostra o renderizador personalizado para a plataforma iOS:

[assembly: ExportRenderer (typeof(NativeListView), typeof(NativeiOSListViewRenderer))]
namespace CustomRenderer.iOS
{
    public class NativeiOSListViewRenderer : ListViewRenderer
    {
        protected override void OnElementChanged (ElementChangedEventArgs<Xamarin.Forms.ListView> e)
        {
            base.OnElementChanged (e);

            if (e.OldElement != null) {
                // Unsubscribe
            }

            if (e.NewElement != null) {
                Control.Source = new NativeiOSListViewSource (e.NewElement as NativeListView);
            }
        }
    }
}

O UITableView controle é configurado criando uma instância da classe, desde que o renderizador personalizado esteja anexado NativeiOSListViewSource a um novo Xamarin.Forms elemento. Essa classe fornece dados ao controle UITableView substituindo os métodos RowsInSection e GetCell da classe UITableViewSource e expondo uma propriedade Items que contém a lista de dados a serem exibidos. A classe também fornece uma substituição do método RowSelected que invoca o evento ItemSelected fornecido pelo controle personalizado NativeListView. Para obter mais informações sobre as substituições de método, confira Subclassificação de UITableViewSource. O método GetCell retorna um UITableCellView que é preenchido com os dados para cada linha na lista e é mostrado no exemplo de código a seguir:

public override UITableViewCell GetCell (UITableView tableView, NSIndexPath indexPath)
{
  // request a recycled cell to save memory
  NativeiOSListViewCell cell = tableView.DequeueReusableCell (cellIdentifier) as NativeiOSListViewCell;

  // if there are no cells to reuse, create a new one
  if (cell == null) {
    cell = new NativeiOSListViewCell (cellIdentifier);
  }

  if (String.IsNullOrWhiteSpace (tableItems [indexPath.Row].ImageFilename)) {
    cell.UpdateCell (tableItems [indexPath.Row].Name
      , tableItems [indexPath.Row].Category
      , null);
  } else {
    cell.UpdateCell (tableItems [indexPath.Row].Name
      , tableItems [indexPath.Row].Category
      , UIImage.FromFile ("Images/" + tableItems [indexPath.Row].ImageFilename + ".jpg"));
  }

  return cell;
}

Esse método cria uma instância de NativeiOSListViewCell para cada linha de dados que será exibida na tela. A instância de NativeiOSCell define o layout de cada célula e os dados da célula. Quando uma célula desaparecer da tela devido à rolagem, ela será disponibilizada para reutilização. Isso evita o desperdício de memória garantindo que haja apenas instâncias de NativeiOSCell para os dados que estão sendo exibidos na tela, em vez de todos os dados da lista. Para obter mais informações sobre a reutilização de células, confira Reutilização de células. O método GetCell também lê a propriedade ImageFilename de cada linha de dados, desde que elas existam, e lê a imagem e a armazena como uma instância de UIImage antes de atualizar a instância de NativeiOSListViewCell com os dados (nome, categoria e imagem) da linha.

A classe NativeiOSListViewCell define o layout para cada célula e é mostrada no exemplo de código a seguir:

public class NativeiOSListViewCell : UITableViewCell
{
  UILabel headingLabel, subheadingLabel;
  UIImageView imageView;

  public NativeiOSListViewCell (NSString cellId) : base (UITableViewCellStyle.Default, cellId)
  {
    SelectionStyle = UITableViewCellSelectionStyle.Gray;

    ContentView.BackgroundColor = UIColor.FromRGB (218, 255, 127);

    imageView = new UIImageView ();

    headingLabel = new UILabel () {
      Font = UIFont.FromName ("Cochin-BoldItalic", 22f),
      TextColor = UIColor.FromRGB (127, 51, 0),
      BackgroundColor = UIColor.Clear
    };

    subheadingLabel = new UILabel () {
      Font = UIFont.FromName ("AmericanTypewriter", 12f),
      TextColor = UIColor.FromRGB (38, 127, 0),
      TextAlignment = UITextAlignment.Center,
      BackgroundColor = UIColor.Clear
    };

    ContentView.Add (headingLabel);
    ContentView.Add (subheadingLabel);
    ContentView.Add (imageView);
  }

  public void UpdateCell (string caption, string subtitle, UIImage image)
  {
    headingLabel.Text = caption;
    subheadingLabel.Text = subtitle;
    imageView.Image = image;
  }

  public override void LayoutSubviews ()
  {
    base.LayoutSubviews ();

    headingLabel.Frame = new CoreGraphics.CGRect (5, 4, ContentView.Bounds.Width - 63, 25);
    subheadingLabel.Frame = new CoreGraphics.CGRect (100, 18, 100, 20);
    imageView.Frame = new CoreGraphics.CGRect (ContentView.Bounds.Width - 63, 5, 33, 33);
  }
}

Essa classe define os controles usados para renderizar o conteúdo da célula e seu layout. O construtor NativeiOSListViewCell cria instâncias dos controles UILabel e UIImageView e inicializa sua aparência. Esses controles são usados para exibir dados de cada linha, com o método UpdateCell sendo usado para definir esses dados na instâncias de UILabel e UIImageView. A localização dessas instâncias é definida pelo método LayoutSubviews substituído especificando suas coordenadas dentro da célula.

Respondendo a uma alteração de propriedade no controle personalizado

Se a propriedade NativeListView.Items for alterada devido a itens serem adicionados ou removidos da lista, o renderizador personalizado precisará responder exibindo as alterações. Isso pode ser feito substituindo o método OnElementPropertyChanged, que é mostrado no exemplo de código a seguir:

protected override void OnElementPropertyChanged (object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
  base.OnElementPropertyChanged (sender, e);

  if (e.PropertyName == NativeListView.ItemsProperty.PropertyName) {
    Control.Source = new NativeiOSListViewSource (Element as NativeListView);
  }
}

O método cria uma nova instância da classe NativeiOSListViewSource que fornece dados para o controle UITableView, desde que a propriedade NativeListView.Items vinculável tenha sido alterada.

Criando o renderizador personalizado no Android

O exemplo de código a seguir mostra o renderizador personalizado para a plataforma Android:

[assembly: ExportRenderer(typeof(NativeListView), typeof(NativeAndroidListViewRenderer))]
namespace CustomRenderer.Droid
{
    public class NativeAndroidListViewRenderer : ListViewRenderer
    {
        Context _context;

        public NativeAndroidListViewRenderer(Context context) : base(context)
        {
            _context = context;
        }

        protected override void OnElementChanged(ElementChangedEventArgs<Xamarin.Forms.ListView> e)
        {
            base.OnElementChanged(e);

            if (e.OldElement != null)
            {
                // unsubscribe
                Control.ItemClick -= OnItemClick;
            }

            if (e.NewElement != null)
            {
                // subscribe
                Control.Adapter = new NativeAndroidListViewAdapter(_context as Android.App.Activity, e.NewElement as NativeListView);
                Control.ItemClick += OnItemClick;
            }
        }
        ...

        void OnItemClick(object sender, Android.Widget.AdapterView.ItemClickEventArgs e)
        {
            ((NativeListView)Element).NotifyItemSelected(((NativeListView)Element).Items.ToList()[e.Position - 1]);
        }
    }
}

O controle nativo ListView é configurado desde que o renderizador personalizado esteja anexado a um novo Xamarin.Forms elemento. Essa configuração envolve a criação de uma instância da classe NativeAndroidListViewAdapter que fornece dados para o controle ListView nativo e o registro de um manipulador de eventos para processar o evento ItemClick. Por sua vez, esse manipulador invocará o evento ItemSelected fornecido pelo controle personalizado NativeListView. A ItemClick assinatura do evento será cancelada se o Xamarin.Forms elemento ao qual o renderizador está anexado for alterado.

O NativeAndroidListViewAdapter deriva da classe BaseAdapter e expõe uma propriedade Items que contém a lista de dados a serem exibidos, além de substituir os métodos Count, GetView, GetItemId e this[int]. Para obter mais informações sobre essas substituições de método, confira Implementando um ListAdapter. O método GetView retorna uma exibição para cada linha, preenchida com os dados, e é mostrado no exemplo de código a seguir:

public override View GetView (int position, View convertView, ViewGroup parent)
{
  var item = tableItems [position];

  var view = convertView;
  if (view == null) {
    // no view to re-use, create new
    view = context.LayoutInflater.Inflate (Resource.Layout.NativeAndroidListViewCell, null);
  }
  view.FindViewById<TextView> (Resource.Id.Text1).Text = item.Name;
  view.FindViewById<TextView> (Resource.Id.Text2).Text = item.Category;

  // grab the old image and dispose of it
  if (view.FindViewById<ImageView> (Resource.Id.Image).Drawable != null) {
    using (var image = view.FindViewById<ImageView> (Resource.Id.Image).Drawable as BitmapDrawable) {
      if (image != null) {
        if (image.Bitmap != null) {
          //image.Bitmap.Recycle ();
          image.Bitmap.Dispose ();
        }
      }
    }
  }

  // If a new image is required, display it
  if (!String.IsNullOrWhiteSpace (item.ImageFilename)) {
    context.Resources.GetBitmapAsync (item.ImageFilename).ContinueWith ((t) => {
      var bitmap = t.Result;
      if (bitmap != null) {
        view.FindViewById<ImageView> (Resource.Id.Image).SetImageBitmap (bitmap);
        bitmap.Dispose ();
      }
    }, TaskScheduler.FromCurrentSynchronizationContext ());
  } else {
    // clear the image
    view.FindViewById<ImageView> (Resource.Id.Image).SetImageBitmap (null);
  }

  return view;
}

O método GetView é chamado para retornar a célula a ser renderizada, como um View, para cada linha de dados na lista. Ele cria uma instância de View para cada linha de dados que será exibida na tela, com a aparência da instância de View definida em um arquivo de layout. Quando uma célula desaparecer da tela devido à rolagem, ela será disponibilizada para reutilização. Isso evita o desperdício de memória garantindo que haja apenas instâncias de View para os dados que estão sendo exibidos na tela, em vez de todos os dados da lista. Para obter mais informações sobre a reutilização da exibição, confira Reutilização da exibição de linha.

O método GetView também preenche a instância de View com os dados, incluindo a leitura dos dados de imagem do nome de arquivo especificado na propriedade ImageFilename.

O layout de cada célula exibida pelo ListView nativo é definido no arquivo de layout NativeAndroidListViewCell.axml, que é inflado pelo método LayoutInflater.Inflate. O exemplo de código a seguir mostra a definição do layout:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:padding="8dp"
    android:background="@drawable/CustomSelector">
    <LinearLayout
        android:id="@+id/Text"
        android:orientation="vertical"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:paddingLeft="10dip">
        <TextView
            android:id="@+id/Text1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textColor="#FF7F3300"
            android:textSize="20dip"
            android:textStyle="italic" />
        <TextView
            android:id="@+id/Text2"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="14dip"
            android:textColor="#FF267F00"
            android:paddingLeft="100dip" />
    </LinearLayout>
    <ImageView
        android:id="@+id/Image"
        android:layout_width="48dp"
        android:layout_height="48dp"
        android:padding="5dp"
        android:src="@drawable/icon"
        android:layout_alignParentRight="true" />
</RelativeLayout>

Esse layout especifica que dois controles TextView e um controle ImageView sejam usados para exibir o conteúdo da célula. Os dois controles TextView têm orientação vertical dentro de um controle LinearLayout, com todos os controles contidos em um RelativeLayout.

Respondendo a uma alteração de propriedade no controle personalizado

Se a propriedade NativeListView.Items for alterada devido a itens serem adicionados ou removidos da lista, o renderizador personalizado precisará responder exibindo as alterações. Isso pode ser feito substituindo o método OnElementPropertyChanged, que é mostrado no exemplo de código a seguir:

protected override void OnElementPropertyChanged (object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
  base.OnElementPropertyChanged (sender, e);

  if (e.PropertyName == NativeListView.ItemsProperty.PropertyName) {
    Control.Adapter = new NativeAndroidListViewAdapter (_context as Android.App.Activity, Element as NativeListView);
  }
}

O método cria uma nova instância da classe NativeAndroidListViewAdapter que fornece dados para o controle ListView nativo, desde que a propriedade NativeListView.Items vinculável tenha sido alterada.

Criando o renderizador personalizado na UWP

O exemplo de código a seguir mostra o renderizador personalizado para a UWP:

[assembly: ExportRenderer(typeof(NativeListView), typeof(NativeUWPListViewRenderer))]
namespace CustomRenderer.UWP
{
    public class NativeUWPListViewRenderer : ListViewRenderer
    {
        ListView listView;

        protected override void OnElementChanged(ElementChangedEventArgs<Xamarin.Forms.ListView> e)
        {
            base.OnElementChanged(e);

            listView = Control as ListView;

            if (e.OldElement != null)
            {
                // Unsubscribe
                listView.SelectionChanged -= OnSelectedItemChanged;
            }

            if (e.NewElement != null)
            {
                listView.SelectionMode = ListViewSelectionMode.Single;
                listView.IsItemClickEnabled = false;
                listView.ItemsSource = ((NativeListView)e.NewElement).Items;             
                listView.ItemTemplate = App.Current.Resources["ListViewItemTemplate"] as Windows.UI.Xaml.DataTemplate;
                // Subscribe
                listView.SelectionChanged += OnSelectedItemChanged;
            }  
        }

        void OnSelectedItemChanged(object sender, SelectionChangedEventArgs e)
        {
            ((NativeListView)Element).NotifyItemSelected(listView.SelectedItem);
        }
    }
}

O controle nativo ListView é configurado desde que o renderizador personalizado esteja anexado a um novo Xamarin.Forms elemento. Essa configuração envolve definir como o controle ListView nativo responderá à seleção de itens, ao preenchimento dos dados exibidos pelo controle, à definição da aparência e do conteúdo de cada célula e ao registro de um manipulador de eventos para processar o evento SelectionChanged. Por sua vez, esse manipulador invocará o evento ItemSelected fornecido pelo controle personalizado NativeListView. A SelectionChanged assinatura do evento será cancelada se o Xamarin.Forms elemento ao qual o renderizador está anexado for alterado.

A aparência e o conteúdo de cada célula ListView nativa são definidos por um DataTemplate denominado ListViewItemTemplate. Esse DataTemplate é armazenado no dicionário de recursos de nível de aplicativo e é mostrado no exemplo de código a seguir:

<DataTemplate x:Key="ListViewItemTemplate">
    <Grid Background="#DAFF7F">
        <Grid.Resources>
            <local:ConcatImageExtensionConverter x:Name="ConcatImageExtensionConverter" />
        </Grid.Resources>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="0.40*" />
            <ColumnDefinition Width="0.40*"/>
            <ColumnDefinition Width="0.20*" />
        </Grid.ColumnDefinitions>
        <TextBlock Grid.ColumnSpan="2" Foreground="#7F3300" FontStyle="Italic" FontSize="22" VerticalAlignment="Top" Text="{Binding Name}" />
        <TextBlock Grid.RowSpan="2" Grid.Column="1" Foreground="#267F00" FontWeight="Bold" FontSize="12" VerticalAlignment="Bottom" Text="{Binding Category}" />
        <Image Grid.RowSpan="2" Grid.Column="2" HorizontalAlignment="Left" VerticalAlignment="Center" Source="{Binding ImageFilename, Converter={StaticResource ConcatImageExtensionConverter}}" Width="50" Height="50" />
        <Line Grid.Row="1" Grid.ColumnSpan="3" X1="0" X2="1" Margin="30,20,0,0" StrokeThickness="1" Stroke="LightGray" Stretch="Fill" VerticalAlignment="Bottom" />
    </Grid>
</DataTemplate>

O DataTemplate especifica os controles usados para exibir o conteúdo da célula, bem como seu layout e aparência. Dois controles TextBlock e um controle Image são usados para exibir o conteúdo da célula por meio da associação de dados. Além disso, uma instância do ConcatImageExtensionConverter é usada para concatenar a extensão de arquivo .jpg a cada nome de arquivo de imagem. Isso garante que o controle Image possa carregar e renderizar a imagem quando sua propriedade Source estiver definida.

Respondendo a uma alteração de propriedade no controle personalizado

Se a propriedade NativeListView.Items for alterada devido a itens serem adicionados ou removidos da lista, o renderizador personalizado precisará responder exibindo as alterações. Isso pode ser feito substituindo o método OnElementPropertyChanged, que é mostrado no exemplo de código a seguir:

protected override void OnElementPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
    base.OnElementPropertyChanged(sender, e);

    if (e.PropertyName == NativeListView.ItemsProperty.PropertyName)
    {
        listView.ItemsSource = ((NativeListView)Element).Items;
    }
}

O método preenche novamente o controle ListView nativo com os dados alterados, desde que a propriedade NativeListView.Items vinculável tenha sido alterada.

Resumo

Este artigo demonstrou como criar um renderizador personalizado que encapsula os controles de lista e layouts de célula nativa específicos a uma plataforma, permitindo mais controle sobre o desempenho do controle de lista nativo.