Southerneer
Southerneer

Reputation: 2012

WPF datagrid arrow (right/left) key navigation exception

I am binding a WPF Datagrid's ItemsSource property to an ObservableCollection. The data gets pulled in fine but I notice strange behavior when I click on a cell (any cell) and start using the keyboard to navigate.

  1. Tab key works as expected (left-to-right and wraps to the next line down).
  2. Up key does nothing (focus stays on the selected cell).
  3. Down key shifts the focus to the top cell in the column. For example, if I'm on row 10 of column B and hit "down" then the focused cell becomes row 0 of column B.
  4. Left or right keys cause an ArgumentOutOfRangeException. "Specified argument was out of the range of valid values. Parameter name: index". There is no InnerException and the Stack Trace isn't much help either.
  5. Double-clicking any of the cells doesn't enable edit mode.
  6. For some reason I get an "extra" row (rowcount=(collection count +1)) at the bottom of the grid that has correct functionality (right/left buttons work, double-click triggers edit mode). When I double click to enter edit mode in this extra row and then click in a bound row above it, an extra row is added to the grid.

I believe I've isolated the incident to the bound ViewModel class (as opposed to the XAML). In preliminary debugging I've stripped the XAML down to the bare minimum (no styles, 2 columns, only binding is the ItemsSource) and still get the weird behavior.

<UserControl x:Name="FuelMileageView" x:Class="TRD.RaceStrategy.Views.FuelMileage"
         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
         xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
         xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
         xmlns:local="clr-namespace:TRD.RaceStrategy"
         xmlns:views="clr-namespace:TRD.RaceStrategy.Views"
         xmlns:vm="clr-namespace:TRD.RaceStrategy.ViewModels;assembly=RaceStrategy.Support"
         xmlns:converters="clr-namespace:TRD.RaceStrategy.Converters;assembly=RaceStrategy.Support"
         xmlns:Behaviours="clr-namespace:TRD.RaceStrategy.Behaviours;assembly=RaceStrategy.Support"
         mc:Ignorable="d" DataContext="{Binding Path=Properties[PrimaryVM].CarEventVM.FuelMileage.FuelMileageLaps, Source={x:Static local:App.Current}}"
         d:DesignHeight="410" d:DesignWidth="485">

<DataGrid x:Name="dgFMLaps" ItemsSource="{Binding}"
            AutoGenerateColumns="False">
    <DataGrid.Columns>
        <DataGridTextColumn Header="Column 1" >
        </DataGridTextColumn>
        <DataGridTextColumn Header="Column 2" >
        </DataGridTextColumn>
    </DataGrid.Columns>            
</DataGrid>
</UserControl>

Aside from the InitializeComponent() call there is no codebehind to speak of which seems to leave the bound FuelMileageLaps collection as the only culprit.

public class FuelMileageLapViewModel : LapViewModel
{
    public FuelMileageLapViewModel() { }    
}

NOTE: ObservableCollectionEx is an extension of the ObservableCollection class which apparently accounts for threading problems (?) I've used this class with other collections that I've in turn plugged into datagrids that didn't have this keyboard navigation problem.

public class ObservableCollectionEx<T> : ObservableCollection<T>
{
    // Override the event so this class can access it
    public override event System.Collections.Specialized.NotifyCollectionChangedEventHandler CollectionChanged;

    protected override void OnCollectionChanged(System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
    {
        // Be nice - use BlockReentrancy like MSDN said
        using (BlockReentrancy())
        {
            System.Collections.Specialized.NotifyCollectionChangedEventHandler eventHandler = CollectionChanged;
            if (eventHandler == null)
                return;

            Delegate[] delegates = eventHandler.GetInvocationList();
            // Walk thru invocation list
            foreach (System.Collections.Specialized.NotifyCollectionChangedEventHandler handler in delegates)
            {
                DispatcherObject dispatcherObject = handler.Target as DispatcherObject;
                // If the subscriber is a DispatcherObject and different thread
                if (dispatcherObject != null && dispatcherObject.CheckAccess() == false)
                {
                    // Invoke handler in the target dispatcher's thread
                    dispatcherObject.Dispatcher.Invoke(DispatcherPriority.DataBind, handler, this, e);
                }
                else // Execute handler as is
                    handler(this, e);
            }
        }
    }
}

At this point I'm at my wit's end as to the next step for debugging. There's no obvious place to throw in a breakpoint or try/catch block. I've Googled my problem to death and haven't found anything worthwhile. Please help!

Here's the FuelMileageViewModel class where FuelMileageLaps is initialized:

public class FuelMileageViewModel : WorkspaceViewModel
{
    /// <summary>
    /// View model for the fuel mileage calculator
    /// </summary>
    /// <param name="car">Car on which to base all calculations</param>
    public FuelMileageViewModel()
    {}

    /// <summary>
    /// A separate collection of laps that store the extra fuel mileage data
    /// </summary>
    public ObservableCollectionEx<FuelMileageLapViewModel> FuelMileageLaps
    {
        get
        {
            if (_fuelMileageLaps == null)
            {
                _fuelMileageLaps = new ObservableCollectionEx<FuelMileageLapViewModel>();
            }
            return _fuelMileageLaps;
        }
        set
        {
            _fuelMileageLaps = value;
            OnPropertyChanged("FuelMileageLaps");
        }
    }
    private ObservableCollectionEx<FuelMileageLapViewModel> _fuelMileageLaps;

    /// <summary>
    /// Number of laps in the race
    /// Affects: Laps_G, Laps_Y
    /// </summary>
    public int NumberOfLaps
    {
        get
        {
            return FuelMileageLaps.Count;
        }
        set
        {
            int count = FuelMileageLaps.Count;

            if (value < 0)
            {
                throw new ArgumentException("Number of laps must be a positive integer");
            }

            if (count != value)
            {
                if( count < value )
                {
                    int diff = value - count;
                    for (int i = 0; i < diff; i++)
                    {
                        FuelMileageLapViewModel lapToAdd = new FuelMileageLapViewModel();

                        FuelMileageLaps.Add(lapToAdd);
                    }
                }
                OnPropertyChanged("NumberOfLaps");
            }
        }
    }
}

Upvotes: 2

Views: 2077

Answers (1)

FTLPhysicsGuy
FTLPhysicsGuy

Reputation: 1065

I know this is an older question, but I was having VERY similar problems with a DataGrid recently. The arrow key problems were just as you described. However, I used DataGridTemplateColumns in which a TextBox allowed for editing (otherwise, as you noted, I couldn't edit the data). If I edited a cell and then clicked out of it (particularly if I clicked the row above it) the DataGrid would often produce a duplicate row (though the count in the collection remained the same).

In my case, I found a bug in my Equals(object obj) method for the class whose members were in the ObservableCollection being bound to the DataGrid. This prevented calls to Equals from determining the true equality between two items, and somehow this caused big issues. For example, if after editing a row the data grid can't find that same row already in its list (via calls to a buggy Equals method), perhaps it generates another row (assuming it must not have a row yet for that entry).

In any case, I'd suggest checking out the Equals method in your LapViewModel.

Hope that helps.

Upvotes: 0

Related Questions