DataGridView. Новый контрол в составе Framework 2.0

Обзор архитектуры, свойств и возможностей. Практика использования.

Автор: Щербунов Нейл
Источник: RSDN Magazine #1-2006
Опубликовано: 23.08.2006
Версия текста: 1.0
Введение
История и предки
Общий обзор и составные элементы
Привязка данных
Общая архитектура
Колонки, строчки, ячейки...
Добавляем колонки
Добавляем строки
Заносим данные в ячейки. Режим свободных данных.
Работа в виртуальном режиме
Как работает DataGridViewCell
Шесть типов встроенных колонок
Если вам хочется задать значения ячеек новой строки по умолчанию, это делается в обработчике события DefaultValueNeeded.
Управление размером колонок и строк

Код к статье

Введение

Настоящая статья посвящена одному из самых мощных и сложных control-ов, входящих в состав .NET Framework 2.0, а именно, control-у DataGridView. Изначально я планировал включить ее как подраздел своей статьи “Исследование WinForms 2.0 (beta 2)”. Однако по достоинству оценив все великолепие 153-х свойств, 87-ми методов и 187-ми событий (и это только публичные члены!!!), я понял, что такой гранд-control явно заслуживает отдельной статьи. Справедливости ради отмечу, что к концу написания этой самой статьи мое мнение несколько изменилось, и теперь я считаю, что DataGridView на самом деле заслуживает отдельной книги. :) Как бы там ни было, я постарался использовать формат статьи максимально эффективно и не упустить практически ни единой хоть сколько-нибудь важной детали нового control-а. Рассмотрены не только свойства/методы/события как таковые, но и (по мере возможности) архитектура и идеология, приведшая к возникновению именно этих (а не иных) членов нового класса. Итак... DataGridView на сцене платформы .NET, встречаем!

История и предки

Как известно, в начале был Framework 1.0/1.1 и его приснопамятный control DataGrid. Сказать, что сообщество разработчиков было от него в таком уж диком восторге, было бы, пожалуй, чересчур. Неторопливый, местами не совсем логичный, с далеко не ясной и стройной архитектурой, он, конечно, выполнял свои прямые обязанности, но при этом ставил столько вопросов... В форумах и группах новостей этот “герой” был рекордсменом как по вопросам, непосредственно им самим спровоцированными, так и по жалобам на низкую эффективность, и по восклицаниям типа “а вот как же в нем не хватает!..”. Частенько звучали сетования на сложность (а местами и просто невозможность) настройки поведения данного control-а. Также нельзя сказать, чтобы DataGrid был таким уж “словоохотливым” в деле оповещения программиста о тех или иных действиях пользователя. Одним словом, предшественник исследуемого control-а получился так себе. Так что общего между двумя этими control-ами разве только то, что оба они являются реализацией grid-ов. DataGridView - это абсолютно новый control с совершенно иной архитектурой. Поэтому для разработчиков, много и часто работавших со старым DataGrid, у меня, как полагается, сразу две новости: хорошая состоит в том, что новый control вобрал и реализовал в себе массу пожеланий, оставленных программистами в форумах и письмах в Microsoft, на сайты поддержки и т.п. А плохая заключается в том, что знание (даже очень хорошее) DataGrid вовсе не освобождает их от необходимости внимательного чтения данной статьи. :) Посмотрим, что же такого нового предлагает DataGridView по сравнению с DataGrid. К примеру, лежащие на поверхности различия приведены в таблице 1.

DataGrid - Framework 1.0/1.1 DataGridView - Framework 2.0
Фактически 2 вида колонок: чтобы показать bool-значение и чтобы показать текст (DataGridBoolColumn, DataGridTextBoxColumn – припоминаете?). Гораздо большее разнообразие типов и способов представления информации. Более того – если вас все еще не удовлетворяет новая палитра колонок, добавление нового (вашего собственного) типа колонки происходит гораздо проще и изящнее.
Используется только внешний источник данных (тот же DataTable, к примеру). То же плюс может отображать непривязанные (иначе - свободные) данные, сохраненные в самом control-е, плюс комбинация первого и второго.
Умеренные (если не сказать бедные) возможности по настройке отображения данных. Целая куча событий и свойств направлены исключительно на то, что бы указать, как данные будут отформатированы и отображены. К примеру, внешний вид строк/колонок/ячеек может изменяться в зависимости от тех данных, что они содержат. Или, еще пример, данные одного типа могут быть заменены отображением эквивалентных данных другого типа.
За небольшим исключением “монолитный” control, позволяющий менять внешний вид себя только как единого целого. Индивидуальные компоненты контрола индивидуально же настраиваются. Примеры: строки/колонки могут быть “заморожены” для предотвращения их скроллинга; строки/колонки, а также их заголовки могут быть индивидуально скрыты; они же могут индивидуально выравниваться; способ, которым пользователь выбирает заинтересовавшие его записи, может быть изменен; строки/колонки/ячейки могут иметь индивидуальные всплывающие подсказки (ToolTips) и быстрые (shortcut) меню; и т.д.
Таблица 1

Замечу, что есть одна вещь, которую DataGrid делать умеет, а новый control – нет. Помните, когда у нас в DataSet-е были две таблицы, связанные соотношением master/detail, и мы привязывались к мастер-таблице, то на заголовках строк появлялись такие трогательные плюсики, щелкая по которым можно было просматривать соответствующие дочерние записи? В DataGridView взят на вооружение тезис “две таблицы? Значит и control-ов, их отображающих, тоже должно быть два”, т.е. теперь связанные подобным образом таблицы предлагается отображать в двух независимых DataGridView-control-ах. На мой взгляд, это совершенно логичное и идеологически верное решение. Так что и это как бы “упущение” нового control-а является лишь следствием выпрямления не совсем однозначной, чтобы не сказать сильнее, архитектуры старого.

ПРИМЕЧАНИЕ

Откровенно говоря, функциональность как функциональность, и никому ее наличие не помешало бы. Так что на наш взгляд речь идет просто о том, что разработчики Microsoft отказались от реализации этой возможности, сосредоточившись на других, более приоритетных задачах. – прим. ред.

Полагаю, что теперь от агитации (если она еще была нужна) за отказ от DataGrid и переход на DataGridView можно перейти к собственно описанию последнего.

Общий обзор и составные элементы

Рассмотрим элементный фундамент, на котором основывается вся эта функциональная мощь. В своей простейшей форме DataGridView имеет базисные компоненты, представленные на рисунке 1.


Рисунок 1.

Помимо базисных элементов и базисного внешнего вида у этого control-а есть базовое поведение. Иными словами, если поместить новый DataGridView на форму и не производить никаких спецнастроек, то control будет:

Помимо этого control будет поддерживать редактирование содержимого:

Если DataGridView привязан через свойство DataSource к источнику данных, то по умолчанию выполняется следующее.

Излишне говорить, что практически все из перечисленного выше может быть разрешено/запрещено/настроено.

Привязка данных

Как известно, прежде чем начать усиленно и красиво отображать данные, эти самые данные надо получить. DataGridView поддерживает три режима работы с данными:

  1. Первый, основной – отображение данных из внешних коллекций (например, ListView, DataTable).
  2. Специальный режим отображения свободных (unbound) данных, то есть данные хранятся в самом control-е.
  3. Еще один особый режим работы – виртуальный (Virtual mode). В нем control посылает событие, при поступлении которого прикладной код возвращает некоторые данные. Так как данные при этом не обязаны где-то храниться, виртуальный режим может оперировать миллионами строк без каких-либо проблем с производительностью или нехваткой памяти.

80% времени control будет работать в основном режиме, так как в большинстве случаев данные будут поступать из СУБД, при этом копируясь в промежуточные коллекции, например, DataTable.

Привязывать элементы пользовательского интерфейса можно отнюдь не исключительно к таблично представленным данным. Практически любая структура данных может выступить в роли их источника – обычные объекты, массивы, коллекции и т.д. Хотя вопрос привязки данных в мире WinForms (Windows Forms Data Binding) совершенно выходит за рамки данной статьи ввиду его масштабности, не упомянуть ключевые моменты этой технологии было бы несомненным упущением. Сжато исследуем вопрос – как рекомендуется привязывать DataGridView к данным, и чем Framework 2.0 может нас порадовать при сравнении с версиями 1.x.

В Framework 2.0 процедура привязки данных упростилась. Чтобы продемонстрировать это, разберем, как осуществлялась привязка данных во Framework 1.x (см рисунок 2).


Рисунок 2.

А что сегодня? Сегодня у нас новый герой – BindingSource (см. рисунок 3).


Рисунок 3.

