Генератор отчетов на C# - Часть 2 (Чтение CSV и XLSX в dataGridView)

В прошлой части было составлено техническое задание для требуемой программы, а также был разработан внешний интерфейс. Теперь пришло время заняться непосредственно написанием кода. В этой части мы напишем код для автоматического открытия базы ОГРН (CSV) и открытия выгрузки (XLSX) в dataGridView.



Для всей дальнейшей работы с кодом следует добавить следующие директивы using в код MainForm.cs:

using System;
using System.Text;
using System.IO;
using System.Data;
using ExcelDataReader;
using ExcelLibrary;
using System.Windows.Forms;

Также следует скачать библиотеки ExcelDataReader и ExcelLibrary и добавить их в свой проект.

Метод загрузки CSV


Создадим метод для чтения CSV файла:

public DataTable CsvFile() // Метод загрузки CSV-файла
{
    DataTable dt = new DataTable("Base");
    DataColumn colOgrn = new DataColumn("ОГРН");
    DataColumn colAteId = new DataColumn("Код АТЕ");
    DataColumn colAteName = new DataColumn("Наименование АТЕ");
    // Добавляем столбцы в таблицу
    dt.Columns.AddRange(new DataColumn[] { colOgrn, colAteId, colAteName });
    try
    {
        DataRow dr = null; // Создаем пустую строку
        string[] ogrnValues = null; // пустой массив типа string
        // Создаем массив и заполняем его содержимым csv-файла.
        // Один элемент массива = одна строка из файла
        string[] ogrn = File.ReadAllLines(Application.StartupPath + "\\Base\\OgrnBase.csv", Encoding.Default);
        for (int i = 0; i < ogrn.Length; i++) // Проходим по массиву с базой
        {
            if (!String.IsNullOrEmpty(ogrn[i])) // Если элемент массива НЕ пустой
            {
                ogrnValues = ogrn[i].Split(';'); // Указываем разделитель "точка с запятой" и, полученные между разделителями элементы строки помещаем в массив ogrnValues
                dr = dt.NewRow(); // Создаем новую строку по шаблону dt
                dr["ОГРН"] = ogrnValues[0]; // Ищем столбец с именем ОГРН, добавляем туда значение 0 позиции ogrnValues
                dr["Код АТЕ"] = ogrnValues[1];
                dr["Наименование АТЕ"] = ogrnValues[2]; // Таким образом получаем строку для dt
                dt.Rows.Add(dr); // Добавляем полученную строку в DataTable dt
            }
        }
        butBrowseSelectionFile.Enabled = true;
        tbLogs.Text += Environment.NewLine + Environment.NewLine + DateTime.Now + ": Успешная загрузка базы ОГРН."; // Отчитываемся об успешной загрузке базы
    }
    catch (Exception ex) // Если базу загрузить не удалось
    {
        MessageBox.Show(ex.Message); // Выводим сообщение об ошибке
        // Отчитываемся о полученной ошибке в логах
        tbLogs.Text += Environment.NewLine + Environment.NewLine + DateTime.Now + ": Не удалось загрузить базу ОГРН. Проверьте наличие базы (Base\\OgrnBase.csv) в корневой папке программы. Также проверьте её состав (Шаблон: 'ОГРН; КОД АТЕ; НАИМЕНОВАНИЕ АТЕ;').";
        butBrowseSelectionFile.Enabled = false;
    }
    return dt; // Возвращаем DataTable
}

