状态栏记录 x / y

7

我有一个ObservableCollection,它是Grid的DataContext。该Grid是插入到主窗口中的用户控件。

我想在状态栏中显示“第x条记录,共y条记录”,因此作为第一步,我尝试使用以下XAML在Grid中显示它:

<TextBlock Grid.Row="2" Grid.Column="3">
    <TextBlock Text="{Binding CurrentPosition}" /> <TextBlock Text="{Binding Count}" />
</TextBlock>

计数器可以正常工作,并在添加新项时自动更新。我的代码中定义的CurrentPosition始终保持为0。
如何使CurrentPosition自动更新?我希望不必使用INotify**,因为这已经是一个ObservableCollection了。
我也没有任何代码后台处理,所以我希望可以在我的类(或模型)和XAML中实现。
我尝试过使用CurrentChanged,但没有成功:
    public MyObservableCollection() : base() {
        this.GetDefaultView().CurrentChanged += MyObservableCollection_CurrentChanged;
    }

我的可观察集合:

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;

namespace ToDoApplication.Models {
    public class MyObservableCollection<T> : ObservableCollection<T> {

        public MyObservableCollection() : base() {
        }

        public MyObservableCollection(List<T> list) : base(list) {
        }

        public MyObservableCollection(IEnumerable<T> collection) : base(collection) {
        }

        private System.ComponentModel.ICollectionView GetDefaultView() {
            return System.Windows.Data.CollectionViewSource.GetDefaultView(this);
        }

        public int CurrentPosition {
            get {
                return this.GetDefaultView().CurrentPosition;
            }
        }

        public void MoveFirst() {
            this.GetDefaultView().MoveCurrentToFirst();
        }
        public void MovePrevious() {
            this.GetDefaultView().MoveCurrentToPrevious();
        }
        public void MoveNext() {
            this.GetDefaultView().MoveCurrentToNext();
        }
        public void MoveLast() {
            this.GetDefaultView().MoveCurrentToLast();
        }

        public bool CanMoveBack() {
            return this.CurrentPosition > 0;
        }
        public bool CanMoveForward() {
            return (this.Count > 0) && (this.CurrentPosition < this.Count - 1);
        }
    }

    public enum Navigation {
        First, Previous, Next, Last, Add
    }
}

更新:我添加了以下代码作为可能的解决方案,但我并不喜欢它,而且我希望有一个更好的解决方案出现,不需要我使用INotifyPropertyChanged——我怀疑我将重复所有应该已经在ObservableCollection中可用的功能。(我也不知道为什么我需要重新通知Count发生了变化。) 更新2:以下不是(完整的)解决方案,因为它会干扰集合的其他行为(通知),但我将它保留在这里,以防它包含任何有用的信息。
namespace ToDoApplication.Models {
    public class MyObservableCollection<T> : ObservableCollection<T>, INotifyPropertyChanged {
        public new event PropertyChangedEventHandler PropertyChanged;
        private int _currentPos = 1;

        public MyObservableCollection() : base() {
            this.GetDefaultView().CurrentChanged += MyObservableCollection_CurrentChanged;
            this.CollectionChanged += MyObservableCollection_CollectionChanged;
        }

        public MyObservableCollection(List<T> list) : base(list) {
            this.GetDefaultView().CurrentChanged += MyObservableCollection_CurrentChanged;
            this.CollectionChanged += MyObservableCollection_CollectionChanged;
        }
        public MyObservableCollection(IEnumerable<T> collection) : base(collection) {
            this.GetDefaultView().CurrentChanged += MyObservableCollection_CurrentChanged;
            this.CollectionChanged += MyObservableCollection_CollectionChanged;
        }

        void MyObservableCollection_CurrentChanged(object sender, EventArgs e) {
            this.CurrentPosition = this.GetDefaultView().CurrentPosition;
        }
        void MyObservableCollection_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e) {
            RaisePropertyChanged("Count");
        }

        private System.ComponentModel.ICollectionView GetDefaultView() {
            return System.Windows.Data.CollectionViewSource.GetDefaultView(this);
        }