Не правда ли – разница видна невооруженным глазом. Что же это за новый класс – BindingSource? Про него, на самом деле, тоже можно написать свою статью. Ну, уж заметку как минимум. :) Поэтому, стараясь оставаться в рамках поставленной цели, сжато опишу причины его возникновения и принципы работы. Приведенные выше иллюстрации показывают, что BindingSource представляет собой промежуточный слой между источником данных и control-ом, к нему привязанным. Также можно предположить (и совершенно обоснованно!), что, должно быть, этот класс взял на себя функциональность, ранее предоставлявшуюся CurrencyManager и PropertyManager. Второй из этих двух классов применялся очень редко, зато первый – весьма часто. Ведь надо же было как-то узнавать текущую позицию в коллекции, к которой привязан control, узнавать общее количество записей в ней, получать оповещения об изменениях этих записей и решать прочие подобные задачи. Но проблема была в том, что в .NET 1.x CurrencyManager создавался неявно. И значительная группа разработчиков, не давшая себе труда или не нашедшая времени детально разобраться в механизмах такого и вправду совсем не банального механизма, как Windows Forms Data Binding, попросту совершенно не представляла, как вообще подступиться к подобным вопросам? Да и те, кто вопрос изучил, были обречены продираться к CurrencyManager через дебри еще одного вспомогательного класса – BindingContext. Это была одна из причин возникновения нового класса – построить удобный и внятный интерфейс к функциональности CurrencyManager. BindingSource является, помимо всего, компонентом, и может быть помещен на форму. После этого он попадает на панель компонентов, и с ним можно работать через окно свойств. Удобно? Разумеется, главная задача – предоставить всю функциональность CurrencyManager – была с блеском выполнена. Знакомые и полюбившиеся свойства Count, Current, List, Position представлены непосредственно самим классом BindingSource. Видимо, предвидя, что переход с CurrencyManager на BindingSource может вызвать у особо впечатлительных натур приступы жестокой ностальгии по “старым и добрым временам”, авторы компонента даже снабдили его свойством CurrencyManager, возвращающим тот самый старый менеджер из 1.x. Иных побудительных мотивов возникновения этого свойства я не вижу, ибо новый класс включает в себя абсолютно все свойства/методы/события старого, да еще добавляет свои собственные, делая применение CurrencyManager сомнительным занятием. (думаем, все это было сделано из куда более прозаических соображений совместимости – прим.ред.) Итак, первый побудительный мотив – упростить работу с CurrencyManager. Вторая причина такова. Допустим, у нас есть Label, TextBox и ComboBox, привязанные к таблице (DataTable). Предположим, что для обновления информации был создан и заполнен данными из БД новый DataTable. Встает несложная, в общем-то, задача – сменить у всех трех control-ов источник данных. В 1.x задача решалась очень просто – ручками. Все три (тридцать три, как вариант) control-а перепривязывались к новой таблице, и дело с концом. Теперь можно изменить свойства DataSource/DataMember единственного BindingSource. Всё. Все три (или тридцать три) control-а привязаны к новому источнику. Вообще, BindingSource привнес массу улучшений в вопрос привязки данных. Чтобы заинтересовать читателя и подтолкнуть его к глубокому изучению данного класса, приведем пример.

Как известно, в 1.x для привязки control-а к коллекции “чего-нибудь”, эта коллекция обязана была реализовывать как минимум интерфейс IList. А это три свойства и семь методов. BindingSource умерил свои аппетиты до скромного интерфейса IEnumerable. Поэтому в мире Framework 2.0 вполне возможны подобные привязки:

      public
      partial
      class Form1 : Form
{
  public Form1()
  {
    InitializeComponent();
    //_biSour - объект типа BindingSource
    _biSour.DataSource = new PersonCollection(); 
    //_grid - обычный, без настроек, DataGridView
    _grid.DataSource = _biSour; 
  }
}

publicclass PersonCollection : System.Collections.IEnumerable
{
  public System.Collections.IEnumerator GetEnumerator()
  {
    for(uint i = 0; i <= 5; i++)
    {
      yieldreturnnew Person("Name_" + i.ToString(), 20 + i, 'M');
    }
  }
}

publicclass Person
{
  privatestring _name;
  privateuint _age;
  privatechar _gender;
....// свойства, инкапсулирующие эти три поля
public Person(string name, uint age, char gender) { ... }
}

Отображает grid с шестью записями. Заманчиво? :)

А каков общий подход при привязке любого WinForms-control-а к BindingSource? В общем случае – довольно несложный. Допустим, у нас есть DataSet NorthwindDataSet с единственной таблицей Products. Тогда первым шагом будет привязка BindingSource к этому источнику данных:

      // _biSour - объект типа BindingSource
      _biSour.DataSource = this.NorthwindDataSet;
// сразу привязываемся к конкретной таблице
_biSour.DataMember = "Products"; 

Теперь BindingSource сам становится полноценным источником данных. Единственное, что отличает его от "нормального" источника вроде того же DataTable, – отсутствие "собственных" данных, т.к. данные BindingSource – это данные нижележащего источника данных. Таким образом, при необходимости привязки свойства Text к колонке ProductName таблицы Products мы можем смело писать:

      this.label1.DataBindings.Add(
  new Binding("Text", _biSour, "ProductName", true));

Так же обстоит дело со сложной привязкой той же колонки к control-у, поддерживающему подобную привязку:

      this.comboBox1.DataSource = _biSour;
this.comboBox1.DisplayMember = "ProductName";

И совсем уже нехитрой представляется привязка DataGridView ко всей таблице Products:

      //_grid - обычный, без настроек, DataGridView_grid.DataSource = _biSour; 

Обратите внимание, что свойство DataMember в последнем случае остается незадействованным. BindingSource уже привязан к интересующей нас таблице, и уточнять путь к ней внутри DataSet-а необходимости нет.

В среде Framework 2.0 любую привязку визуального элемента формы к источнику данных настоятельно рекомендуется проводить только через класс-посредник BindingSource. Действуя так, вы просто не можете проиграть. В самом крайнем случае вы получите просто избыточную функциональность новообразованной связи, что, понятно, много лучше ее дефицита. Поэтому отныне вашим девизом должен стать: "Говорим Data Binding – подразумеваем BindingSource".

И, чтобы перекинуть мостик от "привязки любого WinForms-control-а" к "привязка конкретно DataGridView", отметим, что формальное требование к источнику данных у нового control-а осталось практически неизменным по сравнению с его предшественником, DataGrid. Единственное исключение выражается в требовании того, что свойства DataSource и DataMember теперь должны в сочетании определять некоторый список, то есть коллекцию, реализующую IEnumerable или интерфейсы, унаследованные от него, также возможно использовать в качестве источника данных компонент, реализующий IListSourse. Два свойства нужны, например, для того, чтобы указать некоторый DataTable, входящий в состав DataSet.

Отличие же от предшественника заключается в том, что DataGrid мог привязываться к коллекции коллекций, наподобие DataSet с несколькими таблицами внутри, после чего начиналась упомянутая иерархическая навигация по элементам всех коллекций. На рисунке 4 приведена блок-схема привязки.


Рисунок 4.

С практической точки зрения все эти хитросплетения означают, что DataGridView следует привязывать исключительно к BindingSource, который сам реализует один из требуемых интерфейсов (именно – IBindingListView) и позволяет привязываться к широкому диапазону источников данных. Вариант с источником, реализующим IEnumerable был рассмотрен выше. Кстати, если в процессе исполнения приложения нужно отслеживать изменения значений свойств DataSource и DataMember, можно воспользоваться событиями DataSourceChanged и DataMemberChanged.

Рассмотрим также событие DataGridView.DataBindingComplete. Оно будет сгенерировано как при изменении значения любого из двух упомянутых свойств, так и при наполнении control-а новыми данными (например, методом Fill адаптера данных). Нужно помнить только, что это все – события DataGridView, к самому источнику данных они никакого отношения не имеют. На практике же, чаще всего, интересны изменения данных именно в источнике, а не в control-е, эти данные отображающем. Для такого сценария (в который уже раз!) пригодится объект BindingSource с его замечательными событиями AddingNew, BindingComplete, CurrentChanged, CurrentItemChanged, ListChanged и целым рядом других. Остается в очередной раз досадовать, что подробное изучение этих событий увело бы нас в сторону от основной темы изложения.

Общая архитектура

Одной из особенностей DataGridView является обилие у него классов-компаньонов. Только не путайте их с классами, производными от DataGridView! Последних просто нет ни единого, пока вы сами не создадите таковой. А вот первых, классов-компаньонов, реально много. Таким образом, изучаемый control имеет расширяемую архитектуру, где значительную часть функциональности, доступной конечному пользователю, обеспечивают сторонние классы-компаньоны (или классы-плагины, если хотите). Идея хоть и не революционная, но от этого ничуть не менее блестящая. При таком подходе к вопросу, создавая свои собственные классы-плагины (или наследуя их от существующих и расширяя готовую функциональность), мы можем наращивать и видоизменять интересные нам аспекты поведения или внешнего вида конечного control-а практически до бесконечности. Впрочем, авторы control-а не поскупились и поставили вместе с ним вполне достойный набор готовых плагинов. Они сами образовали весьма раскидистое дерево классов. На рисунке 3 приведена парочка первых, самых крупных, его "ветвей".


