Поставляемый в составе Framework SDK DataGrid контроль имеет мощную
функциональность и почти всем хорош. Но есть у него определенные
недостатки, присущие всем гридам такого типа:
- DataGrid как и многие его аналоги прежде всего заточен для
In-place редактирования-заведения записей, что не всегда хорошо (я
лично просто ни когда не использую эту возможность редактируя записи
в диалогах). Для того, чтобы запретить это редактирование надо
сильно постараться.
- Нет стандартной возможности выводить значки в ячейках (можно
написать свой ClumnStyle, но опять же это не просто)
- Самое главное. Представим себе открытие формы, на которой лежит
грид, отображающий содержимое таблицы в 100 000 записей. Для
открытия формы необходимо закачать все 100 000 записей в память, что
занимает достаточно серьезное время (точно более 10 секунд).
Обновить содержимое грида - опять таки жать перезакачки.
Последнее соображение - самое главное. Ждать 10-20 секунд каждый раз,
когда надо обновить форму это слишком. Причем существует альтернативный
путь, который реализован в стандартном LuistView control - так
называемый virtual mode. В данном режиме контроль сразу задается общее
количество записей, но текст каждой записи контроль запрашивает по мере
необходимости, когда нужно отобразить соответствующую запись. В данном
случае нет необходимости сразу закачивать все записи в память, можно
открыть курсор и качать их по мере запросов ListView.
К сожалению, ListView класс из Framework SDK не позволяет
использовать данный режим - он просто не обвязан соответствующими
функциями. Ниже, я покажу, как можно доработать класс ListView для того,
чтоб использовать данный режим.
Итак, создадим класс, наследованный от ListView:
public class MyList : ListView {
}
Прежде всего, нужно включить в данном классе режим virtual mode, это
необходимо сделать задав соответствующий стиль окна при СОЗДАНИИ (нельзя
включать-выключать этот стиль когда окно уже создано). Это делается
переписыванием свойства CreateParams:
protected override CreateParams CreateParams {
get{
CreateParams p = base.CreateParams;
p.Style |= 0x1000;
return p;
}
}
Далее, в конструкторе класса надо задать стандартные стили ListView
необходимые нам для работы. Я использую FullRowSeect, SingleSelect ,
Report режимы. Впрочем, это дело вкуса.
Далее, нам понадобится возможность задавать количество элементов в
списке. Добавим метод:
const int LVN_FIRST =unchecked ((int)0xFFFFFF9C);
const int LVM_SETITEMCOUNT = LVM_FIRST + 47;
[DllImport("user32.dll")]
static extern bool SendMessage(IntPtr hWnd, Int32 msg, Int32 wParam, Int32 lParam);
public void SetCount(int Count) {
SendMessage(this.Handle , (Int32)LVM_SETITEMCOUNT, 1000, 0);
}
С этого момента следует заметить, что мы не используем коллекцию
Items класса ListView. Сам ListView с этого момента не обращается к
данной коллекции. Хранение данных следует вести самому
Итак, мы задали количество элементов. Теперь ListView будет
запрашивать данные о конкретном элементе путем посылки сообщения
родительскому окну. НО ! Родительское окно чаще всего не имеет
представления, о том, где брать данные и т.д. Гораздо удобнее всю работу
сосредоточить в самом классе, не закладываясь на то, что его родитель
что то знает о сообщениях и элементах. Сделать это можно ,
воспользовавшись message reflection механизмом посылки WM_NOTIFY
сообщений. Вкратце, перед тем как послать это сообщение родителю
контроль посылает его сам себе, смещая номер сообщения на некое число.
Итак, нам необходимо перехватить WM_NOTIFY + N сообщение. Для этого
переписываем виртуальную ф-ию WndProc:
// Здесь описания номеров сообщения
const int WM_USER = 0x0400;
const int WM_NOTIFY = 0x004E;
const int WM_REFLECT = WM_USER + 0x1C00;
const UInt32 LVN_GETDISPINFO = 0xFFFFFF4F;
const int WM_DESTROY = 0x02;
// Здесь описания необходимых структур, приходящих в LPARAM сообщений
[StructLayoutAttribute(LayoutKind.Sequential)]
struct NMHDR {
public UInt32 hwndFrom;
public UInt32 idFrom;
public UInt32 code;
}
[StructLayoutAttribute(LayoutKind.Sequential)]
struct NMLVDISPINFO {
public UInt32 hwndFrom;
public UInt32 idFrom;
public UInt32 code;
public UInt32 mask;
public Int32 iItem;
public Int32 iSubItem;
public UInt32 state;
public UInt32 stateMask;
[MarshalAs(UnmanagedType.LPWStr)] public String pszText;
public Int32 cchTextMax;
public Int32 iImage;
public Int32 lParam;
}
protected override void WndProc(ref Message m){
switch ( m.Msg ) {
case (WM_NOTIFY + WM_REFLECT):
NMHDR hdr = (NMHDR)m.GetLParam (typeof(NMHDR));
switch ( hdr.code ) {
case LVN_GETDISPINFO:
NMLVDISPINFO inf = (NMLVDISPINFO)m.GetLParam(typeof(NMLVDISPINFO));
inf.pszText = m_Items[inf.iItem];
inf.iImage = 0;
Marshal.StructureToPtr(inf, m.LParam , false);
break;
//case
default:
break;
}
break;
case WM_DESTROY:
break;;
default:
base.WndProc (ref m);
break;
}
}
Итак ! Что мы видим в коде. Прежде всего, перехватываем сообщение
WM_DESTROY и НЕ зовем базовый класс. Это сделано вот почему: если у вас
есть выделенная строка, то по закрытию ListView пытается освободить
некоторые внутренние структуры (список SelectedItems), которые ни кто не
заполнял (потому как с Items ни кто не работает). Возникает исключение.
Далее, мы перехватываем сообщение WEM_NOTIFY + WM_REFLECT, приводим
LPARAM к структуре NMHDR и проверяем, что за сообщение пришло:
NMHDR hdr = (NMHDR)m.GetLParam (typeof(NMHDR));
switch ( hdr.code )
Если это LVN_GETDISPINFO то мы приводим LPARAM к структуре
NMLVDISPINFO и заполняем inf.pszText поле. (тут должен стоять код,
извлекающий откуда то данные. Лично я тут проверяю, закачаны ли уже
записи по inf.iItem включительно, если нет, делаю fetch нужное
количество раз и подставляю запись) После чего структуру мы должны
запаковать обратно посредством Marshal.StructureToPtr(inf, m.LParam ,
false); вызова.
ВНИМАНИЕ ! У меня сложилось впечатление, что заполняя поле
inf.pszText некоей строкой туда кладется указатель на память , которая
содержит данную строку. Поэтому необходимо следить, чтобы данная строка
не очищалась сборщиком мусора до конца работы.
Примечание ! Иногда (я не уловил в каких случаях) операция
NMLVDISPINFO inf = (NMLVDISPINFO)m.GetLParam(typeof(NMLVDISPINFO));
приводит к исключению. Я решил эту проблему просто:
try {
NMLVDISPINFO inf = (NMLVDISPINFO)m.GetLParam(typeof(NMLVDISPINFO));
catch (Exception) {
return;
}
На функциональность не влияет, возникает очень редко. В чем дело -
пока не понял, но данный ход спасает от этой проблемы.
Теперь мы получили почти полноценный nonedit datagrid . Осталось
добавить в него код, позволяющий переносить окошки, включать-выключать
сортировки по нажатию на заголовок, сохранять размер и порядок окошек.
Но это не столь сложно и каждый может самостоятельно проделать это.