        public int CurrentPosition {
            get {
                return _currentPos;
            }
            private set {
                if (_currentPos == value + 1) return;
                _currentPos = value + 1;
                RaisePropertyChanged("CurrentPosition");
            }
        }

        private void RaisePropertyChanged(string propertyName) {
            if (PropertyChanged != null) {
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
        }

        public void MoveFirst() {
            this.GetDefaultView().MoveCurrentToFirst();
        }
        public void MovePrevious() {
            this.GetDefaultView().MoveCurrentToPrevious();
        }
        public void MoveNext() {
            this.GetDefaultView().MoveCurrentToNext();
        }
        public void MoveLast() {
            this.GetDefaultView().MoveCurrentToLast();
        }

        public bool CanMoveBack() {
            return this.CurrentPosition > 1;
        }
        public bool CanMoveForward() {
            return (this.Count > 0) && (this.CurrentPosition < this.Count);
        }
    }

    public enum Navigation {
        First, Previous, Next, Last, Add
    }
}

通过这个,我可以在网格中显示“1/3项”:

    <TextBlock Grid.Row="2" Grid.Column="3" x:Name="txtItemOf">Item 
            <TextBlock x:Name="txtItem" Text="{Binding CurrentPosition}" /> of 
            <TextBlock x:Name="txtOf" Text="{Binding Count}" />
    </TextBlock>

我不再需要这个TextBlock,因为我可以直接在(主)StatusBar中引用DataContext属性:

    <StatusBar DockPanel.Dock="Bottom">
        Item <TextBlock Text="{Binding ElementName=vwToDo, Path=DataContext.CurrentPosition}" />
        Of <TextBlock Text="{Binding ElementName=vwToDo, Path=DataContext.Count}" />
    </StatusBar>

问题和解决方案

根据 @JMarsch 的回答:将我的属性命名为 CurrentPosition 会掩盖 DataContext 中已有的同名属性,因为绑定是到集合的默认视图(该视图具有此属性)。

解决方法要么是将其重命名为 MyCurrentPosition,并从 StatusBar 引用原始属性; 或者像我一样,彻底删除这个属性 (以及 GetDefaultView),它们没有做任何特别有用的事情。

然后,我使用以下简单的ValueConverter将 0、1、2、.. 转换为 StatusBar 中的 1、2、3、..。

[ValueConversion(typeof(int), typeof(int))]
class PositionConverter : IValueConverter {
    public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) {
        return (int)value + 1;
    }

    public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) {
        return (int)value - 1;
    }
}

状态栏:

    <StatusBar DockPanel.Dock="Bottom" x:Name="status">
        Item <TextBlock Text="{Binding ElementName=vwToDo, 
            Path=DataContext.CurrentPosition, Converter={StaticResource posConverter}}" />
        Of <TextBlock Text="{Binding ElementName=vwToDo, Path=DataContext.Count}" />
    </StatusBar>

底层集合的 Xaml 是否声明与当前项同步?将其设置为 true 可以对集合视图产生有益的副作用,从而可能会给您带来更好的结果。 - Gayot Fow
IsSynchronizedWithCurrentItem适用于像ListBox这样的ItemsControl。我没有使用这些控件,所以该属性不可用。(我使用“下一个”、“上一个”按钮在项之间移动。) - Andy G
http://stackoverflow.com/questions/5570332/currentposition-property-not-being-updated-properly 有用吗? - Gayot Fow
谢谢您提供的链接,但我认为那里没有任何东西适用于我的情况。 - Andy G
2个回答