Рисунок 5.

Какие выводы можно сделать, просматривая эту диаграмму? Ну, во-первых, совершенно ясно, что классы-плагины именуются строго по шаблону DataGridView<Назначение_Плагина>. Думается, что будет весьма правильно, если разработчики собственных плагинов будут следовать ему же.

ПРИМЕЧАНИЕ

Вообще-то, это грубое нарушение соглашений по именованию публичных интерфейсов, принятых Microsoft и весьма назойливо им же популяризируемых. Эти требования логично говорят, что для объединения группы классов нужно использовать пространства имен, а не префиксы или суффиксы. На самом деле, данный пример показывает, что в самом Microsoft не слишком строго соблюдают свои же соглашения. – прим.ред.

Базовым классом для всех плагинов DataGridView является класс DataGridViewElement. Любой элемент-плагин должен уметь сообщить, к какому DataGridView он "приписан", а также уметь сообщить, в каком состоянии он находится. В данном случае "состояние" – это комбинация потенциально возможных режимов отображения элемента. К примеру, элемент может сказать, что он доступен только для чтения (ReadOnly), и что он «заморожен» (Frozen, скроллинг запрещен). Так вот, первое из двух "умений" обеспечивается свойством DataGridView (тип DataGridView) базового класса, а второе – его же свойством State, возвращающим комбинацию значений перечисления DataGridViewElementStates. Оба свойства, разумеется, доступны только для чтения.

Классы-плагины подразделяются на две фундаментальные разновидности: ячейки (cells) и полоски (bands). Если с первыми все более-менее понятно, то что есть полоски? Собственно, это не более чем линейная коллекция ячеек. Основная идея такой коллекции заключается в том, что управлять группой ячеек много проще, чем каждой одиночной ячейкой. Базовых разновидностей полосок всего две (как и можно было предположить) – строки и колонки. Итак, полоски собирают ячейки в группы и управляют ими как единой сущностью. Базовым классом для создания новых полосок служит класс DataGridViewBand. Ячейки же наследуются от абстрактного класса DataGridViewCell. Кстати, отметьте для себя интересный момент, заголовки строк и колонок тоже являются ячейками, ибо наследуются от того же абстрактного класса. Что же, идея расширения чуть ли не моментально нашла совершенно осязаемое применение. Создали, фактически, обычные ячейки, наделили их особой функциональностью – и совершенно особый визуальный элемент получает путевку в жизнь. Стоит упомянуть, что хотя заголовки колонок образуют красивую горизонтальную полоску, а строк – не менее красивую вертикальную, и, казалось бы, ничто не может помешать нам записать тех и других в члены полосок (bands), тем не менее, сделав так, мы поступим неосмотрительно. DataGridViewHeaderCell и его наследники не считаются членами полосок. И это совершенно логично. Возьмем, к примеру, полоску-колонку. Каждая обычная ячейка такой полоски (не являющаяся заголовком), по сути, совершенно идентична своим собратьям, как по внешнему виду, так и по поведению. Заголовок же будет разительно отличаться по обоим параметрам. Ровно то же самое относится к строкам. Таким образом, наследники DataGridViewHeaderCell, будучи неразрывно связанными с соответствующими полосками, членами последних все же не являются. Обычные же ячейки, безусловно, являются членами полосок. DataGridViewCell не является наследником System.Windows.Forms.Control. Зато он может содержать control-ы. Обратите внимание, что если ячейка может редактироваться (как, например, ячейка с текстом), то почти всегда функциональность редактирования обеспечивает не она сама, а размещаемый в ней control. Есть и небольшая хитрость. Вы не можете просто взять и "положить" в ячейку обычный TextBox. Вы можете создать его наследника, но для размещения в ячейке нужно будет еще реализовать не очень сложный интерфейс – IDataGridViewEditingControl. В готовых к размещению control-ах DataGridViewComboBoxEditingControl, DataGridViewTextBoxEditingControl он уже реализован. Самостоятельное размещение произвольного control-а будет рассмотрено в специальном разделе ниже.

Колонки, строчки, ячейки...

Добавляем колонки

Добавление колонок – шаг, абсолютно необходимый перед тем, как control будет предъявлен конечному пользователю. Если мы не добавили ни одной колонки, DataGridView будет представлять собой тускло-серый прямоугольник на форме, совершенно лишенный какой-либо функциональности. Авторы control-а предусмотрели ряд способов добавления колонок. В самом общем случае мы сталкиваемся с четырьмя возможными сценариями:

  1. Есть источник данных, он доступен во время разработки, и мы готовы добавлять колонки в это время.
  2. Нет источника данных, но уже во время разработки мы знаем состав и тип колонок, и готовы добавлять их.
  3. Есть источник данных, но он доступен только во время исполнения, а во время разработки ничего не известно ни о нем, ни о составе колонок.
  4. Нет источника данных, а состав/тип колонок выясняется динамически, во время исполнения, а во время разработки неизвестен тип и, возможно, даже количество колонок.

Все прочие вариации сводятся к комбинациям этих четырех базисных сценариев. Еще раз напомню, что DataGridView позволяет в одном control-е иметь колонки, привязанные к данным, и колонки со свободными данными. Так что действительно любой из перечисленных сценариев может пересечься с любым другим, а то и не одним.

1. Источник данных доступен во время разработки

Простейший вариант. После задания значений свойств DataSource и DataMember control автоматически изучает схему источника и генерирует по колонке для каждой колонки таблицы или свойства объекта, коллекция которых используется как источник данных. Причем делает это "умно", подбирая не только подходящий заголовок колонки, но и тип колонки. Т.е. если тип колонки будет чем-то вроде int/decimal/string, то добавится колонка типа DataGridViewTextBoxColumn. А если такая колонка будет иметь тип boolean, то добавится уже DataGridViewCheckBoxColumn. Разумеется, в нашей власти удалить "лишние" с нашей точки зрения колонки, поправить текст заголовка, а также тип колонки. Вот как это делается. После задания значений свойств DataSource и, при необходимости, DataMember, мы уже имеем сгенерированные по описанному выше алгоритму колонки. Выделив grid в дизайнере, нажмем его "умный ярлык" (smart tag). "Умный ярлык" находится в верхнем правом углу control-а (причем почти любого) и предоставляет доступ к меню, состав элементов которого можно охарактеризовать как "наиболее часто используемые настройки" (рисунок 6).


Рисунок 6.

Меню позволяет делать с grid-ом много интересных вещей, но в рассматриваемом сценарии наиболее интересен пункт 'Edit Columns…'. При выборе этого пункта открывается диалог редактирования колонок (рисунок 7).


Рисунок 7.

В нем можно удалить лишние колонки (кнопка Remove), изменить заголовок колонки (свойство HeaderText), тип колонки (свойство ColumnType) и ряд других свойств каждой колонки. В списке Selected Columns слева показываются все колонки, причем их порядок "сверху-вниз" соответствует порядку "слева-направо" реального grid-а. Парой кнопок со стрелками можно менять их порядок в этом списке, автоматически меняя его же в реальном control-е. На рисунке 8 показан список иконок и соответствующих им типов колонок.


Рисунок 8.

Как видно из двух предыдущих иллюстраций, все поля таблицы будут отображаться колонками одного и того же типа – DataGridViewTextBoxColumn, за исключением колонки Discontinued, которые будет иметь тип DataGridViewCheckBoxColumn. Если предположить, что пользователю будут более понятны слова True/False, а не какие-то там коробочки с галочками – нет вопросов. Меняем у данной колонки 'ColumnType' на все тот же DataGridViewTextBoxColumn и получаем требуемое.

Резюме: в данном сценарии дизайнер делает основную работу, программист подправляет и проверяет его, а кодогенератор выдает статический код для описания нужных колонок.

2. Отсутствие источника данных в дизайн-тайм

В этом сценарии мы не устанавливаем значения свойств DataSource и DataMember, а готовимся работать в свободном, непривязанном режиме. Открываем все то же smart tag-меню, но выбираем 'Add Columns…' (рисунок 9).


Рисунок 9.

Поскольку источника данных нет (DataSource и DataMember выставлены в null), переключатель 'Databound column' не работает. Итак, нам доступен только переключатель 'Unbound column' и подчиненные ему поля. Задавая значения в этом окне, мы, фактически, задаем следующие свойства новой колонки:

Оставшиеся три переключателя помогают "на лету" задать соответствующие свойства колонки.

Характерно, что после нажатия на кнопку 'Add' данный диалог не закрывается, а предлагает новые значения по умолчанию – Column2, Column3 и т.д., позволяя очень быстро добавить изрядное их количество. Как уже было сказано, работа с разобранными в этом подразделе несвязанными колонками (т.е. наполнение их реальными данными) рассматривается в другом разделе. Пока мы просто учимся работать с колонками, безотносительно к данным.

Резюме: в данном сценарии программист указывает, какие колонки и в каком виде он хочет видеть.

3. Готовый источник данных, подключаемый во время исполнения

Сценарий, соперничающий по простоте со сценарием номер 1. Если свойство DataGridView.AutoGenerateColumns выставлено в true (а по умолчанию так и есть), то во время исполнения любое изменение свойств DataSource/DataMember вызывает генерацию колонок по алгоритму сценария 1. Можно также запустить (перезапустить) этот процесс генерации и добавления, установив упомянутое свойство в false, а потом вернув его в true.

Что произойдет, если сначала подключить grid к одному источнику, имеющему колонки Column1, Column2 и Column3, а после – к другому, имеющему колонки Column4, Column5 и Column6? Получим ли мы после такой операции grid с шестью колонками? Или только с тремя последними? Вообще было бы логично предположить, что перед очередной автогенерацией неплохо очистить существующую коллекцию колонок и начать все с "чистого листа". По счастью, авторы control-а, видимо, рассуждали именно так, поэтому верным будет второй ответ – результатом второй привязки будет grid с тремя колонками Column4, Column5 и Column6. Разумеется, зная этот алгоритм, можно после его окончания немного "поправить" результат работы генератора. Все колонки (в т.ч. сгенерированные) хранятся в коллекции DataGridViewColumnCollection, а доступ к ней производится через свойство Columns. Поэтому если есть опасения, что во время исполнения автогенератор задаст второй колонке слишком малую ширину и не очень правильный заголовок, можно поступить следующим образом:

_grid.AutoGenerateColumns = true; 
// источник должен содержать не менее 2-х колонок, 
// иначе следующая строка выдаст исключение_grid.DataSource =_biSour; 
_grid.Columns[1].Width = 188;
_grid.Columns[1].HeaderText = "MyHeader";

А что же произойдет при привязке к источнику с AutoGenerateColumns, выставленным в false? Да в общем-то, ничего интересного. Grid останется пустым или будет содержать колонки от старого источника. Но не данные! Зато это дает полный контроль над происходящим. Можно самостоятельно очистить коллекцию от старых элементов и начать наполнять ее новым содержимым, "выкраивая" каждую новую колонку по своему "лекалу". Этот сценарий перекликается со сценарием номер 4, он удобен, если вы любите держать под контролем каждый байт вашего кода.

Резюме: в данном сценарии программист пользуется коллекцией предварительно сгенерированных колонок, внося минимальные изменения.

4. Отсутствие источника данных во время исполнения

В данном случае нет иного выхода, кроме как поработать ручками и составить собственную коллекцию колонок. Первый шаг – определить тип колонок, которые хотелось бы видеть в grid-е. Поскольку колонки всех типов (и встроенные, и пользовательские) добавляются в grid одинаково, в данном разделе будет рассмотрена работа с самым распространенным типом колонки – DataGridViewTextBoxColumn. Работа со всеми прочими типами колонок абсолютно аналогична.

Есть следующие пути программного добавления колонок:

_grid.Columns.Add("MyColumnName", "MyColumnHeaderText"); 
_grid.Columns.Add(new DataGridViewColumn(...));
_grid.DataSource = null; //если до этого была привязка к источнику
_grid.ColumnCount = 5;

При этом будут созданы колонки, инициализированные значениями по умолчанию (пустыми строками и нулями), но это можно настроить отдельно.

О последнем способе добавления колонок стоит сказать особо. Во-первых, нельзя манипулировать этим свойством, если DataGridView привязан к данным. Поэтому первым шагом необходимо "отвязать" grid от источника, что и делает первая строчка приведенного примера. Но, во-вторых – если записать в это свойство число, меньшее, чем количество уже существующих колонок, то колонки, ставшие "лишними", будут просто отброшены. Причем сначала отбрасывается крайняя правая колонка (речь об экранном представлении), затем предшествующая ей, и так далее, вплоть до самой левой, которая останется в гордом одиночестве, вздумай вы присвоить ColumnCount число, равное единице. Впрочем, никакой трагедии – всегда можно явно указать, какие колонки нужно удалить. Для этого существуют методы Remove и RemoveAt коллекции колонок. Обратите внимание, что вторая строчка примера говорит лишь, что общее количество колонок должно стать равным пяти, а уж надо для этого колонки срезать или добавить, решается в зависимости от ситуации. Таким образом, "игры" с этим свойством могут рассматриваться как "быстрый, но грязный" метод регулировки числа колонок в grid-е. Да, и еще одно применение данного свойства: установка его в ноль эффективно очищает коллекцию колонок. Для этого же можно использовать метод Clear.

Резюме: в данном сценарии программист полностью отвечает за создание новых колонок и за их внесение в коллекцию. Настройка новых колонок также полностью лежит на нем.

Добавляем строки

После добавления колонок логично будет научиться добавлять строки. Сразу договоримся, что в данном разделе строки будут рассматриваться как полоски (bands), объединяющие ячейки. Нас пока интересует не наполнение строк реальным содержимым, а только их создание как независимых объектов с последующим помещением в коллекцию строк. Как вы, возможно, догадались, раз есть коллекция колонок, должна быть и коллекция строк. Действительно, свойство DataGridView.Rows (типа DataGridViewRowCollection) обеспечивает доступ к такой коллекции. Пользуясь им, можно добавить строки в grid. Но в отличие от целых четырех возможных сценариев добавления колонок, в случае добавления строк сценарий всего один. Дело в том, что добавлять строки во время разработки нельзя в принципе, так как свойство Rows помечено атрибутом Browsable(false) и не отображается в PropertyGrid. Добавить строки в DataGridView можно или программно, воспользовавшись методом Add коллекции строк, или подключив к нему некоторый источник данных.

Метод Add() имеет четыре варианта:

        // добавляет одну строку, заполняя ее значениями по умолчанию
        int Add(); 
// добавляет одну строку, заполняя ее значениями из массива valuesint Add(params object[] values);
// добавляет несколько строк, заполняя их значениями по умолчаниюint Add(int count);
// добавляет заранее созданную строкуint Add(DataGridViewRow dataGridViewRow); 

DataGridView допускает наличие в одной колонке ячеек разных типов! Что же для этого нужно сделать? Как Для этого сначала объект типа DataGridViewRow должен быть создан и заполнен отдельно. И только затем добавлен в grid. При этом количество колонок в строке должно соответствовать числу колонок grid-а.

Ниже приведен пример добавления переключателя в первую колонку третьей строки :

_grid.DataSource = null; 
// создадим 3 колонки типа DataGridViewTextBoxColumn 
_grid.ColumnCount = 3; 
_grid.Rows.Add();
_grid.Rows.Add();

DataGridViewRow newRow = new DataGridViewRow();
// Создаем ячейку типа CheckBox
DataGridViewCheckBoxCell checkCell = new DataGridViewCheckBoxCell();
checkCell.Value = true;
// Добавляем в качестве первой ячейки новой строки ячейку типа CheckBox
newRow.Cells.Add(checkCell); 
// Остальные ячейки заполняем ячейками типа TextBox
newRow.Cells.Add(new DataGridViewTextBoxCell()); 
newRow.Cells.Add(new DataGridViewTextBoxCell()); 
// эта строчка будет с переключателем в первой колонке
_grid.Rows.Add(newRow); 

Другой способ создания разнородных строк или ячеек – воспользоваться методом DataGridViewRow.CreateCells(). Этот метод заполняет экземпляр строки ячейками, считываемыми из экземляра DataGridView, указанного в качестве параметра. У этого метода есть два перегруженных варианта, второй из которых, кроме всего прочего, позволяет задать значения ячеек. Ниже приведен пример, в котором создается новая строка, описание колонок которой считывается из DataGridView, после чего одна из ячеек заменяется другой, с другим типом, после чего строка добавляется в DataGridView:

_grid.DataSource = null; 
_grid.ColumnCount = 3; 
DataGridViewRow heter_row = new DataGridViewRow();
// создаем строку, считывая описания колонок с _grid
heter_row.CreateCells(_grid);
// удаляем вторую ячейку
heter_row.Cells.RemoveAt(1); 
// и добавляем вместо нее комбинированный список
heter_row.Cells.Insert(1, new DataGridViewComboBoxCell()); 
// добавляем модифицированную строку _grid.Rows.Add(heter_row); 