Метод CsvFile вызывается без аргументов и возвращает таблицу типа DataTable. Алгоритм метода:
  1. Инициализируем переменную dt типа DataTable.
  2. Инициализируем переменные colOgrn, colAteId, colAteName типа DataColumn.
  3. Добавляем полученные DataColumn в dt при помощи метода AddRange. Таким образом получается "заготовка" таблицы с данными по ОГРН.
  4. Инициализируем переменную dr типа DataRow и присваиваем ей значение null. В неё мы будем помещать строки из CSV-файла. 
  5. Инициализируем массив ogrnValues типа string и значением null для значений строки между разделителями. 
  6. Инициализируем массив ogrn типа string, в который помещаем значения строк в CSV-файле при помощи метода File.ReadAllLines. В аргументах метода мы указываем корневую папку приложения (Application.StartupPath) и добавляем к ней путь, по которому будет находиться база ОГРН, в моем случае это \Base\OgrnBase.csv.
  7. Начинаем цикл for с 0 и до конца массива ogrn.
  8. Если значение текущего элемента массива не пустое, то при помощи метода Split(';') мы получаем несколько элементов, разделенных точкой с запятой, из текущего элемента (по циклу) массива ogrn и записываем эти значения в массив ogrnValues. Присваиваем переменной dr схему новой строки dt с помощью метода dt.NewRow(). Записываем в dr значения из ogrnValues согласно имени столбца. После этого добавляем полученную строку в нашу DataTable.
  9. Цикл повторяется пока не будет достигнут конец массива ogrn.
  10. После цикла делаем активной кнопку выбора файла выгрузки (butBrowseSelectionFile) и с помощью tbLogs оповещаем пользователя об успешной загрузке базы.
  11. В случае форс-мажорных ситуаций (удаление, изменение файла базы и др.) приложение должно запретить пользователю дальнейшие операции с программой до исправления ошибки. Поэтому весь код от инициализации dr до оповещения об успешной загрузки базы мы помещаем в тело блока try.
  12. В блоке catch мы выводим диалоговое окно с сообщением о возникшей ошибке, также оповещаем пользователя об этом через логи и деактивируем кнопку выбора файла выгрузки.
  13. На этом код завершается и можно вернуть полученное значение dt при помощи команды return dt.
Для удобства использования программы (чтобы не приходилось при каждом запуске выбирать одну и ту же базу ОГРН) было решено загружать её автоматически при запуске из корневой папки. Для этого изменим конструктор формы MainForm следующим образом:

public MainForm()
{
    InitializeComponent();
    tbLogs.Text = "Отчет о работе 'Генератор отчетов ФИС ФРДО'" + Environment.NewLine + Environment.NewLine + "Запуск программы: " + DateTime.Now;
    dgvBase.DataSource = CsvFile(); 
}

После инициализации компонентов формы происходит запись в tbLogs приветственного сообщения с текущим временем. Источником данных для dgvBase назначается результат выполнения метода CsvFile().

Метод XlsFile


Для открытия XLSX-файлов я решил воспользоваться OpenSource-библиотекой ExcelDataReader. Таким образом, не имеет значения, какой офис установлен на машине пользователя и установлен ли он вообще.

public DataTable XlsFile(string pathToExcelFile) // Метод загрузки Excel-файла
{
    progressBar.Value = 0;
    progressBar.Minimum = 0;
    progressBar.Maximum = 100;

    FileStream stream = File.Open(pathToExcelFile, FileMode.Open, FileAccess.Read);
    IExcelDataReader excelReader; // присваиваем ExcelDataDeader переменной
    excelReader = ExcelReaderFactory.CreateOpenXmlReader(stream);
    progressBar.Value = 30;
    DataSet ds = excelReader.AsDataSet();
    ds.Tables[0].Rows[0].Delete();

    DataTable dt = new DataTable();
    dt = ds.Tables[0];
    progressBar.Value = 60;
    dt.Columns[0].ColumnName = "Название организации";
    dt.Columns[1].ColumnName = "ОГРН";
    int year = 2000;
    for (int i = 2; i < dt.Columns.Count - 1; i++)
    {
        dt.Columns[i].ColumnName = year.ToString();
        year++;
    }
    dt.Columns[dt.Columns.Count - 1].ColumnName = "Итого по ОО";
    progressBar.Value = 100;
    return dt;
}

