如何绑定到列表中的最后一项?

Posted

技术标签:

【中文标题】如何绑定到列表中的最后一项?【英文标题】:How can I bind to the last item in a list? 【发布时间】:2021-07-12 07:42:15 【问题描述】:

在 WPF 中使用 XAML 时,我们可以轻松地绑定到列表中的第一项,如下所示:

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

但是如果我想绑定到最后一项呢?假设我不知道列表可能有多少项,否则我可以轻松使用该索引。

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

不幸的是,以上都不起作用。有谁知道如何绑定到最后一项?

【问题讨论】:

没有简单的执行方法。最简单的方法是在 ViewModel 中设置一个属性,该属性将与集合中的最后一项同步。如果这都不可能,那么在 View 中提出实现选项是有可能的,但是他实现起来会比较困难。 啊,你是说虽然我可以使用 [x] 获取特定索引,但我无法调用“Last”语法? 你可以尝试用不同的方式设置最后一个元素的索引,包括[^ 1]有这样一种方式。但问题是,Binding 不会观察索引的变化。它将在 Binding 初始化时评估一次,然后将返回该先前计算的索引处的元素。因此,不可能进行简单的绑定。但是您可以在将跟踪最后一个元素的逻辑中创建一个简单的代理。并绑定到这个代理。 有趣。你有一个链接,我可以在[^1] 上阅读更多信息吗? 我问是因为现在当我使用[^1] 时,我只得到列表中的第二项 【参考方案1】:

您可以在视图模型LastItem 中创建一个新属性并绑定到该属性。

private Person lastItem;
public Person LastItem

    get  return lastItem; 
    set
    
        lastItem= value;
        OnPropertyChanged();
    

在加载Persons的代码后添加新行:

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

Xaml:

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

【讨论】:

是的,但我想知道 xaml 本身是否有可用的“Last”语法 这是明智的做法。 添加了这个作为最实用的方法。【参考方案2】:

这是代理代码。 我写得很快,没有检查 - 现在没有足够的空闲时间。 但是代码很简单,应该没有错误。 如果事情不成功,那就写吧。 我会努力解决的。

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;


    

【讨论】:

这太多了。只需实现IValueConverter 需要订阅才能更改集合(CollectionChanged 或 ListChanged)。这不能在转换器中完成。或者我不知道这个方法。可以创建一个 IMultiValueConverter,其中一个绑定将用于 Count。但是如果最后一项被它的索引替换它也无济于事:Collection [Collection.Count - 1] = newItem; 您只需通过设置集合属性或在修改源集合后显式引发事件来引发 PropertyChanged。 无论 ViewModel 实现如何,我的解决方案都可以在视图中使用。如果您对 ViewModel 进行了更改,那么,正如我在第一条消息中立即写的那样,最好立即将与集合的最后一个元素同步的属性添加到 ViewModel。这将比提高 PropertyCanged 和添加转换器更有效和更容易。 @user2250152 在他的回答中展示了这个实现。 在我看来,它为一个微不足道的问题增加了太多的复杂性。这是一个好主意,但是太复杂了,代码行太多。检查我的答案。我发现它的代码更少,可读性更强。【参考方案3】:

最简单的解决方案是实现 IValueConverter(参见 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();

用法

<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

或者引入一个专用的LastPerson 属性作为绑定源。

【讨论】:

嗯,很棒的方法。我要试一试【参考方案4】:

如果您必须通过 Getter 从 Xaml 导入 Last Item,请尝试此方法。 但是,添加 IValueConverter 或 Property 并不像添加那么简单。

我放了示例源代码。

GitHub

使用 xaml

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

代码

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));
    

视图模型

class MainViewModel

    public UserItems Users  get; set; 

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

【讨论】:

以上是关于如何绑定到列表中的最后一项?的主要内容,如果未能解决你的问题,请参考以下文章

MVC 模型绑定到列表 - 仅适用于列表中的第一项

与其使用 single() 返回表中的一项,如何返回与列表中相同 ID 绑定的多个项?

如何删除列表中的最后一项?

如何使 QML 容器中的最后一项填充剩余空间?

如何在 Django 模板中引用列表中的最后一项? list.-1.key

为啥我的 ArrayList 包含添加到列表中的最后一项的 N 个副本?