Возможность "на лету" менять тип ячейки, и, следовательно, визуальное представление данных, представляется очень интересной и многообещающей возможностью, особенно с учетом потенциала создания ячеек собственного типа с нужным именно вам стилем отображения данных. Думаю, разработчики реальных проектов не преминут воспользоваться этой возможностью в самом скором времени.

Заносим данные в ячейки. Режим свободных данных.

Чаще всего данные попадают в DataGridView из подключенного источника данных. При этом встроенный механизм Windows Forms Data Binding автоматически заполняет каждую ячейку значением из соответствующей ячейки источника. Но, как вы уже знаете, новый control поддерживает также специальный режим отображения «свободных» (не привязанных, unbound) данных. Кроме того, поддерживается комбинированный режим одновременного отображения связанных и свободных данных.

Немного терминологии для начала... Свободными данными называются данные, заносимые в ячейки вручную. Связанными данными называются данные, заносимые в ячейки автоматически из привязанного источника данных. Будут ли данные считаться связанными или свободными, определяется на уровне колонок.

Колонка становится связанной, если в ее свойство DataGridViewColumn.DataPropertyName (типа string) заносится название колонки или свойства объекта из источника данных. Колонка считается свободной, если упомянутое свойство становится равным пустой строке.

Если свойство AutoGenerateColumns выставлено в true, DataGridView не только генерирует новый список колонок при подключении источника данных, но и привязывает эти колонки к колонкам или свойствам источника данных.

Однако это не означает, что нельзя управлять привязкой колонок после их автоматического формирования. Разберем следующий пример. Допустим, у нас есть две простые таблицы (рисунок 10).


Рисунок 10.

ПРИМЕЧАНИЕ

В ближайшем примере будет задействована только первая таблица – MyTbl1. Вторая будет задействована позже.

Следующий код подключает MyTbl1 к grid-у через BindingSource и, в демонстрационных целях, удаляет колонку City1:

_grid.DataSource = this.myTbl1BindingSource;
_grid.Columns.RemoveAt(2);

При этом остается две привязанные колонки, заполненные данными из БД:


Рисунок 11.

Теперь добавим третью колонку:

_grid.Columns.Add("additionalColumn", "FreeForStart");

и получим смешанный режим: две привязанные и одна свободная колонка:


Рисунок 12.

Наконец, привяжем только что созданную колонку grid-а к колонке из источника данных:

_grid.Columns["additionalColumn"].DataPropertyName = "City1";

Это опять создаст ситуацию, когда все колонки привязаны. Но на этот раз их три:


Рисунок 13.

Допустим, свободные колонки у нас есть. А как теперь работать с их ячейками? Другими словами – как добраться до конкретной ячейки grid-а и ее содержимого? Для этого нужно обратиться к индексатору Rows[], получить ссылку на конкретную строку (DataGridViewRow), а уже в ней, обратясь к индексатору Cells[] – ссылку на интересующую нас ячейку. Единственное, что надо учесть – ссылка будет иметь тип DataGridViewCell, а мы, чаще всего, будем заинтересованы в функциональности, присущей не любой ячейке вообще, а ячейке именно того или иного типа. Поэтому извлечение ячейки из коллекции практически всегда завершается приведением типа. Вернитесь к разделу "Добавляем строки" и еще раз посмотрите последний фрагмент кода. В нем мы создали grid 3х3, в котором все ячейки текстовые, за исключением ячейки с индексом 1 в строке с индексом 2 (2:1, далее для краткости будет использоваться такая запись). Указанная ячейка имеет тип DataGridViewComboBoxCell. Чтобы заполнить ее список четырьмя элементами, можно написать следующее:

DataGridViewComboBoxCell comboCell = (DataGridViewComboBoxCell)_grid.Rows[2].Cells[1];
//наполняем.....
comboCell.Items.AddRange(newstring[] { "VS2003", "VS2005", "MSDN", "RSDN" });

Упомянутое приведение типов в этом примере выделено красным.

Доступ к значению ячейки в общем случае осуществляется через свойство Value. Тип этого свойства – object. Реальный тип значения полностью зависит от типа ячейки. Например, в предпоследнем фрагменте раздела "Добавляем строчки" кода мы работали с ячейкой типа DataGridViewCheckBoxCell и присваивали этому свойству значения типа bool. А DataGridViewTextBoxCell будет работать, что вполне ожидаемо, со строками. Вот как можно занести значение "Great!" в четвертую строку колонки "FreeForStart" из приведенного чуть выше примера:

DataGridViewTextBoxCell txtCell = 
  (DataGridViewTextBoxCell)_grid.Rows[4].Cells[2];
txtCell.Value = "Great!";

В данном случае приведение типов избыточно, так как свойство Value есть у базового класса DataGridViewCell. Результат этого примера приведен на рисунке 14.


Рисунок 14.

У пытливых читателей статьи может возникнуть вопрос – а если попытаться манипулировать содержимым ячейки привязанной колонки? Так вот – привязка вовсе не запрещает разработчику менять содержимое ячеек по своему усмотрению. Следующий код работает как часы:

DataGridViewCell txtCell2 = _grid.Rows[2].Cells[1]; //первая колонка привязанная!
txtCell2.Value = "Also good!";


Рисунок 15.

При этом изменение значения ячейки отражено в привязанном источнике данных. Свойству Value можно присвоить не только строку, но и все, что может быть сконвертировано в строку.

Работа в виртуальном режиме

DataGridView поддерживает специальный виртуальный режим (Virtual mode) отображения данных. Основная идея такого режима заключена в том, что внутри control-а не хранится никаких данных. Вместо этого DataGridView генерирует события, в обработчиках которых программист может «подсунуть» ему данные, или наоборот, получить данные, введенные пользователем.

«Подсовывать» данные можно только свободным колонкам. Если колонка привязана к источнику данных, то, несмотря на использование виртуального режима, данные для нее будут браться из этого источника.

Колонки для виртуального режима добавляются уже известным нам методом Add коллекции колонок. Со строками все чуть-чуть сложнее. Если grid, находящийся в виртуальном режиме, подключен к источнику данных, то количество строк определяется источником. В противном случае количество строк может быть установлено через свойство RowCount. Строки можно также добавлять с помощью методов Add или Insert коллекции строк. Важно понимать, что метод Add физически не добавляет строк, а всего лишь изменяет значение свойства RowCount. Однако использование Add/Insert имеет некоторый дополнительный смысл, так как при этом, кроме собственно добавления строк, производится некоторая дополнительная работа, связанная с прокруткой, перемещением фокуса ввода и т.д.

Когда grid-у требуется узнать данные ячейки (например, для отрисовки или подбора оптимальной ширины колонки) в виртуальном режиме он генерирует событие CellValueNeeded. Его параметр «e» (типа DataGridViewCellValueEventArgs) имеет три свойства - RowIndex, ColumnIndex и Value. Первые два из них доступны только на чтение и предоставляют целочисленный индекс строки и колонки, соответственно. Свойство Value имеет тип Object и доступно на запись. Собственно, цель обработчика события заключается в том, чтобы на основе индексов колонки и строки вычислить значение ячейки и подставить его в свойство Value. Именно это значение и будет использовать DataGridView в качестве значения ячейки.

Чтобы в виртуальном режиме получить вводимые данные, необходимо реализовать обработчик еще одного события, CellValuePushed. Его параметр «e» имеет тот же тип, что и у события CellValueNeeded, но свойство Value используется не для задания значения ячейки, а наоборот, для считывания значения, введенного пользователем.

Ниже приведен пример примитивной электронной таблицы, данные в которой хранятся в хеш-таблице. Электронная таблица имеет большое количество ячеек. В данном примере это 65 535 строк * 26 (по числу букв английского алфавита) колонок = 1703910. Понятно, что если хранить значения каждой из них займут довольно много памяти. Однако в большинстве случаев в электронной таблице используется очень мало ячеек, которые, тем не менее, могут быть случайным образом разбросаны по всей таблице. Самый эффективный способ хранения в таких условиях – хеш-таблица, ключом которой является сочетание индексов строки и колонки.

Виртуальный режим DataGridView позволяет показывать пользователю всю таблицу (то есть все 1703910 ячеек), но выводит данные только для заполненных ячеек.

        using System.Collections.Generic;
using System.ComponentModel;
using System.Windows.Forms;

