如何绑定到列表中的最后一项?
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"
;
【讨论】:
以上是关于如何绑定到列表中的最后一项?的主要内容,如果未能解决你的问题,请参考以下文章
与其使用 single() 返回表中的一项,如何返回与列表中相同 ID 绑定的多个项?