3
关于绑定到集合的一些重要内容是,XAML 始终 绑定到一个集合视图,而不是集合本身。因此,即使您的 XAML 看起来绑定到了集合,但在运行时,您实际上是绑定到默认的集合视图。
这里有一个很酷的副作用:CollectionView 已经有一个 CurrentPosition 属性。我认为问题出在您无意中干扰了您的集合。
下面是一个非常简单粗暴的程序,它演示了如何绑定到 CurrentPostion 和 Count,而不需要在集合上定义 currentposition(因为在内部,您实际上是绑定到 CollectionView,它已经具有通知更改的 CurrentPosition 属性)。
运行此程序,并注意当您单击增量按钮时,UI 会相应更新。
以下是 XAML:
<Window x:Class="WpfApplication1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <StackPanel x:Name="ContentPanel">
        <Button x:Name="IncrementButton" Content="Increment" Click="IncrementButton_Click"/>
        <StackPanel Orientation="Horizontal">
            <TextBlock x:Name="CurrentPositionTextBlock" Text="{Binding CurrentPosition}"/>
            <TextBlock Text=" / "/>
            <TextBlock Text="{Binding Count}"/>
        </StackPanel>
    </StackPanel>
</Window>

这是代码的后台部分:
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Windows;
using System.Windows.Data;

namespace WpfApplication1
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            this.Collection = new TestObservableCollection<object>() {new object(), new object(), new object()};
            this.ContentPanel.DataContext = this.Collection;
        }

        public TestObservableCollection<object> Collection { get; set; }

        private void IncrementButton_Click(object sender, RoutedEventArgs e)
        {
            this.Collection.GetDefaultView().MoveCurrentToNext();
        }
    }

    public class TestObservableCollection<T> : ObservableCollection<T>
    {
        public ICollectionView GetDefaultView()
        {
            return CollectionViewSource.GetDefaultView(this);
        }
    }
}

以下是更多的阅读资料:

http://msdn.microsoft.com/zh-cn/library/ms752347(v=vs.110).aspx

http://msdn.microsoft.com/zh-cn/library/system.windows.data.collectionviewsource(v=vs.110).aspx

编辑

如果你想要更多的证明,可以将下面的代码复制到我的按钮单击处理程序中 -- 你会看到文本框绑定的实际对象类型是ListCollectionView,而不是实际的集合:

System.Diagnostics.Debug.WriteLine(this.CurrentPositionTextBlock.GetBindingExpression(TextBlock.TextProperty).ResolvedSource.GetType().FullName);

这个有效,谢谢。本质上,我的CurrentPosition版本掩盖了已经可用的默认版本。然而,它显示0而不是1。请问如何将1添加到该值?如果没有这个,我可能还需要为我的每个方法使用PropertyChanged事件,这将很麻烦。 - Andy G
1
没问题,ValueConverter可以解决这个问题。不过我还没有决定是否要采纳建议,我需要研究一下其他的建议。 - Andy G
值转换器是我会建议的!听起来你已经掌握了它! - JMarsch
1
你可以采用的另一种方法是有意识地使用CollectionView代替Collection,并明确绑定到它。CollectionView的目的是充当控件与集合之间的中间层。视图添加了当前记录、过滤和排序的概念。如果需要,你可以拥有2个不同的视图,具有不同的排序选项和不同的当前记录值,同时都访问同一基础集合。 - JMarsch

1

您的计数绑定正在更新,因为基础的ObservableCollection<T> 在添加和删除项时会触发 PropertyChanged 事件。

当您重新定位当前记录指针时,您的定位代码需要触发 PropertyChanged 事件,以便绑定子系统知道重新查询该属性,这也是 INotifyPropertyChanged 存在的原因。我可能会像下面这样编写它,注意使用ObservableCollection<T>OnPropertyChanged 来从您已实现的继承树中的INotifyPropertyChanged 引发正确的事件。

        public void MoveFirst() {
            this.GetDefaultView().MoveCurrentToFirst();
            OnPropertyChanged(new PropertyChangedEventArgs("CurrentPosition"));
        }
        public void MovePrevious() {
            this.GetDefaultView().MoveCurrentToPrevious();
            OnPropertyChanged(new PropertyChangedEventArgs("CurrentPosition"));
        }
        public void MoveNext() {
            this.GetDefaultView().MoveCurrentToNext();
            OnPropertyChanged(new PropertyChangedEventArgs("CurrentPosition"));
        }
        public void MoveLast() {
            this.GetDefaultView().MoveCurrentToLast();
            OnPropertyChanged(new PropertyChangedEventArgs("CurrentPosition"));
        }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接