namespace WindowsApplication1
{
  public partial class Form1 : Form
  {
    public Form1()
    {
      ((ISupportInitialize)_grid).BeginInit();
      _grid.VirtualMode = true;
      _grid.CellValueNeeded += _dataGridView_CellValueNeeded;
      _grid.CellValuePushed += _dataGridView_CellValuePushed;
      _grid.Dock = DockStyle.Fill;
      // формируем 26 колонок с именами, соответствующими 
      // буквам латинского алфавитаfor (int i = 0; i < 'Z' - 'A'; i++)
      {
        string name = ((char)('A' + i)).ToString();
        _grid.Columns.Add(name, name);
      }

      _grid.RowCount = ushort.MaxValue;

      Controls.Add(_grid);
      ((ISupportInitialize)_grid).EndInit();
    }

    DataGridView _grid = new DataGridView();
    // хеш-таблица, хранящая данные заполненных ячеек
    Dictionary<int, object> _values = new Dictionary<int, object>();

    // вычисляет ключ по значениям индексов строки и колонки
staticint CalcKey(int rowIndex, int columnIndex)
    {
      return rowIndex + (columnIndex << 16);
    }

    // обработчик события, генерируемого при запросе данных grid-ом
privatevoid _dataGridView_CellValueNeeded(
      object sender, DataGridViewCellValueEventArgs e)
    {
      object value;

      if (_values.TryGetValue(CalcKey(e.RowIndex, e.ColumnIndex), out value))
        e.Value = value;
    }

    // обработчик события, генерируемого при изменении данных ячейкиprivatevoid _dataGridView_CellValuePushed(
      object sender, DataGridViewCellValueEventArgs e)
    {
      _values[CalcKey(e.RowIndex, e.ColumnIndex)] = e.Value;
    }
  }
}


Рисунок 16.

Интерес могут представлять также еще несколько событий. Событие NewRowNeeded вызывается, когда grid переходит в режим ввода новой строки. Заметьте, что при этом новая строка еще не создана, и если пользователь сойдет со строки, не введя никаких данных, новая строка создана не будет. Событие CancelRowEdit генерируется, когда пользователь отменяет редактирование или встаку строки. Событие UserDeletingRow генерируется, если пользователь удаляет строку целиком, выделив ее заголовок и нажав клавишу Del.

По ссылке http://msdn2.microsoft.com/en-us/library/2b177d6d(VS.80).aspx можно найти пример реализации работы с большими наборами данных с помощью виртуального режима работы control-а DataGridView. По приведенным там ссылкам можно найти и другие примеры работы с виртуальным режимом. Например, по ссылке http://msdn2.microsoft.com/en-us/library/ms171624.aspx доступен пример, в котором производится динамическая подгрузка данных из большой таблицы БД.

Как работает DataGridViewCell

Для понимания структуры DataGridView важно разобраться, как устроена работа с ячейками.

Значения ячеек

Главное в ячейках – их значения. В свободных колонках (если grid не находится в виртуальном режиме) значения хранятся в экземплярах ячеек. В случае ячеек, связанных с источником данных, ячейки вообще ничего не знают о значениях и не хранят их. Если требуется значение ячейки, grid обращается к источнику данных за значением. В виртуальном режиме, как уже говорилось, для получения значения ячейки используется событие CellValueNeeded. На уровне ячейки все это контролируется с помощью метода DataGridViewCell.GetValue().

За тип данных, хранящихся в ячейке (то есть в ее свойстве Value), отвечает свойство ValueType, указывающее ячейке, данные какого типа она должна хранить. По умолчанию считается, что в свойстве Value ячейки хранится объект неопределенного типа (ValueType равно object). Если изменить это значение на значение конкретного типа, ячейка начнет контролировать значение, конвертируя его в/из типа, указанного в свойстве ValueType.

При связывании колонки с источником данных ее свойство ValueType получает значение (соответствующее значению типа данных колонки/свойства источника данных), что приводит к изменению значения ValueType ячеек.

Форматирование для отображения

Для отрисовки ячейки grid должен получить значение ее свойства FormattedValue. Свойства FormattedValueType колонок и ячеек определяют тип, используемый для экранного отображения. В большинстве случаев используется тип string, но, например, в ячейках типа DataGridViewCheckBoxCell или DataGridViewImageCell используются другие типы. В DataGridViewImageCell как значение FormattedValueType по умолчанию используется тип Image. А в DataGridViewCheckBoxCell значение FormattedValueType по умолчанию равно ThreeState. На уровне ячейки все это контролируется с помощью метода DataGridViewCell.GetFormattedValue().

Важно понимать, что потенциально ячейки могут хранить значения любых типов данных. Имеет значение лишь возможность конвертирования этих типов данных в тип, указанный в FormattedValueType. По умолчанию DataGridView для этого преобразования использует TypeConverter-ы. Выбор нужного TypeConverter основывается на значениях свойств ValueType и FormattedValueType. Например, если имеется собственный класс Person и нужно отображать имя и фамилию, разделяя их запятой, то придется создать TypeConverter, преобразующий значение типа Person в string и обратно. Этот TypeConverter нужно ассоциировать с типом Person посредством атрибута TypeConverter. Если требуется только отображать данные, достаточно переопределить у класса Person метод ToString.

В случае ячейки FormattedValue запрашивается много раз. При каждой отрисовке ячейки или автоматическом изменении размеров колонки по содержимому ячейки, и даже при определении, находится ли курсор мыши над данной ячейкой. При каждом запросе FormattedValue DataGridView генерирует событие CellFormatting. Это дает возможность изменять форматирование ячейки.

Если ячейка не может получить значение FormattedValue, генерируется событие DataError.

Частью форматирования ячейки является определение ее правильных размеров. Размер складывается из комбинации FormattedValue ячейки, отступов и заполнителей, а также окантовки ячейки.

Отрисовка

За отрисовку ячейки отвечает сама ячейка. Кроме значения, получаемого через FormattedValue, при отрисовке используются стили, от которых зависят такие параметры, как выравнивание, перенос строк и т.п. Сама отрисовка производится внутри метода Paint, который переопределяется в наследниках класса DataGridViewCell. Кроме того, DataGridView посылает событие CellPainting, в обработчике которого можно изменить отрисовку ячеек, не изменяя собственного типа ячейки.

Разбор вводимого значения

Важно понимать, что при редактировании ячейки пользователь изменяет не ее значение, а значение ее FormattedValue. При сохранении значения FormattedValue нужно преобразовать обратно в тип, хранимый в ячейке. На уровне ячейки этим управляет метод DataGridViewCell.ParseFormattedValue (int rowIndex).

По умолчанию ParseFormattedValue использует для обратного преобразования TypeConverter-ы. DataGridView при этом генерирует событие CellParsing, давая возможность изменить способ разбора FormattedValue ячейки.

При невозможности корректного разбора FormattedValue генерируется событие DataError.

Шесть типов встроенных колонок

В приведенной в начале статьи схеме иерархии классов показано, что все колонки имеют одного предка – класс DataGridViewColumn. У этого класса есть важное свойство – CellTemplate типа DataGridViewCell. Это свойство (чаще всего его значение задают через перегруженный конструктор DataGridViewColumn(DataGridViewCell cellTemplate)) определяет, ячейки какого типа будут генерироваться при добавлении строк. Но установка этого свойства всего лишь определит, что именно генерировать автоматически при добавлении новой строки, и вовсе не запретит колонке иметь в своем составе ячейки других типов.

По сути, классы, производные от DataGridViewColumn просто передают своему базовому классу соответствующие значения свойства CellTemplate. Кроме того, эти производные классы задают значения ряда дополнительных свойств.

СОВЕТ

При создании ячейки собственного типа унаследуйте ее класс от DataGridViewCell и присваивайте ему имя вида DataGridView<ваш_тип>Cell. Одновременно с этим не забудьте унаследовать от DataGridViewColumn еще один класс с именем DataGridView<ваш_тип>Column.

В этом разделе мы кратко коснемся других типов ячеек и колонок. Разбор же процесса создания пользовательских типов будет сделан в одном из последующих разделов.

Давайте сразу же обратимся к коду короткого примера:

        using System;
using System.ComponentModel;
using System.Data;
using System.Windows.Forms;
using System.Drawing;

