Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

StatusBar record x of y

Tags:

c#

wpf

I have an ObservableCollection which is the DataContext for a Grid. The Grid is a User Control inserted into the main Window.

I would like to display 'Record x of y' in the StatusBar so, as a first step, I am attempting to display it in the Grid using this XAML:

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

The Count works without issue, and updates automatically as new items are added. The CurrentPosition, which is defined in my code below, stays at 0 constantly.

How can I cause the CurrentPosition to update automatically? I am hoping not to have to use INotify** because this is already an ObservableCollection.

I also do not have any code-behind, so I hope it is achievable within my class (or model) and the XAML.

I did attempt to work with CurrentChanged but without success:

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

MyObservableCollection:

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
    }
}

Update: I'm adding the following code as a possible solution, but I don't really like it and I'm hoping a better one comes along that doesn't require me to use INotifyPropertyChanged - I suspect I'll end repeating all the functionality that should already be available with an ObservableCollection. (I also don't know why I need to re-notify of the Count changing.)

Update 2: The following is not a (full) solution as it interferes with other behaviours (notifications) of the collection, but I've kept it here in-case it contains any useful information.

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
    }
}

With this I can display "Item 1 of 3" in the Grid:

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

I no longer need this TextBlock though, as I can refer to the DataContext properties directly in the (main) StatusBar:

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

PROBLEM AND SOLUTION

Following @JMarsch 's answer: Naming my property CurrentPosition is masking the property of the same name that is already available directly from the DataContext, because the binding is to the collection's default view (which has this property).

The solution is either to rename to MyCurrentPosition, and refer to the original property from the StatusBar or, as I did, to remove my version of this property (and of GetDefaultView) altogether: they aren't doing anything particularly useful.

I then use the following simple ValueConverter to convert 0,1,2,.. to 1,2,3,.. in the StatusBar.

[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:

    <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>
like image 707
Andy G Avatar asked May 14 '14 17:05

Andy G


2 Answers

Something very important to know about databinding to collections is that XAML always databinds to a collection view, and not the collection itself. So, even though your XAML appears to bind to the collection, at runtime, you are really binding to the default collection view.

Here's a cool side-effect: A collectionview already has a CurrentPosition property. I think that things are breaking for you because you are inadvertently intervening with your your colleciton.

Below is a really quick and dirty little program that illustrates a working binding to CurrentPostion and Count, without ever defining a currentposition on the collection (because under the covers, you're really binding to the CollectionView, and it already has a CurrentPosition property that notifies on change.

Run this program, and notice that when you click the increment button, the UI updates appropriately.

Here's the 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>

Here's the code-behind:

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

Here's more reading for you:

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

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

EDIT

If you want a little more proof of what's going on, paste the code below into my button-click handler -- you will see that the actual object type that the textbox is bound to is a ListCollectionView, and not the actual collection:

System.Diagnostics.Debug.WriteLine(this.CurrentPositionTextBlock.GetBindingExpression(TextBlock.TextProperty).ResolvedSource.GetType().FullName);
like image 109
JMarsch Avatar answered Nov 01 '22 02:11

JMarsch


Your Count binding is updating because the base ObservableCollection<T> raises PropertyChanged when items are added and removed.

Your positioning code needs to raise PropertyChanged when you reposition the current record pointer, so that the Binding subsystem knows to requery the property, it's rather why INotifyPropertyChanged exists. I would probably write it like the below, though. Note the use of ObservableCollections<T>'s OnPropertyChanged to raise the right event from the already-implemented INotifyPropertyChanged in your inheritance tree.

        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"));
        }
like image 40
Tetsujin no Oni Avatar answered Nov 01 '22 01:11

Tetsujin no Oni