Search code examples
c#wpfcollectionviewsource

Test if the current entry is the first or last entry in a CollectionViewSource


I am reading rows from an MS SQL Server table into a C# CollectionViewSource via Entity Framework. One row = one collection entry.

I use data binding to connect each CollectionViewSource entry's data elements to a WPF GUIs' controls. The user uses buttons on the GUI to page backwards and forwards through the collection entries using command handlers like the one below.

    private void DisplayNextRecordButtonCommandHandler(object sender, ExecutedRoutedEventArgs e)               //  Select the Next record for Display.
    {
        MyCollectionViewSource.View.MoveCurrentToNext();
        //Prevent the display of an "empty" record
        if (MyCollectionViewSource.View.IsCurrentAfterLast)
        {
            orgUnitAssetRskViewSource.View.MoveCurrentToPrevious();
        }
        selectedRecordPosition = orgUnitAssetRskViewSource.View.CurrentPosition;
    }

All worked well until I started including "SelectionChanged" and "TextChanged" events in my GUI ComboBox and Text controls. These events fire when I move to the next or previous entry in the collection. Everything works until I get to the first or last entries in the collection.

The "IsCurrentAfterLast" test doesn't stop me from paging past the last entry in the collection and when I do I get an "Object reference not set to an instance of an object" exception. I'm assuming that the exception is caused when the "SelectionChanged" and "TextChanged" events encounter spurious data before the first or after the last collection entries.

In the absence of something slick like "IsCurrentFirst" and "IsCurrentLast" can anyone suggest an efficient way to count the entries in the collection so that I can avoid moving past the first and last ones?


Solution

  • In the absence of something slick like "IsCurrentFirst" and "IsCurrentLast"

    Simple enough to create some extension methods on the ICollectionView abstraction to provide the desired functionality

    public static class CollectionViewExtensions {
    
        public static bool IsCurrentFirst(this ICollectionView view) {
            return view.CurrentItem != null && view.CurrentPosition == 0;
        }
    
        public static bool IsCurrentLast(this ICollectionView view) {
            if (view.CurrentItem == null) return false;
            var index = view.CurrentPosition;
            var max = view.Count() - 1;
            return index == max;
        }
    
        public static bool CanMoveCurrentToNext(this ICollectionView view) {
            return !view.IsCurrentLast();
        }
    
        public static bool CanMoveCurrentToPrevious(this ICollectionView view) {
            return !view.IsCurrentFirst();
        }
    
        static int Count(this ICollectionView source) {
            int count = 0;
            var e = source.GetEnumerator();
            checked {
                while (e.MoveNext()) count++;
            }
            return count;
        }
    }
    

    The extension methods should now allow for such checks.

    Creating some derived ICommand implementations that can be hooked directly to previous and next buttons.

    MoveCurrentToNextCommand

    public class MoveCurrentToNextCommand : ICommand {
        private readonly ICollectionView view;
    
        public MoveCurrentToNextCommand(ICollectionView view) {
            this.view = view;
            this.view.CurrentChanged += (s, e) => {
                CanExecuteChanged(this, EventArgs.Empty);
            };
        }
    
        public event EventHandler CanExecuteChanged = delegate { };
    
        public bool CanExecute(object parameter = null) => view.CanMoveCurrentToNext();
    
        public void Execute(object parameter = null) {
            if (CanExecute(parameter))
                view.MoveCurrentToNext();
        }
    }
    

    MoveCurrentToPreviousCommand

    public class MoveCurrentToPreviousCommand : ICommand {
        private readonly ICollectionView view;
    
        public MoveCurrentToPreviousCommand(ICollectionView view) {
            this.view = view;
            this.view.CurrentChanged += (s, e) => {
                CanExecuteChanged(this, EventArgs.Empty);
            };
        }
    
        public event EventHandler CanExecuteChanged = delegate { };
    
        public bool CanExecute(object parameter = null) => view.CanMoveCurrentToPrevious();
    
        public void Execute(object parameter = null) {
            if (CanExecute(parameter))
                view.MoveCurrentToPrevious();
        }
    }
    

    This simplifies binding with commands in a view model

    public ICommand Next => new MoveCurrentToNextCommand(MyCollectionViewSource.View);
    public ICommand Previous => new MoveCurrentToPreviousCommand(MyCollectionViewSource.View);
    

    Here are some unit tests on the commands for good measure.

    [TestClass]
    public class CollectionViewCommandsTests {
        [TestMethod]
        public void Should_Not_Move_Previous() {
            //Arrange
            var items = new[] { new object(), new object(), new object() };
            var view = new CollectionView(items);
            var expected = view.CurrentItem;
            bool changed = false;
            ICommand command = new MoveCurrentToPreviousCommand(view);
            command.CanExecuteChanged += delegate {
                changed = true;
            };
    
            //Act
            command.Execute(null);
    
            //Assert
            var actual = view.CurrentItem;
            actual.Should().Be(expected);
            changed.Should().BeFalse();
        }
    
        [TestMethod]
        public void Should_Move_Next() {
            //Arrange
            var items = new[] { new object(), new object(), new object() };
            var view = new CollectionView(items);
            var expected = items[1];
            bool changed = false;
            ICommand command = new MoveCurrentToNextCommand(view);
            command.CanExecuteChanged += delegate {
                changed = true;
            };
    
            //Act
            command.Execute(null);
    
            //Assert
            var actual = view.CurrentItem;
            actual.Should().Be(expected);
            changed.Should().BeTrue();
        }
    
        [TestMethod]
        public void Should_Not_Move_Next() {
            //Arrange
            var items = new[] { new object(), new object(), new object() };
            var view = new CollectionView(items);
            view.MoveCurrentToLast();
            var expected = view.CurrentItem;
            bool changed = false;
            ICommand command = new MoveCurrentToNextCommand(view);
            command.CanExecuteChanged += delegate {
                changed = true;
            };
    
            //Act
            command.Execute(null);
    
            //Assert
            var actual = view.CurrentItem;
            actual.Should().Be(expected);
            changed.Should().BeFalse();
        }
    
        [TestMethod]
        public void Should_Move_Previous() {
            //Arrange
            var items = new[] { new object(), new object(), new object() };
            var view = new CollectionView(items);
            view.MoveCurrentToLast();
            var expected = items[1];
            bool changed = false;
            ICommand command = new MoveCurrentToPreviousCommand(view);
            command.CanExecuteChanged += delegate {
                changed = true;
            };
    
            //Act
            command.Execute(null);
    
            //Assert
            var actual = view.CurrentItem;
            actual.Should().Be(expected);
            changed.Should().BeTrue();
        }
    }