- •В ведение в ado.Net
- •Потребитель данных
- •Поставщики данных
- •Источник данных — xml-файл
- •Элементы управления на форме Меню
- •Как устроены планки меню, статуса и панелей инструментов
- •Планка инструментов (ToolStrip)
- •Навигатор связей
- •Контейнер, расщепляющий форму
- •Создание таблиц и внедрение их в DataSet
- •Пояснения
- •Запись и чтение данных
- •Отображение данных связанной таблицы
- •Добавляем ограничение
- •Вторая, подчиненная таблица
- •Автоматическая навигация по записям связанной таблицы
- •Элемент управления BindingNavigator
- •Реакции на события в DataTable
- •Канонизация имен студентов
- •Вычисляемые колонки DataTable (Expression-Based DataColumn Objects)
- •Поиск в таблице DataTable и в компоненте DataGridView
- •Коррекция пользовательского интерфейса
- •Результат поиска
- •Поиск в DataGridView
- •Отбор данных из DataTable и отображение их в ListView
- •Как легко вносятся ошибки
- •Образ таблицы DataView
- •Методы Find и FindRows класса DataView
- •Задание
- •Элемент управления, который позволяет управлять отображением колонок DataGridView
Поиск в DataGridView
Анализ поведения приложения в режиме поиска, наводит на мысль о том, что целесообразно производить поиск не в таблице, поддерживаемой классом DataTable, а непосредственно в компоненте DataGridView. Хотелось бы сделать поиск более удобным.
Продолжать поиск со строки, следующей за той, которая в данный момент содержит фокус.
Зациклить процедуру поиска так, чтобы при прохождении всего списка можно было повторить ее сначала. Желательно сделать это без назойливого напоминания о том, что очередной цикл поиска закончился.
Пользователь должен иметь возможность искать в любой колонке как gridStud, так и gridExam.
Измените алгоритм метода Find.
void Find()
{
string colName = // Определите имя колонки (используйте индексы: searchTableID и searchColumnID
try
{
string text = Trim(comboFind.Text); // Искомый текст
if (findWhat != text)
findWhat = text;
if (bFindNext)
searchRowID++;
if (searchRowID < 0 || grids[searchTableID].Rows.Count <= searchRowID)
searchRowID = 0;
if (FindNext())
{
if (!comboFind.Items.Contains(comboFind.Text))
comboFind.Items.Add(comboFind.Text);
bFindNext = true;
return;
}
else
throw new Exception();
}
catch { }
SetErrorMsg(findWhat, colName);
}
Доверьте IntelliSense создание заготовки метода SetErrorMsg, затем введите код, как показано ниже.
void SetErrorMsg(string criteria, string colName)
{
string msg = "Could not find: '" + criteria + "'";
msg += colName != null ? " in column: " + colName : ". Select a column to search";
new FormMsg(msg, 2000);
}
Добавьте метод FindNext.
bool FindNext()
{
bool bFound = false;
DataGridView grid = grids[searchTableID];
for (int i = searchRowID; findWhat.Length > 0 && !bFound; ) // Цикл поиска
{
object o = grid[searchColumnID, i].Value; // ячейка в строке i и столбце searchColumnID
if (o != null)
{
string val = o.ToString();
if (Если текст в ячейке начинается с искомой строки)
{
bFound = true;
grid.CurrentCell = grid[searchColumnID, i];
// Выделите всю строку grid
// Запомните индекс строки в переменной searchRowID
break;
}
}
if (i == grid.RowCount - 1)
i = -1;
i++;
if (i == searchRowID)
break;
}
return bFound;
}
Проверка показывает удовлетворительное поведение приложения в режиме поиска, но здесь, конечно, остались подводные камни. Чтобы увидеть их, надо связать DataGridView с реальной таблицей базы данных, которая может содержать тысячи строк.
Отбор данных из DataTable и отображение их в ListView
Рассмотрим, как реализовать поиск в таблице DataTable (то есть, в памяти) и отобразить все данные, удовлетворяющие критерию поиска. Отобранное множество данных таблицы, возвращаемое методом Select (это — массив объектов DataRow), преобразуем в текстовые строки и заполним ими список (стандартный элемент управления ListView). Список ListView поместим в отдельную, временную форму, которая должна появиться рядом с главным окном.
Сейчас мы собираемся показать результат поиска в элементе ListView, а позже мы добавим еще один тип поиска: фильтрация данных с помощью объекта DataView. Результат поиска при этом будет отображен в DataGridView путем привязки к образу таблицы DataView.
В классе формы мы уже имеем объявление переменной Form formFound;
Добавьте метод, который создает эту вспомогательную форму, вычисляет ее координаты, вставляет в коллекцию дочерних элементов формы список с результатами поиска и отображает форму на экране.
void CreateFormFound(Control control, string text)
{
// Здесь ваш код. Создайте метку (Label msg) с текстом "Ищу то-то в такой-то таблице"
formFound = new Form
{
Text = "Search results",
MinimizeBox = false,
MaximizeBox = false,
Owner = this,
Width = Math.Max(msg.Width, control.Width) + 24,
Height = msg.Height + control.Height + 50,
Visible = true,
Location = new Point(Location.X + Width, Location.Y)
};
control.Location = new Point(3, msg.Height + 5);
formFound.Controls.Add(msg);
formFound.Controls.Add(control);
}
Сейчас мы разработаем алгоритм поиска. Если вы уже реализовали поиск в DataGridView, то добейтесь того, чтобы новый алгоритм не мешал старому (он должен функционировать, как и прежде). Окно вспомогательной формы formFound должно создаваться и обновляться при каждом нажатии на кнопку btnFindList.
Проверьте, что код инициализации переменных: grids, searchTableID и searchColumnID уже существует.
Добавьте код, который изменяет значениее searchTableID в реакции listTable на событие SelectedIndexChanged. Кроме того в этот момент надо заново заполнить список listColumn.
Добавьте код, который изменяет значение searchColumnID в реакции listColumn на событие SelectedIndexChanged. Последнее действие можно выполнить с помощью лямбда выражения.
listColumn.SelectedIndexChanged += (s, a) => searchColumnID = listColumn.SelectedIndex;
Добавьте код в заглушку метода GetSearchCriteria.
string GetSearchCriteria()
{
string criteria = null;
try
{
findWhat = Trim(comboFind.Text);
Type type = ds.Tables[searchTableID].Columns[searchColumnID].DataType;
switch (type.Name)
{
case "String": criteria = " LIKE '" + findWhat + "*'"; break;
case "Decimal":
case "Byte":
// Здесь ваш код. Добавьте ветви для всех числовых типов данных
case "Boolean":
case "Int32": criteria = "=" + findWhat; break;
case "DateTime":
DateTime dt;
bool ok = DateTime.TryParse(findWhat, out dt);
string
low = dt.ToShortDateString(),
hi = dt.AddYears(2).ToShortDateString();
if (ok)
criteria = " > '" + low + "' AND " + listColumn.Text + " < '" + hi + "'";
else
throw new Exception("Could not parse date string");
break;
}
return criteria;
}
catch (Exception ex) { new FormMsg(ex.Message, 3000); }
return null;
}
Метод GetSearchCriteria должен корректировать синтаксис SQL-запроса, в зависимости от типа данных активной колонки таблицы. Дело в том, что при выполнении запроса (метода Select) шаблон вида LIKE работает только для текстовых колонок таблицы. Для остальных типов колонок приходится искать точное совпадение данных. Колонка даты экзамена также вносит специфику в формат строки запроса. Так как синтаксис SQL-запроса для числовых и текстовых колонок отличается, то нам приходится производить ветвление алгоритма в зависимости от типа данных в колонке, определяемой индексом searchColumnID для таблицы, определяемой индексом searchTableID. Проверяемое множество типов данных превышает необходимый нам диапазон, но рассматривайте его, как запас прочности. Ведь в будущем мы можем изменить схему таблиц.
Добавьте код в заглушку метода ShowList. Здесь мы производим отбор строк выбранной таблицы путем вызова уже знакомого вам метода Select класса DataTable. Напомним, что он осуществляет поиск в памяти.
void ShowList(string criteria)
{
DataRow[] rows = null;
try
{
string filter = listColumn.Text + criteria;
rows = // Отфильтруйте строки нужной таблицы с помощью метода Select
if (rows.Length != 0)
{
// Здесь ваш код. Запомните искомый текст в выпадающем списке comboFind
CreateFormFound(CreateList(rows), criteria);
return;
}
}
catch (Exception ex) { new FormMsg(ex.Message, 2000); }
SetErrorMsg(criteria, listColumn.Text);
}
Критерий фильтрации, подаваемый на вход метода Select, зависит от активной колонки активной таблицы, поэтому мы вычисляем его таким образом:
string filter = <Имя колонки> + criteria;
Позвольте Intellisense создать заглушку для метода CreateList и добавьте в нее следующий код.
Control CreateList(DataRow[] rows)
{
ListView list = new ListView
{
View = View.Details,
FullRowSelect = true,
GridLines = true,
Width = 280,
Height = this.Height - 74
};
DataColumnCollection cols = rows[0].Table.Columns;
list.Columns.Add(cols[0].ColumnName, 30);
list.Columns.Add(cols[1].ColumnName, 100);
list.Columns.Add(cols[2].ColumnName, 100);
foreach (DataRow row in rows)
list.Items.Add(new ListViewItem(new string[] {
row[0].ToString(), row[1].ToString(), row[2].ToString()
}));
list.SelectedIndexChanged += listFound_SelectedIndexChanged;
return list;
}
В коде метода CreateList мы помещаем результат поиска, который пришел в массиве объектов DataRow, в список, а точнее в элемент управления вида ListView. Для этой цели можно было бы использовать другой элемент — DataGridView, но, когда нет необходимости редактировать данные, то ListView оказывается вполне приемлемым, и вам надо приобрести опыт его использования.
На следующем рисунке вы видите результаты поиска значений 4 в вычисляемой колонке среднего балла. Повторное нажатие кнопки btnFindList вызывает повторное создание (а, следовательно, и перерисовку) формы formFind. Подумайте, как это исправить. При попытке найти студентов со средним баллом 3,83 мы, скорее всего, получим отказ, даже если вы видите (в DataGridView), что такие студенты существуют. Причина в том, что поиск осуществляется по точному совпадению, а DataGridView настроен на отображение величин, округленных до двух знаков после запятой. Подумайте, как это исправить, то есть как заставить метод Select класса DataTable учитывать приближенное совпадение.
Колонка Date таблицы экзаменов также преподносит сюрпризы при выполнении метода ShowList. Просмотрите значения этих полей в XML-файле. Частью даты (объекта DataTime) является время: 2006-01-20T00:00:00+03:00. Символы +03:00, как вы догадались, являются региональным сдвигом.
При выполнении метода Select (то есть, SQL-запроса по дате) наличие времени мешает. Причина в том, что мы вынуждены искать точное совпадение, так как шаблон критерия отбора вида LIKE работает только для текстовых колонок таблицы. Нам, безусловно, не хочется вводить дату в таком сложном формате, да еще с учетом регионального времени. Если в XML-файле вручную убрать время (оставив только дату) и заново прочесть файл, то алгоритм фильтрации по дате работает правильно. Но при следующей записи в файл к данным вновь добавится время. Вместо метода ToShortDateString, который я (в целях экономии) использовал при формировании критерия отбора, надо просто использовать метод ToString.
Пока не понятно, как вообще избавиться от времени, которое, в нашем случае является лишним. Сложное лекарство было упомянуто ранее — при записи в файл не пользоваться методом WriteXml, а работать методами класса XmlDocument, на ходу управляя форматом даты.
Задание
Добавьте возможность поиска студентов по среднему баллу, значение которого попадает в интервал [min, max].
В данный момент команды поиска отдаются с помощью кнопок на панели инструментов. Реакцию на выбор одноименных команд меню, а также отслеживание их состояний, вы без особого труда реализуете самостоятельно. Если сочтете нужным, задействуйте и другие команды меню.
Добавьте возможность выбора строки в списке ListView и синхронизации (отслеживания) позиции в активном DataGridView. Это не так просто, потому, что ListView способен отображать данные обеих таблиц. При переходе к экзамену другого студента необходимо синхронизировать gridStud (прокрутить строки, выбрать строку и выделить ячейку). Если вы сделаете это правильно, то gridExam должен перегрузиться (отследить позицию) автоматически.
Детали
Полезно добавить возможность автоматического открытия файла последнего документа. Это должно происходить при начальном запуске приложения. Для этой цели в момент записи документа в файл мы создаем или открываем ключ Windows-реестра и запоминаем его файловый путь. Другой (более современный) способ — использовать global::Properties. Settings. Рассмотрим способ с использованием реестра.
В момент записи в файл производим запись в реестр:
RegistryKey key = Registry.CurrentUser.CreateSubKey(@"Software\MySoft\StudentsSet");
key.OpenSubKey("StudentsSet", true);
key.SetValue("Last File", fn);
В момент загрузки формы (в реакции на событие Load), мы обращаемся к реестру, узнаем путь к последнему файлу и открываем его.
void DoLoad()
{
SetDesktopLocation(20, 20);
InitDataSet();
RegistryKey key = Registry.CurrentUser.OpenSubKey(@"Software\MySoft\StudentsSet");
if (key != null && (fileName = (string)key.GetValue("Last File")) != null && File.Exists(fileName))
Open(fileName);
else
InitTables();
FillSearchBox();
RelateAndBind();
//. . .
}
Другим подходом (вместо использования Windows-реестра) считается использование XML-файла настроек Settings.settings. Такой файл уже есть в вашем проекте, но он пока не задействован. Его можно задействовать, добавив настройку типа string (например, с именем LastFile) и код вида:
fileName = Settings.Default.LastFile;
if (fileName != null)
Open(fileName);
else
InitTables();
В реакции на событие FormClosing можно добавить сохранение настройки:
Settings.Default.Save();
Просмотрите настройки в студии (раскройте узел Properties в окне Solution Explorer и откройте файл Settings.settings). Неприятность состоит в том, что так можно работать с настройками только уровня User (колонка Scope). Вам не удастся сохранить настройку уровня Application. Настройки уровня User хранятся в специальной папке с адресом:
C:\Users\Ваше имя\AppData\Local\Ваше приложение\...
Я очень не люблю все специальные папки, поэтому не пользуюсь настройками типа User.
