Давайте перейдемо до суті. Вам, ймовірно, доведеться реалізувати функцію експорту в Excel. Експортувати не так вже й складно. Проблема, з якою ви, ймовірно, зіткнетеся, — це імпорт ваших даних, адже будь-хто може завантажити будь-яку форму дивних, спотворених даних. Тут на допомогу можуть прийти DTO.
Ця стаття покаже вам, як експортувати та імпортувати дані в Excel у вашому проекті на C#, а також як використовувати DTO для уникнення можливих проблем, які можуть виникнути.
Логіка імпорту/експорту в Excel
Ми використаємо окремий клас для управління логікою імпорту та експорту. Зручно використовувати JSON для цього, оскільки це дозволяє більш гнучко працювати з даними.
Цей клас обробляє дані у форматі JSON, пропонуючи гнучкість у тому, як ці дані маніпулюються і зберігаються. Для експорту дані формуються в структури, зручні для Excel, а для імпорту — дані з Excel читаються та перетворюються назад у JSON для подальшої обробки.
При експорті перший крок — створити заголовки на основі структури JSON-даних. Ці заголовки визначають стовпці в кінцевій таблиці Excel. Рядки даних потім заповнюються на основі вмісту JSON. Для імпорту заголовки читаються з першого рядка Excel-файлу, після чого читаються відповідні рядки для побудови об'єктів JSON. Ці об'єкти можна потім перевірити чи обробити далі.
using System;
using System.Collections.Generic;
using System.Linq;
using ClosedXML.Excel;
using Newtonsoft.Json.Linq;
namespace WindowsFormsApp1
{
internal class ExcelManager
{
// зберігає JSON як файл Excel за вказаним шляхом
public static void ExportJsonToExcel(string filePath, string jsonData)
{
JArray dataArray = JArray.Parse(jsonData);
using (XLWorkbook workbook = new XLWorkbook())
{
IXLWorksheet worksheet = workbook.Worksheets.Add();
// обробка дивно відформатованих JSON-даних
var headers = dataArray.First?.ToObject()?.Properties().Select(p => p.Name).ToList();
if (headers == null || !headers.Any())
{
throw new ArgumentException();
}
headers.Select((header, i) => worksheet.Cell(1, i + 1).Value = header).ToList();
for (int rowIndex = 0; rowIndex < dataArray.Count; rowIndex++)
{
JObject rowObj = (JObject)dataArray[rowIndex];
for (int colIndex = 0; colIndex < headers.Count; colIndex++)
{
string header = headers[colIndex];
worksheet.Cell(rowIndex + 2, colIndex + 1).Value = rowObj[header]?.ToString() ?? string.Empty;
}
}
// зберігає як акуратну таблицю
worksheet.Range(1, 1, dataArray.Count + 1, headers.Count).CreateTable();
workbook.SaveAs(filePath);
}
}
// читає файл Excel за вказаним шляхом як рядок JSON
public static string ImportExcelToJson(string filePath)
{
using (XLWorkbook workbook = new XLWorkbook(filePath))
{
IXLWorksheet worksheet = workbook.Worksheets.First();
JArray dataArray = new JArray();
List rows = worksheet.RowsUsed().Skip(1).ToList();
List headers = worksheet.Row(1).CellsUsed().Select(c => c.GetValue()).ToList();
foreach (IXLRow row in rows)
{
JObject obj = new JObject();
for (int i = 0; i < headers.Count; i++)
{
string cellValue = row.Cell(i + 1).Value.ToString();
obj[headers[i]] = cellValue;
}
dataArray.Add(obj);
}
return dataArray.ToString();
}
}
}
}
Створення DTO для Excel
Оригінальний клас даних, які ми будемо імпортувати/експортувати, — це клас Employee. Однак оскільки Excel-файл може не включати всі дані з оригінального класу або може відображати їх в іншому форматі, нам знадобиться окремий клас DTO для цієї мети.
DTO виконують роль посередників між додатком і даними, що обробляються. При роботі з імпортованими даними, DTO забезпечують перевірку даних, щоб гарантувати їхню консистентність і правильність.
Наприклад, у контексті даних про працівників, DTO може визначати правила для таких полів, як ім'я працівника, ідентифікаційний номер або статус зайнятості. Ці правила запобігають помилкам, викликаним неправильно відформатованими або відсутніми даними.
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using Newtonsoft.Json;
namespace WindowsFormsApp1.Models
{
// оригінальний клас
internal class Employee
{
public int EmplSeq { get; }
public string EmplName { get; set; }
public string Uidnum7 { get; }
public string EmplNum { get; set; }
public string EmploymentDate { get; set; }
public int IsSafe { get; set; }
public string GetDisplayUidnum7 => Uidnum7.Substring(0, 6) + "-" + Uidnum7.Substring(6, 1) + "******";
public string GetDisplayIsSafe => IsSafe == 1 ? "O" : "X";
public Employee(
int emplSeq,
string emplName,
string uidnum7,
string emplNum,
string employmentDate,
int isSafe
)
{
EmplSeq = emplSeq;
EmplName = emplName;
Uidnum7 = uidnum7;
EmplNum = emplNum;
EmploymentDate = employmentDate;
IsSafe = isSafe;
}
}
// DTO клас для Excel
internal class EmployeeExcelDto
{
// повідомлення про помилки можна за бажанням перекласти
[Required(ErrorMessage = "직원 이름을 입력해야 합니다.")]
public string EmplName { get; set; }
[Required(ErrorMessage = "주민등록번호 (앞 7자리)를 입력해야 합니다.")]
[MinLength(7, ErrorMessage = "주민등록번호는 최소 7자리를 입력해야 합니다.")]
public string GetDisplayUidnum7 { get; set; }
[JsonIgnore]
public string Uidnum7 => GetDisplayUidnum7.Trim().Replace("-", "").Substring(0, 7);
[Required(ErrorMessage = "직원 번호를 입력해야 합니다.")]
public string EmplNum { get; set; }
[Required(ErrorMessage = "안전 여부를 입력해야 합니다.")]
[RegularExpression(@"[OX]", ErrorMessage = "안전 여부는 'O' 또는 'X'여야 합니다.")]
public string GetDisplayIsSafe { get; set; }
[JsonIgnore]
public int IsSafe => GetDisplayIsSafe.Trim() == "O" ? 1 : 0;
public string Validate()
{
var results = new List();
var context = new ValidationContext(this);
bool isValid = Validator.TryValidateObject(this, context, results, true);
return isValid ? null : results.First().ErrorMessage;
}
}
}
Крім того, DTO дозволяє перевіряти неправильні введені дані під час імпорту з Excel. Оскільки форми Excel дуже відкриті, це, ймовірно, спричинить проблеми при імпорті даних у правильному форматі. Використання заздалегідь визначеного DTO для цієї мети дозволить запобігти та перевірити неправильно введені дані.
Ми можемо використовувати метод Validate()
, щоб повернути повідомлення про помилку з DTO та використовувати його на рівні представлення для того, щоб користувач знав, в чому саме полягає причина неправильно введених даних.
Ви навіть можете зробити крок далі та створити базовий клас-предка ExcelDto
з методом Validate()
, і наслідувати його в своїх дочірніх класах DTO.
Окрім перевірки, DTO також можуть форматувати дані для відображення чи обробки. Наприклад, DTO може форматувати ідентифікаційний номер у більш зручну для сприйняття форму, залишаючи при цьому сирі дані для внутрішнього використання. Це забезпечує те, що дані, які показуються користувачу, є зручними для сприйняття, а внутрішня логіка програми залишається стабільною та послідовною.
Реальне використання
Ось приклад для програми на Windows Forms.
Однак, цю ж логіку можна застосувати в будь-якому проекті.
Якщо ви хочете обробляти переклади для заголовків (міток) кожної властивості в Excel файлі, обов'язково включіть словник для відображення властивості та її перекладу, щоб він міг бути правильно застосований і переведений назад під час імпорту даних.
// словник для заголовків (якщо потрібен переклад)
private static readonly Dictionary ExcelHeaderDictionary = new Dictionary()
{
{ nameof(EmployeeExcelDto.EmplName), "직원 이름" },
{ nameof(EmployeeExcelDto.GetDisplayUidnum7), "주민등록번호 (앞 7자리)" },
{ nameof(EmployeeExcelDto.EmplNum), "직원 번호" },
{ nameof(EmployeeExcelDto.GetDisplayIsSafe), "안전 여부 (O/X)" }
};
Цей словник перекладів усуває розрив між внутрішніми іменами властивостей, що використовуються в вашій програмі, та мітками, які користувачі бачать у файлі Excel. Це особливо корисно, коли потрібно працювати з багатомовними середовищами або спеціальними вимогами щодо термінології.
Наприклад, візьмемо властивість EmplName
у вашому DTO, яка представляє ім'я працівника. Хоча для розробників це може бути інтуїтивно зрозуміло, користувачі можуть віддати перевагу заголовку, позначеному як 직원 이름
(корейською "Ім'я працівника"). Словник перекладів відображає EmplName
на її локалізований еквівалент (직원 이름
) і гарантує послідовність під час процесів експорту та імпорту.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Forms;
using Newtonsoft.Json;
using WindowsFormsApp1.Models;
namespace WindowsFormsApp1
{
public partial class Form1 : Form
{
private List employees;
private BindingSource employeesBindingSource;
// словник для заголовків (якщо потрібен переклад)
private static readonly Dictionary ExcelHeaderDictionary = new Dictionary()
{
{ nameof(EmployeeExcelDto.EmplName), "직원 이름" },
{ nameof(EmployeeExcelDto.GetDisplayUidnum7), "주민등록번호 (앞 7자리)" },
{ nameof(EmployeeExcelDto.EmplNum), "직원 번호" },
{ nameof(EmployeeExcelDto.GetDisplayIsSafe), "안전 여부 (O/X)" }
};
public Form1()
{
InitializeComponent();
InitializeEmployees();
}
private void InitializeEmployees()
{
employees = new List
{
new Employee(
emplSeq: 1,
emplName: "Sue",
uidnum7: "9807012",
emplNum: "EMP001",
employmentDate: "2020-01-15",
isSafe: 1
),
new Employee(
emplSeq: 2,
emplName: "Skye",
uidnum7: "0208202",
emplNum: "EMP002",
employmentDate: "2021-06-01",
isSafe: 0
)
};
employeesBindingSource = new BindingSource { DataSource = employees };
dataGridView1.DataSource = employeesBindingSource;
}
private void HandleExportButtonClick(object sender, EventArgs e)
{
using (SaveFileDialog saveFileDialog = new SaveFileDialog())
{
saveFileDialog.Filter = "*.xlsx";
saveFileDialog.FileName = $"Employees_{DateTime.Now:yyyy-MM-dd}";
if (saveFileDialog.ShowDialog() != DialogResult.OK)
{
return;
}
try
{
string filePath = saveFileDialog.FileName;
// конвертуємо дані в DTO
List excelDtos = employees.ConvertAll(emp => new EmployeeExcelDto
{
EmplName = emp.EmplName,
GetDisplayUidnum7 = emp.GetDisplayUidnum7,
EmplNum = emp.EmplNum,
EmploymentDate = emp.EmploymentDate,
GetDisplayIsSafe = emp.GetDisplayIsSafe
});
// серіалізуємо список DTO в JSON
string jsonData = JsonConvert.SerializeObject(excelDtos);
// застосовуємо перекладені заголовки
jsonData = ExcelHeaderDictionary.Aggregate(jsonData, (current, kv) => current.Replace(kv.Key, kv.Value));
ExcelManager.ExportJsonToExcel(filePath, jsonData);
MessageBox.Show("Файл збережено.");
}
catch (Exception err)
{
MessageBox.Show($"Сталася помилка. {err.Message}");
}
}
}
private void HandleUploadButtonClick(object sender, EventArgs e)
{
DialogResult result = MessageBox.Show(
"Дані будуть перезаписані.
Продовжити?",
"Попередження",
MessageBoxButtons.OKCancel,
MessageBoxIcon.Warning
);
if (result != DialogResult.OK)
{
return;
}
using (OpenFileDialog openFileDialog = new OpenFileDialog())
{
openFileDialog.Filter = "*.xlsx";
if (openFileDialog.ShowDialog() != DialogResult.OK)
{
return;
}
try
{
string filePath = openFileDialog.FileName;
string jsonData = ExcelManager.ImportExcelToJson(filePath);
// конвертуємо перекладені заголовки в оригінальні імена властивостей
jsonData = ExcelHeaderDictionary.Aggregate(jsonData, (current, kv) => current.Replace(kv.Value, kv.Key));
JsonSerializerSettings settings = new JsonSerializerSettings() { MissingMemberHandling = MissingMemberHandling.Error };
// десеріалізуємо json в список DTO
List newEmployeeDtos = JsonConvert.DeserializeObject<List<EmployeeExcelDto>>(jsonData, settings);
if (newEmployeeDtos == null || !newEmployeeDtos.Any())
{
throw new JsonSerializationException();
}
string validationError = newEmployeeDtos.Select(dto => dto.Validate()).FirstOrDefault(error => error != null);
if (validationError != null)
{
MessageBox.Show(validationError);
return;
}
// логіка для оновлення або додавання (за бажанням)
foreach (EmployeeExcelDto dto in newEmployeeDtos)
{
Employee existingEmployee = employees.FirstOrDefault(emp =>
emp.EmplName == dto.EmplName && emp.Uidnum7 == dto.Uidnum7);
if (existingEmployee != null)
{
existingEmployee.EmplNum = dto.EmplNum;
existingEmployee.EmploymentDate = dto.EmploymentDate;
existingEmployee.IsSafe = dto.IsSafe;
}
else
{
Employee newEmployee = new Employee(
emplSeq: employees.Any() ? employees.Max(emp => emp.EmplSeq) + 1 : 1,
emplName: dto.EmplName,
uidnum7: dto.Uidnum7,
emplNum: dto.EmplNum,
employmentDate: dto.EmploymentDate,
isSafe: dto.IsSafe
);
employees.Add(newEmployee);
}
}
employeesBindingSource.ResetBindings(false);
MessageBox.Show("Дані завантажено.");
}
catch (JsonSerializationException)
{
MessageBox.Show("Невірний формат Excel. Спробуйте з правильним форматом.");
}
catch (Exception err)
{
MessageBox.Show($"Сталася помилка. {err.Message}");
}
}
}
}
}
Висновок
Ця реалізація спрощує роботу з Excel файлами в додатку на C#. Використання DTO для валідації даних гарантує, що імпортовані файли будуть відповідати очікуваній структурі, знижуючи кількість помилок і полегшуючи налагодження. Включення словника перекладів робить систему гнучкою та зручною для користувачів, дозволяючи використовувати локалізовані або кастомні заголовки стовпців для експорту Excel, одночасно забезпечуючи правильне відображення під час імпорту.
Організувавши логіку експорту та імпорту в окремі методи та використовуючи чіткі робочі процеси, цей підхід дозволяє зберігати код чистим і легким для підтримки. Користувачі отримують зручну обробку файлів, а розробники можуть адаптувати рішення до різних вимог без значних доопрацювань. Це практичний та надійний спосіб обробки даних Excel у структурований і орієнтований на користувача спосіб.
Перекладено з: Import / Export Data to Excel with C# (using DTOs)