How can I bind to the last item in a list?

Viewed 147

While using XAML in WPF, we can easily bind to the first item in a list as follows:

<TextBlock Text="{Binding Model.Persons[0].FullName}"/>

But what if I wanted to bind to the last item? Let's say I don't know how many items the list may have or else I could easily have used that index.

<TextBlock Text="{Binding Model.Persons.Last.FullName}"/>

or

Neither above does not work unfortunately. Does anyone have any idea how to bind to the last item?

4 Answers

You can create a new property in your view model LastItem and bind to this property.

private Person lastItem;
public Person LastItem
{
    get { return lastItem; }
    set
    {
        lastItem= value;
        OnPropertyChanged();
    }
}

Add a new line after the code where you load Persons:

...
LastItem = Persons.LastOrDefault();
...

Xaml:

<TextBlock Text="{Binding Model.LastItem.FullName}"/>

Here is the proxy code. I wrote quickly, did not check - now there is not enough free time. But the code is simple, there should be no errors. If something doesn't work out, then write. I'll try to fix it.

using System;
using System.Collections;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
using System.Windows;

namespace Proxy
{
    public class ProxyLastItem : Freezable
    {
        protected override Freezable CreateInstanceCore()
            => new ProxyLastItem();


        /// <summary>Observable Collection: <see cref="INotifyCollectionChanged"/> or <see cref="IBindingList"/>.</summary>
        public IEnumerable ItemsSource
        {
            get { return (IEnumerable)GetValue(ItemsSourceProperty); }
            set { SetValue(ItemsSourceProperty, value); }
        }

        /// <summary><see cref="DependencyProperty"/> for property <see cref="ItemsSource"/>.</summary>
        public static readonly DependencyProperty ItemsSourceProperty =
            DependencyProperty.Register(nameof(ItemsSource), typeof(IEnumerable), typeof(ProxyLastItem), new PropertyMetadata(null, ItemsSourceChanged));

        private static void ItemsSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            ProxyLastItem proxyLast = (ProxyLastItem)d;

            IEnumerable collection = e.OldValue as IEnumerable;

            if (collection != null)
            {
                if (collection is INotifyCollectionChanged observableCollection)
                {
                    observableCollection.CollectionChanged -= proxyLast.OnCollectionChanged;
                    proxyLast.IsObservableCollection = false;
                }
                if (collection is IBindingList bindingList)
                {
                    bindingList.ListChanged -= proxyLast.OnListChanged;
                    proxyLast.IsBindingList = false;
                }

            }


            collection = e.NewValue as IEnumerable;
            if (collection == null)
            {
                proxyLast.LastItem = null;
            }
            else
            {
                proxyLast.SetLastItem(collection);
                if (collection is INotifyCollectionChanged observableCollection)
                {
                    observableCollection.CollectionChanged += proxyLast.OnCollectionChanged;
                    proxyLast.IsObservableCollection = true;
                }
                else if (collection is IBindingList bindingList)
                {
                    bindingList.ListChanged += proxyLast.OnListChanged;
                    proxyLast.IsBindingList = true;
                }
            }
        }

        private void SetLastItem(IEnumerable collection)
        {
            if (!ReferenceEquals(collection, ItemsSource))
                throw new Exception("The observable collection is not the same as the ItemsSource collection.");

            if (collection is IList list)
            {
                if (list.Count == 0)
                    LastItem = null;
                else
                    LastItem = list[list.Count - 1];
            }
            else
                LastItem = collection.Cast<object>().LastOrDefault();
        }

        private void OnListChanged(object sender, ListChangedEventArgs e)
            => SetLastItem((IEnumerable)sender);

        private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
            => SetLastItem((IEnumerable)sender);

        /// <summary><see cref="ItemsSource"/> is observable collection.</summary>
        public bool IsObservableCollection
        {
            get { return (bool)GetValue(IsObservableCollectionProperty); }
            private set { SetValue(IsObservableCollectionPropertyKey, value); }
        }

        private static readonly DependencyPropertyKey IsObservableCollectionPropertyKey =
            DependencyProperty.RegisterReadOnly(nameof(IsObservableCollection), typeof(bool), typeof(ProxyLastItem), new PropertyMetadata(false));
        /// <summary><see cref="DependencyProperty"/> для свойства <see cref="IsObservableCollection"/>.</summary>
        public static readonly DependencyProperty IsObservableCollectionProperty = IsObservableCollectionPropertyKey.DependencyProperty;



