Pull to refresh

Дело о потерянных строках в DataView

Reading time3 min
Views1.9K
Отлаживая WPF-ный редактор для таблицы БД, столкнулся с Exception-ом при переходе между соседними ячейками с помощью клавиатуры.
Казалось бы, обычное дело — кто из нас эксепшенов не нюхал? Да и компонент DataGrid из WPF Toolkit весьма экспериментальный, так что удивляться нечему. Однако, кое-что меня насторожило.


Эксепшн IndexOutOfRangeException вывалился в DataGrid.cs на строке
object nextItem = Items[nextRowIndex];

И да, nextRowIndex действительно был out of range, он равнялся -1.

Теперь проследим, откуда он там берется. Объявлен он немного выше:
int currentRowIndex = Items.IndexOf(CurrentItem);
int nextDisplayIndex = currentDisplayIndex;
int nextRowIndex = currentRowIndex;

и до момента, где мы получаем эксепшн, его значение (в случае кнопки «вправо») не меняется.
Итак, ставим «бряк» на инициализацию nextRowIndex, воспроизводим действия с редактором… вуаля, currentRowIndex == -1!

Что же мы имеем? Items.IndexOf() говорит нам, что такого элемента не существует. Но мы-то знаем, что это не так. Проверим это в Watch:



CurrentItem равен соответствующему Item-у из коллекции, тут все честно. Индексы для соседних элементов вполне определяются. А вот конкретно «наш» элемент — не найден. Что ж, будем разбираться…

Сначала подозрение пало на собственно ItemCollection, экземпляром коей является Items.
После установки какого-то апдейта на фреймворк, к некоторым библиотекам из WPF (в частности, к интересующей нас PresentationFramework.dll) перестали подходить исходники, поэтому часть пути пришлось проделать в ассемблерном листинге. Однако, даже несмотря на это довольно быстро удалось выяснить, что ItemCollection является лишь оберткой для соответствующего CollectionView-а. А в нашем случае это BindingListCollectionView, который является дефолтным представлением для DataView.

Сам BindingListCollectionView, впрочем, тоже оказался оберткой и отправил вызов к IList.IndexOf() у DataView. После того, как он вернул все ту же «минус единицу», я отказался от мысли, что проблема в WPF-ной части.

Итак, как же работает IndexOf на DataView и почему он не находит нужной строки?

Анализ кода показал (да и в отладчике можно увидеть предпосылки к этому), что DataTable использует индекс для строк (R-B Tree). И DataView обращается именно к индексу таблицы в поисках DataRow, ассоциированного с искомым DataRowView.
Однако, если DataRow находится в режиме редактирования, он создает внутри себя временную запись с копией всех своих значений.
И оказывается, что DataRowView в этот момент ассоциирован именно с временной записью (иначе бы изменения в нем не отразились на самом DataRow), соответственно, ее-то он и пытается искать в индексе!

Вот небольшой пример, чтобы воспроизвести проблему:

static void Main(string[] args)
{
  var dataTable = new DataTable();
  dataTable.Columns.Add("Id", typeof (int));
  dataTable.Columns.Add("Name", typeof (string));

  var row = dataTable.NewRow();
  row["Id"] = 1;
  row["Name"] = "John";
  dataTable.Rows.Add(row);

  row = dataTable.NewRow();
  row["Id"] = 2;
  row["Name"] = "Jack";
  dataTable.Rows.Add(row);

  var view = dataTable.DefaultView;

  //row.BeginEdit();
  
  Console.WriteLine(((IList)view).IndexOf(view[0]));
  Console.WriteLine(((IList)view).IndexOf(view[1]));
}


* This source code was highlighted with Source Code Highlighter.


В исходном виде получим ожидаемые 0 и 1.
Если же раскомментировать row.BeginEdit(), то соответствующая строка перестанет находиться.

Кстати, если вместо DataRow вызвать BeginEdit() на соответствующем DataRowView, то (на первый взгляд) индекс строки будет выведен верно. Это происходит из-за того, что DataRowView использует ленивый вызов — BeginEdit() у связанного DataRow будет вызван лишь при изменениях в данных.

Резюме


Не знаю, насколько это поведение является корректным. Скорее всего это баг, впрочем, кудесники из .NET Team вполне могут объявить его фичей. В любом случае, следует избегать непрофильных операций с редактируемой строкой.
Или использовать более новые модели данных, вроде LINQ2SQL или EntityFramework. Впрочем, там вы не застрахованы от других сюрпризов :)

В качестве воркэраунда я добавил в DataGrid.cs перед Items.IndexOf(CurrentItem) это:
// BUG: item not found if in edit mode
if (IsEditingRowItem)
  CommitRowItem();

На этом дело можно считать закрытым.

Tags:
Hubs:
Total votes 41: ↑34 and ↓7+27
Comments19

Articles