Метод XlsFile принимает один аргумент типа string. В нем должен содержаться путь к XLSX-файлу, который требуется прочитать. Алгоритм метода:
  1. Устанавливаем стандартные значения свойств элемента progressBar (Value, Minimum, Maximum).
  2. Инициализируем файловый поток stream с методом File.Open(). В аргументах метода указываем путь к файлу, операцию открытия и доступ для чтения.
  3. Инициализируем библиотеку IExcelDataReader в переменной excelReader.
  4. Используем метод CreadeOpenXmlReader сторонней библиотеки для чтения данных из stream.
  5. Изменяем значение свойства Value у элемента progressBar на 30.
  6. Инициализируем переменную ds типа DataSet, присваиваем ей значение, полученное методом excelReader.AsDataSet(). Метод автоматически преобразует все данные файла Excel в формат DataSet.
  7. Удаляем первую строку первой таблицы DataSet, так как в ней находятся заголовки столбцов.
  8. Инициализируем переменную dt со значениями первой таблицы ds.
  9. Изменяем значение свойства Value у элемента progressBar на 60.
  10. Теперь мы подпишем столбцы в dt. Первые два и последний столбцы мы подписываем вручную. Остальные (данные с 2000 по 2018 года) подпишем в автоматическом режиме с помощью цикла for. Такое решение было принято чтобы сократить код и сделать программу более универсальной. Теперь даже если появится новый столбец (допустим, в следующем году появятся данные за 2019 год), то программа безошибочно определит его, и правильно подпишет заголовок.
  11. Написание метода завершается. Изменяем значение свойства Value у элемента progressBar на 100 и возвращаем полученную переменную dt.
Написанный метод мы будем применять в событии изменения значения Text элемента tbSelectionFilePath:

private void tbSelectionFilePath_TextChanged(object sender, EventArgs e) // Загрузка выборки в таблицу, форматирование таблицы
{
    // Проверяем, заполнен ли путь к файлу, существует ли файл по указанному пути
    if (File.Exists(tbSelectionFilePath.Text) != false)
    {
        butSaveReport.Enabled = false;
        tabControlPreview.SelectTab(1);
        dgvInitialData.DataSource = "";
        dgvWorkTable.Rows.Clear();
        dgvWorkTable.Columns.Clear();
        WriteToSummary(); // Создаем шаблон свода
        dgvInitialData.DataSource = XlsFile(tbSelectionFilePath.Text);
        tbLogs.Text += Environment.NewLine + Environment.NewLine + DateTime.Now + ": Успешная загрузка файла выгрузки (" + Path.GetFileName(tbSelectionFilePath.Text) + ")";
        butCreateReport.Enabled = true;
    }
}

Алгоритм работы кода:
  1. Кнопка сохранения отчета деактивируется (защита от ошибочных нажатий при последовательном создании нескольких отчетов)
  2. В элементе TabControl активируется вкладка "Файл выгрузки"
  3. Источник данных таблицы с выгрузкой очищается (защита от ошибочных нажатий при последовательном создании нескольких отчетов) 
  4. Очищаются строки и столбцы таблицы для обработанной выгрузки.
  5. Таблица со сводными данными заполняется шаблоном с помощью метода WriteToSummary(), который мы напишем позднее.
  6. Источником данных таблицы файла выгрузки указывается результат выполнения метода XlsFile().
  7. В tbLogs добавляется запись об успешной загрузке с адресом файла и текущем времени.
  8. Активируется кнопка создания отчета.

Изменяться значение tbSelectionFilePath.Text будет после клика по кнопке открытия файла выгрузки:

private void butBrowseSelectionFile_Click(object sender, EventArgs e) // Кнопка выбора файла выгрузки
{
    OpenFileDialog openSelection = new OpenFileDialog();
    openSelection.Filter = "Exel-файлы (*.xls; *.xlsx) |*.xls;*.xlsx";

    if (!String.IsNullOrEmpty(tbSelectionFilePath.Text))
    { openSelection.InitialDirectory = Path.GetDirectoryName(tbSelectionFilePath.Text); }

    if (openSelection.ShowDialog() == DialogResult.OK)
    { tbSelectionFilePath.Text = openSelection.FileName;}
}

Алгоритм работы:
  1. Инициализируем экземпляр класса OpenFileDialog. В свойстве Filter указываем расширения, которые должны отображаться пользователю.
  2. Если во время использования программы уже был открыт другой файл выгрузки, исходной директорией диалогового окна назначается путь к прошлому файлу.
  3. Если файл успешно выбран, значение tbSelectionFilePath.Text будет изменено на адрес путь к выбранному файлу.
На этом я хочу завершить статью. Надеюсь она была вам полезна. В дальнейшем мы разберем код создания отчета и сохранения его в XLS-файл с помощью библиотеки ExcelLibrary. Спасибо за внимание!