        /// <summary><see cref="ItemsSource"/> is BindingList.</summary>
        public bool IsBindingList
        {
            get { return (bool)GetValue(IsBindingListProperty); }
            private set { SetValue(IsBindingListPropertyKey, value); }
        }

        private static readonly DependencyPropertyKey IsBindingListPropertyKey =
            DependencyProperty.RegisterReadOnly(nameof(IsBindingList), typeof(bool), typeof(ProxyLastItem), new PropertyMetadata(false));
        /// <summary><see cref="DependencyProperty"/> for property <see cref="IsBindingList"/>.</summary>
        public static readonly DependencyProperty IsBindingListProperty = IsBindingListPropertyKey.DependencyProperty;

        /// <summary>Last Item from <see cref="ItemsSource"/>.</summary>
        public object LastItem
        {
            get { return (object)GetValue(LastItemProperty); }
            private set { SetValue(LastItemPropertyKey, value); }
        }

        private static readonly DependencyPropertyKey LastItemPropertyKey =
            DependencyProperty.RegisterReadOnly(nameof(LastItem), typeof(object), typeof(ProxyLastItem), new PropertyMetadata(null));
        /// <summary><see cref="DependencyProperty"/> for property <see cref="LastItem"/>.</summary>
        public static readonly DependencyProperty LastItemProperty = LastItemPropertyKey.DependencyProperty;


    }
}

The simplest solution is to implement IValueConverter (see Data conversion).

PersonsToLastPersonNameConverter.cs

class PersonsToLastPersonNameConverter : IValueConverter
{
  public object Convert(object value, Type targetType, object parameter, CultureInfo culture) 
    => value is IEnumerable collection 
      ? collection
        .Cast<Person>()
        .LastOrDefault() 
        .FullName
      : Binding.DoNothing;

  public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => throw new NotSupportedException();
}

Usage

<Window>
  <Window.Resources>
    <PersonsToLastPersonNameConverter x:Key="PersonsToLastPersonNameConverter " />
  </Window.Resources>

  <TextBlock Text="{Binding Model.Persons, Converter={StaticResource PersonsToLastPersonNameConverter}"/>
</Window>

Model.cs

class Model : INotifyPropertyChanged
{
  public List<Person> Persons { get; }

  public Model() => this.Persons = new List<Person>();

  private void AddItem(Person person)
  {
    this.Persons.Add(person);

    // Trigger the converter
    OnPersonsChanged();
  }

  private void RemoveItem(int index)
  {
    this.Persons.RemoveAt(index);

    // Trigger the converter
    OnPersonsChanged();
  }

  // Trigger the converter
  private void OnPersonsChanged() => OnPropertyChanged(nameof(this.Persons));

  // TODO::Implement INotifyPropertyChanged
}

Alternatively introduce a dedicated LastPerson property as binding source.

If you must import the Last Item from Xaml via Getter, try this method. However, adding IValueConverter or Property is not as easy as adding it.

enter image description here

And I put the sample source code.

GitHub

Using xaml

<TextBlock Text="{Binding Users.FirstUser.Name}"/>
<TextBlock Text="{Binding Users.LastUser.Name}"/>

Code

public class UserItems : ObservableCollection<UserItem>, INotifyPropertyChanged
{
    private UserItem _firstItem;
    private UserItem _userItem;

    public UserItem FirstUser
    {
        get { return _firstItem; }
        set { _firstItem = value; OnPropertyChanged(); }
    }

    public UserItem LastUser
    {
        get { return _userItem; }
        set { _userItem = value; OnPropertyChanged(); }
    }

    protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
    {
        base.OnCollectionChanged(e);
        FirstUser = this.FirstOrDefault();
        LastUser = this.LastOrDefault();
    }

    public new event PropertyChangedEventHandler PropertyChanged;
    protected void OnPropertyChanged([CallerMemberName] string name = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
    }
}

ViewModel

class MainViewModel
{
    public UserItems Users { get; set; }

    public MainViewModel()
    {
        Users = new UserItems
        {
            new UserItem { Name = "James" },
            new UserItem { Name = "Elena" }
        };
    }
}
Related