DataGrid's Row Number does not update when changing the Index of Selected Record

Viewed 156

Goal

My goal is to simply move the records up or down and show their row number on the left side of the DataGrid.

Problem

When executing either MoveUpCommand or MoveDownCommand, the rows move, but the Row Number does not update.

See example below:

enter image description here

Summary of the GIF

I select the 1st row with the index of 0. I click the Move Down button to set the index +1 for the selected record in the list. But the problem is, there are now 2 rows with the same row number.

Question

Why is the other record not updating? And how would I update it?

What I have tried

I tried 2 different methods of getting the row numbers. My current code below uses a converter, the other method I have tried was using DataGrid_LoadingRow from this answer here: https://stackoverflow.com/a/4662928/12485722

They both have the same output.

Current Code

XAML

<Window x:Class="WpfApp1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfApp1"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Window.DataContext>
        <local:VM />
    </Window.DataContext>
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>

        <DataGrid ItemsSource="{Binding MainList}"
                  SelectedItem="{Binding SelectedRecord}"
                  CanUserAddRows="False"
                  EnableRowVirtualization="False">
            <DataGrid.Columns>
                <DataGridTextColumn Binding="{Binding RelativeSource={RelativeSource AncestorType=DataGridRow}, Converter={local:RowToIndexConverter}}" />
            </DataGrid.Columns>
        </DataGrid>

        <StackPanel Grid.Column="1">
            <Button Content="Move Up" Command="{Binding MoveUpCommand}"/>
            <Button Content="Mode Down" Command="{Binding MoveDownCommand}" />
        </StackPanel>

    </Grid>
</Window>

Model

public class Model
{
    public int Id { get; set; }
    public string ModelName { get; set; }
}

View Model

public class VM
{
    public ObservableCollection<Model> MainList { get; set; } = new ObservableCollection<Model>();
    public Model SelectedRecord { get; set; }

    public VM()
    {
        LoadData();

        MoveUpCommand = new RelayCommand(MoveUp);
        MoveDownCommand = new RelayCommand(MoveDown);
    }

    private void LoadData()
    {
        for (int i = 0; i < 50; i++)
        {
            MainList.Add(new Model { Id = i, ModelName = $"Model Version {i}" });
        }
    }

    public ICommand MoveUpCommand { get; }
    private void MoveUp(object param)
    {
        if (SelectedRecord != null)
        {
            int currentIndex = MainList.IndexOf(SelectedRecord);
            if (currentIndex > 0)
                MainList.Move(currentIndex, currentIndex - 1);
        }
    }

    public ICommand MoveDownCommand { get; }
    private void MoveDown(object param)
    {
        if (SelectedRecord != null)
        {
            int currentIndex = MainList.IndexOf(SelectedRecord);
            if (currentIndex != -1 && currentIndex < MainList.Count - 1)
                MainList.Move(currentIndex, currentIndex + 1);
        }
    }
}

Converter

public class RowToIndexConverter : MarkupExtension, IValueConverter
{
    static RowToIndexConverter converter;

    public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        DataGridRow row = value as DataGridRow;
        if (row != null)
            return row.GetIndex();
        else
            return -1;
    }

    public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        throw new NotImplementedException();
    }

    public override object ProvideValue(IServiceProvider serviceProvider)
    {
        if (converter == null) converter = new RowToIndexConverter();
        return converter;
    }

    public RowToIndexConverter()
    {
    }
}
2 Answers

RowToIndexConverter is called for each row. When the DataGrid is loaded, each row calls RowToIndexConverter once. And accepts the number accordingly.

When a row is moved, only that row will call RowToIndexConverter, so only the index of that row will be updated.

For some uses, you can not use anything, and this is one of them.

You can add a property called RowNumber to the model class and use it.

public class Model
{
    public int RowNumber { get; set; } 
    public int Id { get; set; }
    public string ModelName { get; set; }
}

And LoadData method

private void LoadData()
{
   for (int i = 0; i < 50; i++)
   {
      MainList.Add(new Model { RowNumber = i, Id = i, ModelName = $"Model Version {i}"});
   }
}

If you want the row numbers to be in order with any changes in the rows, you can use the following code.

Change Model to :

public class Model : INotifyPropertyChanged
{
   private int _RowNumber;
   public int RowNumber
   {
      get
      {
         return _RowNumber;
      }
      set
      {
         if (value != _RowNumber)
         {
            _RowNumber = value;
            OnPropertyChanged("RowNumber");
         }
      }
   }
   public int Id { get; set; }
   public string ModelName { get; set; }
   public event PropertyChangedEventHandler PropertyChanged;

   protected void OnPropertyChanged(string propertyName)
   {
      PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
   }
}

Change Moveup to :

private void MoveUp(object param)
{
   if (SelectedRecord != null)
   {
       int currentIndex = MainList.IndexOf(SelectedRecord);
       if (currentIndex > 0)
       {
          var currentItem = MainList.Where(a => a.RowNumber == currentIndex).First();
          var oldItem = MainList.Where(a => a.RowNumber == currentIndex - 1).First();
          oldItem.RowNumber = oldItem.RowNumber + 1;
          currentItem.RowNumber = currentItem.RowNumber - 1;
       }

       if (currentIndex > 0)
          MainList.Move(currentIndex, currentIndex - 1);
   }
}

Change MoveDown to :

private void MoveDown(object param)
{
    if (SelectedRecord != null)
    {
       int currentIndex = MainList.IndexOf(SelectedRecord);
       if (currentIndex < MainList.Count - 1)
       {
          var currentItem = MainList.Where(a => a.RowNumber == currentIndex).First();
          var oldItem = MainList.Where(a => a.RowNumber == currentIndex + 1).First();
          oldItem.RowNumber = oldItem.RowNumber - 1;
          currentItem.RowNumber = currentItem.RowNumber + 1;
       }
       
       if (currentIndex != -1 && currentIndex < MainList.Count - 1)
          MainList.Move(currentIndex, currentIndex + 1);
    }
}

There is no change notification for the rows whose index is changed because of the other row moving. They just don't notice their index has changed.
You will have to explicitly update all row indices affected. The converter seems kind of an obstacle here since it doesn't create a binding to a property path that notifies on change. Maybe have a look at these solutions, they're probably easy to use and integrate WPF 4 DataGrid: Getting the Row Number into the RowHeader

Related