publicclass Form1 : Form
{
  public Form1()
  {
    DataGridView _grid = new DataGridView();
    _grid.Dock = DockStyle.Fill;
    _grid.AllowUserToAddRows = false;
    Controls.Add(_grid);
    _grid.Columns.Add(new DataGridViewTextBoxColumn());
    _grid.Columns[0].HeaderText = "TextBoxColumn";
    _grid.Columns.Add(new DataGridViewLinkColumn());
    _grid.Columns[1].HeaderText = "LinkColumn";
    _grid.Columns.Add(new DataGridViewButtonColumn());
    _grid.Columns[2].HeaderText = "ButtonColumn";
    _grid.Columns.Add(new DataGridViewCheckBoxColumn());
    _grid.Columns[3].HeaderText = "CheckBoxColumn";
    _grid.Columns.Add(new DataGridViewComboBoxColumn());
    _grid.Columns[4].HeaderText = "ComboBoxColumn";
    _grid.Columns.Add(new DataGridViewImageColumn());
    _grid.Columns[5].HeaderText = "ViewImageColumn";

    _grid.Rows.Add();

    for (int i = 0; i <= 3; i++)
    {
      DataGridViewRow heter_row = new DataGridViewRow();

      for (int j = 0; j < _grid.Columns.Count; j++)
        heter_row.Cells.Add(new DataGridViewTextBoxCell());

      switch (i)
      {
        case 0:
          heter_row.HeaderCell.Value = "Value";
          break;
        case 1:
          heter_row.HeaderCell.Value = "ValueType";
          break;
        case 2:
          heter_row.HeaderCell.Value = "FormattedValue";
          break;
        case 3:
          heter_row.HeaderCell.Value = "FormattedValueType";
          break;
      }

      _grid.Rows.Add(heter_row);
    }

    // Заполнение строки 0

    DataGridViewRow row0 = _grid.Rows[0];
    row0.HeaderCell.Value = "Внешний вид ячейки";

    DataGridViewTextBoxCell cell0 = (DataGridViewTextBoxCell)row0.Cells[0];
    cell0.Value = "dotNET";

    DataGridViewLinkCell cell1 = (DataGridViewLinkCell)row0.Cells[1];
    cell1.Value = "RSDN.ru";

    DataGridViewButtonCell cell2 = (DataGridViewButtonCell)row0.Cells[2];
    cell2.Value = "Accept";

    DataGridViewCheckBoxCell cell3 = (DataGridViewCheckBoxCell)row0.Cells[3];
    cell3.Value = true;

    DataGridViewComboBoxCell cell4 = (DataGridViewComboBoxCell)row0.Cells[4];
    cell4.Items.AddRange(newstring[] { "Trace", "Debug", "Release" });
    cell4.Value = "Release";

    DataGridViewImageCell cell5 = (DataGridViewImageCell)row0.Cells[5];
    cell5.ImageLayout = DataGridViewImageCellLayout.Zoom;
    cell5.Value = Image.FromFile(@"C:\WINDOWS\Blue Lace 16.bmp");

    // Заполнение строки 1for (int j = 0; j < _grid.Columns.Count; j++)
      _grid.Rows[1].Cells[j].Value = _grid.Rows[0].Cells[j].Value.ToString();

    // Заполнение строки 2for (int j = 0; j < _grid.Columns.Count; j++)
      _grid.Rows[2].Cells[j].Value =
       _grid.Rows[0].Cells[j].ValueType.ToString();

    // Заполнение строки 3for (int j = 0; j < _grid.Columns.Count; j++)
      _grid.Rows[3].Cells[j].Value =
        _grid.Rows[0].Cells[j].FormattedValue.ToString();

    // Заполнение строки 4for (int j = 0; j < _grid.Columns.Count; j++)
      _grid.Rows[4].Cells[j].Value =
        _grid.Rows[0].Cells[j].FormattedValueType.ToString();
  }
}

Результат исполнения этого кода показан на рисунке 17.


Рисунок 17.

Этот пример демонстрирует внешний вид ячеек каждого типа и значения некоторых свойств. Более детально информация, выводимая в примере, описана в таблице 2.

Метка строки Что показывает
Value Реальный объект, лежащий в ячейке на момент отрисовки последней. К этому объекту может быть применено автоматическое форматирование и конвертирование типа, если тип значения, записываемого в это свойство, отличается от типа, ожидаемого ячейкой.
ValueType Тип объекта, хранящегося в ячейке.
FormattedValue Значение, полученное после форматирования или конвертирования.
FormattedValueType Тип значения, полученного после форматирования или конвертирования
Таблица 2.

Ну вот, все готово для анализа и описания каждого из типов ячеек, используемых во встроенных колонках.

DataGridViewTextBoxCell

В ячейке этого типа может содержаться любой объект, имеющий разумную реализацию метода ToString().

Об этих ячейках и колонках уже, пожалуй, сказано вполне достаточно. Еще раз напомню, что именно они являются типом, используемым DataGridView по умолчанию. Если ячейка данного типа не является доступной только для чтения, и пользователь инициирует её редактирование (нажатием F2 или щелчком мыши), то внутрь ячейки помещается экземпляр control-а типа DataGridViewTextBoxEditingControl, которому передается текущее значение ячейки, на который и ложится вся функциональность редактирования "по месту". Этот control является наследником обычного TextBox и реализует интерфейс IDataGridViewEditingControl. Этот интерфейс должны реализовывать все редакторы, используемые в DataGridView.

Пока ячейка находится в режиме редактирования, ее значение не меняется, а редактор работает с его локальной копией. От него в любой момент можно отказаться, просто нажав на ESC. После ухода фокуса из такой редактируемой ячейки DataGridViewTextBoxEditingControl отбрасывается и происходит одно из двух:

DataGridViewLinkColumn

DataGridViewLinkColumn – это тип колонка, ячейки которой содержат ссылки. Это полезно при выводе значений URL или как альтернатива кнопке.

Этот тип колонок не генерируется автоматически при связывании данных, такие колонки приходится создавать вручную и вручную же привязывать к данным.

Для обработки щелчка по ссылке нужно создать и подключить обработчик события CellContentClick grid-а. В этом событии передаются только координаты ячейки, так что сами данные придется добывать вручную.

DataGridViewButtonColumn

Собственно, предыдущий абзац о ячейке со ссылкой можно было бы просто скопировать и вставить сюда. Ибо функционально эти оба типа ячеек абсолютно идентичны – активная область, при щелчке по которой "что-то происходит". Данная ячейка небезуспешно пытается прикинуться "взрослой" кнопкой и даже честно отрисовывает моменты нажатия и отпускания. Схема работы та же, что у DataGridViewLinkColumn, но обрабатывать надо событие CellClick.

DataGridViewCheckBoxColumn

Тип объекта, по умолчанию ожидаемого ячейкой при установке нового значения через свойство Value: bool/CheckState/null.

Обладает возможностью редактирования, но, в отличие от DataGridViewTextBoxColumn не пользуется для редактирования каким-либо специальным control-ом. Ячейка данного типа всегда находится в фазе редактирования – DataGridView.IsCurrentCellInEditMode для неё всегда вернет true. С помощью свойства ThreeState для ячейки можно включить поддержку трехпозиционного режима (включено, выключено и не определено). По умолчанию же режим двухпозиционный. В трехпозиционном режиме недетерминированному состоянию соответствуют значения CheckState.Indeterminate, null или 2.

DataGridViewComboBoxColumn

Безусловно, самый сложный среди всех встроенных типов ячеек. Похож на DataGridViewTextBoxColumn тем, что имеет специальную фазу редактирования, при которой использует для редактирования control типа DataGridViewComboBoxEditingControl. Понятно, что в целом ячейка данного типа пытается "прикинуться" обычным комбобоксом. Это удается ей лишь отчасти, поскольку настоящий комбобокс может иметь текстовую часть для прямого ввода значения. Данная же ячейка работает строго в режиме ComboBoxStyle.DropDownList, то есть никакого прямого ввода, только выбор из списка. Подписавшись на событие DataGridView EditingControlShowing можно заставить редактирующий control показать-таки поле ввода текста напрямую (режим ComboBoxStyle.DropDown). Но это уже будет разновидность "лёгкого хакинга". С помощью свойства AutoComplete (тип bool) можно включить встроенную функциональность автозавершения вводимых значений.

Свойство DropDownWidth (тип int) установит ширину выпадающего списка, а свойство MaxDropDownItems того же типа ограничит количество одновременно показываемых записей (если записей больше, чем установлено этим свойством – выпадающий список будет с вертикальной полосой прокрутки). Но, наверно, самым важным свойством для данного типа ячеек, наряду с необсуждаемым по степени важности Value, является свойство Items, возвращающее коллекцию ObjectCollection, в которую и заносятся элементы выпадающего списка. Это позволяет заполнить список вручную.

Примечательно, что DataGridViewComboBoxCell поддерживает собственную привязку к источнику, независимую от содержащего её grid-а! Для этого у нее имеются свойства DataSource, DisplayMember и ValueMember. Кстати, в последнем случае свойство Value возвращает не то, что видно пользователю (т.е. не DisplayMember выбранного элемента), а ValueMember этого элемента.

DataGridViewImageColumn

Еще одна не редактируемая ячейка, позволяющая показывать картинки и пиктограммы. Ячейка этого типа предоставляет ряд дополнительных свойств, среди которых можно выделить ImageLayout (принимает одно из значений перечисления DataGridViewImageCellLayout) определяющее, как будет вписываться в ячейку изображение, не совпадающее по размерам с прямоугольником ячейки. Отмечу также ValueIsIcon (тип bool). Его можно выставить в true, если нужно отрисовать объект типа Icon, а не типа Image.

Благодаря тому, что с типом Image ассоциирован TypeConvertor ImageConvertor, в качестве значения ячеек этого типа можно использовать массив байт, содержащий сериализованное изображение. Это обстоятельство особенно ценно при привязке колонки подобного типа к источнику данных БД, так как последний обычно хранит изображения именно как массив байт.

Если нужно изменить список элементов выпадающего списка, есть два пути. Первый позволяет изменить список элементов всех ячеек некоторой колонки, а второй – настроить списки отдельных ячеек. В первом случае нужно получить колонку, требующую настройки, привести ее к DataGridViewComboBoxColumn и через свойство Items добавить элементы:

          private
          void button3_Click(object sender, EventArgs e)
{
  ((DataGridViewComboBoxColumn)_grid.Columns[0]).Items.AddRange(
    newstring[] { "One", "Two", "Three", "Four" });
}

Во втором случае нужно получить ячейку, подлежащую настройке, и привести ее к DataGridViewComboBoxCell, и точно так же настроить список.

          private
          void button4_Click(object sender, EventArgs e)
{
  DataGridViewComboBoxCell cell =
    (DataGridViewComboBoxCell)_grid.Rows[2].Cells[0];
  cell.Items.Clear();
  cell.Items.AddRange(newstring[] { "ABC", "KLM", "XYZ" });
}

Обратите внимание, что сначала нужно настраиваемую ячейку "освободить" от доставшегося ей по шаблону списка. Если этого не сделать, то новый список "сольётся" со старым будет состоять из семи элементов:


Рисунок 18.

Если вам хочется задать значения ячеек новой строки по умолчанию, это делается в обработчике события DefaultValueNeeded.

Управление размером колонок и строк

Новый grid имеет на удивление развитую систему контроля над размерами колонок и строк.

Управление шириной колонок

Режим управления шириной колонки можно узнать из значения свойства InheritedAutoSizeMode конкретной колонки. Узнать, но не установить, так как свойство доступно только для чтения. Значение этого свойства вычисляется следующим образом. Если свойство колонки AutoSizeMode установлено в NoSet, то значение InheritedAutoSizeMode определяется значением свойства DataGridView AutoSizeColumnsMode. Иначе значение свойства InheritedAutoSizeMode соответствует значению свойства колонки AutoSizeMode. Другими словами, если для колонки не задан конкретный автоматический режим управления размером, используется общая для всего grid-a настройка. Это позволяет задать общий режим выравнивания для всех колонок grid-a и, если это нужно, изменить режим для отдельных колонок.

AutoSizeColumnsMode и AutoSizeMode имеют тип enum (DataGridViewAutoSizeColumnsMode и DataGridViewAutoSizeColumnMode, соответственно), и их значения различаются всего на один элемент NoSet, который есть у последнего и отсутствует у первого. В таблице 3 приведены члены перечисления DataGridViewAutoSizeColumnMode.

Значение Описание
AllCells Ширина колонок подбирается автоматически так, чтобы содержимое любых ячеек (и обычных и заголовочных) было видно целиком.
AllCellsExceptHeader То же, что и AllCells, но заголовочные ячейки в расчет не берутся.
ColumnHeader То же, что и AllCells, но в расчет берутся только заголовочные ячейки.
DisplayedCells То же, что и AllCells, но в расчет берутся только ячейки, реально отображаемые на экране.
DisplayedCellsExceptHeader То же, что и DisplayedCells, но заголовочные ячейки в расчет не берутся.
Fill Режим пропорционального масштабирования колонок при изменении размеров grid-a. Подробно рассматривается далее.
None (по умолчанию) Ширина колонок автоматически не подбирается.
NoSet Значение определяется настройками grid-a.
Таблица 3.

По умолчанию свойство InheritedAutoSizeMode любой колонки, не подвергавшейся какой-либо настройке, имеет значение None, так как это соответствует значению по умолчанию свойства AutoSizeColumnsMode grid-а, а значение AutoSizeMode по умолчанию равно NoSet.

Перейдем к многочисленным сценариям автоматического подбора ширины. Первый из них, AllCells, является самым универсальным и может быть рекомендован почти в любых сценариях автоподбора ширины.

Следующий режим, AllCellsExceptHeader, рекомендован если заголовок колонки содержит очень длинный, но не очень критичный текст. Применив этот режим, можно неплохо оптимизировать использование экранного пространства. Режим ColumnHeader логически противоположен предыдущему.

Следующая пара – DisplayedCells и DisplayedCellsExceptHeader по производимому эффекту аналогичны AllCells и AllCellsExceptHeader, соответственно. Но они не рассчитывают ширину всех ячеек колонки, а оценивают только ячейки, видимые в данный момент на экране. Проблема Displayed*-режимов в том, что они НЕ пересчитывают ширину колонки при прокрутке. Если после нажатия на PgDown в область видимости попадает ячейка с содержимым длиннее любой ячейки предшествующего экрана, такое содержимое срезается по последним символам. Но при этом изменение значений ячеек при редактировании "обслуживается" нормально – ширина колонки мгновенно меняется в нужную сторону. В качестве очевидного решения лежащего на поверхности проблемы со скроллингом можно предложить подписку на событие DataGridView.Scroll с вызовом в обработчике:

          // пересчитать нулевую колонку (её ширину),
// принимая во внимание только реально отображаемые ячейки
_grid.AutoResizeColumn(0, DataGridViewAutoSizeColumnMode.DisplayedCells);

Все эти режимы объединяет одно обстоятельство – если колонка находится в одном из них пользователь не может "схватиться" мышкой за разделитель и регулировать её ширину по собственному усмотрению. У нас остался последний, ну очень специальный режим – режим заполнения (Fill). Он кардинально отличается от разобранных, причем разница начинается уже с его взаимодействия с пользователем. Регулировать ширину Fill-колонки пользователь может так, как сочтет нужным, при условии, что таких колонок две или более. В чем же суть режима Fill?

Если среди колонок имеется одна или несколько колонок, помеченные типом автовыравнивания Fill, ширина этих колонок подбирается так, чтобы эти колонки разделяли между собой часть рабочей области grid-а, не занятую другими (не помеченными типом Fill) колонками. Если для обыкновенных колонок места не хватает, появляется горизонтальная полоса прокрутки, а каждой из Fill-колонок присваивается ширина из её свойства MinimumWidth (тип int, измеряется в пикселах). Минимальное значение этого свойства – всего 2 пиксела, по умолчанию оно равно 5. Оставшаяся часть grid-а делится между Fill-колонками не абы как, а в соответствии со значениями их свойства DataGridViewColumn.FillWeight (тип float). Вот он и определит, как делить остаток клиентской части.

Чуть ранее упоминалось еще одно свойство Fill-колонок – пользователь может менять их ширину по своему усмотрению. Собственно, он может менять только ширину колонок, режим заполнения у которых выставлен в None или Fill. Ну, с первым все понятно – пользователь фактически просто меняет значение Width. А в случае Fill-колонки он будет менять весовой коэффициент.

Управление высотой строк

Управление высотой строк возможно только на уровне всего grid-а через свойство AutoSizeRowsMode (тип – перечисление DataGridViewAutoSizeRowsMode). Упомянутое перечисление весьма похоже по составу на DataGridViewAutoSizeColumnMode и предлагает шесть режимов автоподбора высоты строки и один "ручной" режим. По умолчанию автоподбор высоты для строк выключен. Конкретные названия членов перечисления можно посмотреть в MSDN.

В разговоре об автоподборе высоты строки нельзя не коснуться важного свойства каждой отдельной ячейки, точнее, свойства ее стиля, (о стилях речь еще впереди) WrapMode (тип – перечисление DataGridViewTriState), доступного через свойство Style. Если его установить в DataGridViewTriState.True, то слишком длинная строка, не вмещающаяся в границы ячейки, будет переноситься. Но такой перенос вовсе не означает автоматической подгонки высоты ячейки по высоте всего "параграфа". Тут-то очень пригодится один из режимов автоформатирования высоты, хотя бы тот же AllCells:

_grid.AutoSizeRowsMode = DataGridViewAutoSizeRowsMode.AllCells;
_grid.Rows[7].Cells[0].Style.WrapMode = DataGridViewTriState.True;

Если хочется задать перенос не для одной ячейки, а для всей колонки, нужно либо задать перенос в свойстве колонки DefaultCellStyle, либо задать стиль для ячейки, помещенной в свойство колонки CellTemplate.


Эта статья опубликована в журнале RSDN Magazine #1-2006. Информацию о журнале можно найти здесь