Ваш вопрос (ы) достаточно широк по объему, и SO на самом деле не ориентированы на это. Например, из ваших многочисленных вопросов кажется, что вы хотите:
Чтение и запись файла CSV без использования специальной библиотеки,
Во время чтения CSV-файл, в котором вы хотите сохранить данные в некоторой структуре DATA, такой как DataTable
или List<T>
,
- со структурой DATA, заполненной данными, которые вы хотите привязать к
DataGridView
, - Затем вы хотите иметь возможность «удалить» выбранные строки из сетки,
- Удалить ВСЕ строки из сетки,
- Затем с некоторыми дополнительными текстовыми полями, поля со списком, средство выбора даты… вы хотите, чтобы пользователь мог вводить значения в эти элементы управления, затем нажмите кнопку «ДОБАВИТЬ», чтобы «добавить» эти значения в качестве новой строки в сетке.
- , которую нужно проверить данные, которые пользователь вводит,
- В сетке вы хотите создать столбец, который «вычисляет» значение на основе других ячеек в этой строке…
Я попытаюсь сломать это до управляемых частей, которые вы можете изменить, чтобы соответствовать Требования это не сложно сделать.
CSV «подразумевает», что разделителями являются запятые (,), где каждая запятая разделяет каждое поле. Однако, как вы заметили, пользователь может ввести в данные запятую (,), и это может испортить структуру csv. Следовательно, этот подход предполагает, что вы можете определить «разделитель» для любого символа. В приведенных ниже примерах я использовал символ TAB (\ t) в качестве разделителя.
Одной из проблем при чтении CSV-файла является указание количества столбцов (полей) в файле. Это важно знать, прежде чем читать файл. Если число полей в каждой строке неизвестно, то вы можете прочитать файл один раз, разделив каждую строку в разделителе, а затем обратите внимание на строку с полями MOST. При таком подходе вам гарантировано, что при повторном чтении файла вы получите ВСЕ данные. Это требует чтения файла дважды и, возможно, не является хорошим вариантом.
Другой подход заключается в том, что если у файла с разделителями есть строка «заголовка» в первой строке, вы можете использовать его для определения количества столбцов (полей). ) которые находятся в файле. Это может сработать; однако это не гарантирует, что верхняя строка заголовка верна.
Наконец, вы можете ИСПРАВИТЬ количество столбцов, если вы уже знаете количество полей. Это подход, который использует код ниже. В этом примере мы знаем, что в файле csv есть 6 полей в каждой строке. Мне известно, что на самом деле существует 7 полей, однако 7-е поле является полем «Баланс» и «вычисляется» из других полей и не сохраняется в файле CSV. Поэтому в этом примере мы будем предполагать, что каждая строка в CSV-файле имеет шесть (6) полей в следующем порядке: идентификатор, дата, тип, имя, расходы и доход. Поле Saldo (Balance) будет вычислено.
Считать CSV в DataTable
Приведенный ниже код считывает файл TAB DELIMITED и сохраняет данные в DataTable
. Это DataTable
является глобальной переменной. Экспонирование этой переменной не является лучшей практикой, однако для этого примера будет достаточно. Во-первых, это метод, который «создает» DataTable
. Этот метод просто инициализирует DataTable
и добавляет определение столбца, но НЕТ строк. Мы добавим строки позже.
Обратите внимание, что последний столбец «Баланс» является столбцом «Выражение» для вычисления дохода - расходов. При изменении полей Income или Expense это значение будет обновляться автоматически.
private void SetTableColumns() {
BudgetTable = new DataTable();
BudgetTable.Columns.Add("Id", typeof(Int32));
BudgetTable.Columns.Add("Date", typeof(DateTime));
BudgetTable.Columns.Add("Type", typeof(String));
BudgetTable.Columns.Add("Name", typeof(String));
BudgetTable.Columns.Add("Expenses", typeof(decimal));
BudgetTable.Columns.Add("Income", typeof(decimal));
BudgetTable.Columns.Add("Balance", typeof(decimal));
BudgetTable.Columns["Balance"].Expression = "Income - Expenses";
}
Далее следует метод, который заполняет вышеуказанное значение DataTable
из файла с разделителями табуляции. Он использует StreamReader
и потребует using System.IO;
. Переменная splitArray
используется для «разделения» каждой строки символа TAB. Прежде чем мы начнем читать файл, мы хотим проверить, сколько столбцов имеет DataTable
. Это переменная maxColumns
, и она используется для проверки, если в текущей прочитанной строке недостаточно полей в строке. Если мы пренебрегаем проверкой этого значения для КАЖДОГО чтения строки, то, когда одна из строк содержит менее 6 полей, код будет взламывать sh в строке newRow[i] = splitArray[i];
, поскольку splitArray[5]
! ... если в строке чтения БОЛЬШЕ этих 6 полей, то мы проигнорируем эти значения и просто возьмем первые 6. Также обратите внимание, что переменная maxColumns
установлена на число столбцов МИНУС 1, так как мы хотим игнорировать вычисленный «Баланс» column.
После того, как мы проверили количество прочитанных полей, мы можем безопасно l oop через все поля / столбцы и добавить их в новый DataRow
newRow
.
private void FillDataTableFromCSV(string file) {
StreamReader sr = new StreamReader(file);
string[] splitArray;
int maxColumns = BudgetTable.Columns.Count - 1; // <- the last column is a computed column we dont want to save
int curColCount = maxColumns;
string curLine = sr.ReadLine();
if (curLine != null) {
DataRow newRow;
while ((curLine = sr.ReadLine()) != null) {
splitArray = curLine.Split('\t');
curColCount = maxColumns;
if (splitArray.Length < maxColumns) {
curColCount = splitArray.Length;
}
newRow = BudgetTable.Rows.Add();
for (int i = 0; i < curColCount; i++) {
newRow[i] = splitArray[i];
}
}
}
sr.Close();
}
Приведенный выше код будет работать безопасно, если все данные в порядке, однако все еще существует проблема, если одно из считанных «числовых» полей не является допустимым числом. Если значение расхода в csv-файле «некоторая строка», то код будет записывать sh в строке newRow[i] = splitArray[i];
. Похоже, нам нужно провести дополнительную проверку данных ДО того, как мы добавим их в DataTable
. То же самое применимо, если бы мы использовали структуру данных List<T>
для хранения данных. Приведенный ниже код снова будет читать файл с разделителями TAB, но будет считывать данные в List<T>
. В этом примере данные проверяются на действительные числа до того, как данные добавляются в список. Вы должны иметь возможность применить ту же проверку к приведенному выше примеру.
Считать CSV в BindingList<T>
Сначала нам нужно определить Class T
, который мы будем позвоните BudgetItem
. Эта голая кость BudgetItem
Класс может выглядеть следующим образом Обратите внимание, что свойство publi c Date
является строкой, основанной на частном объекте DateTime
_Date
. Это только для отображения и не является действительно необходимым. Также обратите внимание, что свойство publi c «Баланс» вычисляется из свойств «Доход - расходы». Наконец, полный конструктор.
К вашему сведению: свойство PRIVATE _Date
НЕ будет отображаться в сетке. Когда BindingList<T>
используется как DataSource
до DataGridView
, будут отображаться ТОЛЬКО ОБЩЕСТВЕННО выставленные свойства. Кроме того, сетка НЕ будет отображать какие-либо свойства типа COLLECTION в классе. Следовательно, в этом примере класса в сетке будут отображаться только Id, Date, Type, Name Expense, Income and Balance.
public class BudgetItem {
public int Id { get; set; }
private DateTime _Date { get; set; }
public string Date => _Date.ToShortDateString();
public string Type { get; set; }
public string Name { get; set; }
public decimal Expense { get; set; }
public decimal Income { get; set; }
public decimal Balance => Income - Expense;
public BudgetItem(int id, DateTime date, string type, string name, decimal expense, decimal income) {
Id = id;
_Date = date;
Type = type;
Name = name;
Expense = expense;
Income = income;
}
}
Далее следует метод для считывания csv в BindingList<BudgetItem>
. Опять же, этот список является глобальной переменной. Поскольку требуется проверка данных, второй метод GetBudgetItemFromString(curLine))
берет строку, прочитанную из файла, и возвращает новый BudgetItem
, который мы можем добавить в список привязок.
private void FillListFromCSV(string file) {
BudgetList = new BindingList<BudgetItem>();
StreamReader sr = new StreamReader(file);
string curLine = sr.ReadLine(); // <- ignore header row
if (curLine != null) {
BudgetItem BI;
while ((curLine = sr.ReadLine()) != null) {
if ((BI = GetBudgetItemFromString(curLine)) != null) {
BudgetList.Add(BI);
}
else {
MessageBox.Show("Error invalid Budget Item: " + curLine);
}
}
}
sr.Close();
}
Для проверки данных Приведенный ниже метод следует этой логике c, если в строке менее 6 полей, возвращается нулевое значение. Если существует более 6 значений, дополнительные значения игнорируются. Для проверки числовых значений c используется TryPase
, и если строковое значение НЕ является действительным числом, возвращаемое число будет равно нулю (0). Не выдается никакой ошибки, и пользователь просто увидит нули (0) в полях цифры c, где числа недопустимы. Вы можете изменить это.
private BudgetItem GetBudgetItemFromString(string data) {
string[] arr = data.Split('\t');
if (arr.Length >= 6) {
int.TryParse(arr[0], out int id);
DateTime.TryParse(arr[1], out DateTime date);
decimal.TryParse(arr[4], out decimal exp);
decimal.TryParse(arr[5], out decimal inc);
return new BudgetItem(id, date, arr[2], arr[3], exp, inc);
}
return null;
}
Запись данных сетки в файл CSV
Я предполагаю, что вы могли бы создать метод для записи DataTable
в CSV, затем другой метод для записи BindingList<BudgetItem>
в CSV ... однако, если мы напишем метод, который записывает DataGridView
в CSV, то нам потребуется только один метод, и не будет иметь значения, какой «тип» источник данных:
Метод WriteGridToCSV(DtgTable, filePath);
принимает DataGridView
и путь к файлу и записывает данные в сетке в файл TAB DELIMITED. Здесь мы будем использовать StreamWriter
для записи данных в файл. Сначала пишутся заголовки, затем простой l oop через строки и столбцы сетки. Примечание: как уже говорилось, мы НЕ хотим писать столбец «Баланс».
private void WriteGridToCSV(DataGridView dgv, string filename) {
StreamWriter sw = new StreamWriter(filename);
// write headers - we do not want to write the balance column
for (int col = 0; col < dgv.Columns.Count - 1; col++) {
sw.Write(dgv.Columns[col].HeaderText);
if (col < dgv.Columns.Count - 2) {
sw.Write("\t");
}
}
sw.WriteLine();
// Write data - we do not want to save the balance column
for (int row = 0; row < dgv.RowCount; row++) {
for (int col = 0; col < dgv.ColumnCount - 1; col++) {
if (!dgv.Rows[row].IsNewRow) {
sw.Write(dgv.Rows[row].Cells[col].Value);
if (col < dgv.ColumnCount - 2) {
sw.Write("\t");
}
}
}
sw.WriteLine();
}
sw.Close();
MessageBox.Show("Write finished");
}
Как только у нас будет сбор данных, мы можем использовать его как DataSource
для DataGridView
.
// for the DataTable
DtgTable.DataSource = BudgetTable;
// For the `BindingList<BudgetItem>
DtgTable.DataSource = BudgetList;
Теперь, когда данные отображаются в сетке, нам нужна кнопка, которая «удаляет» выбранные строки. Предполагается, что для режима сетки Selection
установлено некоторое значение FULL ROW. Нам нужно взять выбранную «строку» из DataTable
и удалить ее. Или нам нужно взять выделенный BudgetItem
и удалить его из BindingList<BudgetItem>
При использовании DataTable
private void btnDelete_Click(object sender, EventArgs e) {
foreach (DataGridViewRow row in DtgTable.SelectedRows) {
DataRowView dr = (DataRowView)row.DataBoundItem;
dr.Delete();
}
DtgTable.DataSource = null;
DtgTable.DataSource = BudgetTable;
}
}
При использовании BindingList<BudgetItem>
private void btnDelete_Click(object sender, EventArgs e) {
BudgetItem target;
foreach (DataGridViewRow row in DtgTable.SelectedRows) {
target = (BudgetItem)row.DataBoundItem;
BudgetList.Remove(target);
}
DtgTable.DataSource = null;
DtgTable.DataSource = BudgetList;
}
Для очистки сетки от ВСЕХ рядов…
private void btnClearAll_Click(object sender, EventArgs e) {
BudgetTable.Rows.Clear();
}
private void btnClearAll_Click(object sender, EventArgs e) {
BudgetList = new BindingList<BudgetItem>();
DtgTable.DataSource = null;
DtgTable.DataSource = BudgetList;
}
Затем добавляем новые строки в сетку из текстовых полей, в которые вводит пользователь.
В приведенном ниже примере я добавил текстовое поле идентификатора и использовал DateTimePicker
для значения даты. DateTimePicker
избавит от необходимости «проверять» дату, поскольку пользователь НЕ может выбрать неверную дату. Поле со списком «type» является строкой и не нуждается в проверке. Это также относится к полю имени. Поэтому нам нужно убедиться, что поля не пусты, а поле ID является допустимым целым числом, а поля Расходы и Доходы являются действительными десятичными числами. Типы int
и decimal
имеют свойство TryParse
и являются удобным способом проверки чисел.
Оба метода ниже идентичны, за исключением того, что один добавляет строку к DataTable
, а другой добавить новый BudgetItem
в BindingList<BudgetItem>
.
Добавление строки в DataTable
private void btnAddItem_Click(object sender, EventArgs e) {
if (!AllFieldsEntered()) {
MessageBox.Show("'ID', 'Type','Name', 'Expense' and 'Income' fields cannot be empty!");
}
else {
StringBuilder errorString = new StringBuilder("Invalid Values: " + Environment.NewLine);
bool noErrors = true;
if (!int.TryParse(txtID.Text, out int id)) {
errorString.AppendLine("ID must be a valid integer");
noErrors = false;
}
if (!decimal.TryParse(expenseField.Text, out decimal exp)) {
errorString.AppendLine("Expense must be a valid decimal");
noErrors = false;
}
if (!decimal.TryParse(incomeField.Text, out decimal inc)) {
errorString.AppendLine("Income must be a valid decimal");
noErrors = false;
}
string date = dtpDate.Value.ToString("MM/dd/yyyy");
if (noErrors) {
BudgetTable.Rows.Add(id, date, cbbxType.Text, nameField.Text, exp, inc);
}
else {
MessageBox.Show(errorString.ToString());
}
}
}
Добавление строки в BindingList
private void btnAddItem_Click(object sender, EventArgs e) {
if (!AllFieldsEntered()) {
MessageBox.Show("'ID', 'Type','Name', 'Expense' and 'Income' fields cannot be empty!");
}
else {
StringBuilder errorString = new StringBuilder("Invalid Values: " + Environment.NewLine);
bool noErrors = true;
if (!int.TryParse(txtID.Text, out int id)) {
errorString.AppendLine("ID must be a valid integer");
noErrors = false;
}
if (!decimal.TryParse(expenseField.Text, out decimal exp)) {
errorString.AppendLine("Expense must be a valid decimal");
noErrors = false;
}
if (!decimal.TryParse(incomeField.Text, out decimal inc)) {
errorString.AppendLine("Income must be a valid decimal");
noErrors = false;
}
if (noErrors) {
BudgetList.Add(new BudgetItem(id, dtpDate.Value, cbbxType.Text, nameField.Text, exp, inc));
DtgTable.DataSource = null;
DtgTable.DataSource = BudgetList;
}
else {
MessageBox.Show(errorString.ToString());
}
}
}
Убедитесь, что все поля имеют данные
private bool AllFieldsEntered() {
if (string.IsNullOrWhiteSpace(cbbxType.Text) ||
string.IsNullOrWhiteSpace(expenseField.Text) ||
string.IsNullOrWhiteSpace(txtID.Text) ||
string.IsNullOrWhiteSpace(incomeField.Text) ||
string.IsNullOrWhiteSpace(nameField.Text)) {
return false;
}
return true;
}
Наконец, для текстовых полей, где нужны цифры, мы можем помочь пользователю, допустив только цифры и возможный ОДИН период (.) Для десятичные числа. Чтобы подключить это, вам нужно зарегистрировать события KeyPressed
для текстовых полей цифр c. Эти строки можно поместить в конструктор форм ПОСЛЕ строки InitializeComponent();
.
this.txtID.KeyPress += new System.Windows.Forms.KeyPressEventHandler(this.IntegerOnlyField_KeyPress);
this.expenseField.KeyPress += new System.Windows.Forms.KeyPressEventHandler(this.DecimalOnlyField_KeyPress);
this.incomeField.KeyPress += new System.Windows.Forms.KeyPressEventHandler(this.DecimalOnlyField_KeyPress);
После того, как эти события были соединены, нам нужно указать эти зарегистрированные методы в описанных выше событиях… Проверяется, чтобы увидеть, нажата ли клавиша di git, возврат или период. Если это не один из тех персонажей, то игнорируйте его. ПРИМЕЧАНИЕ. Пользователь по-прежнему сможет вставлять недопустимые значения в текстовое поле.
private void DecimalOnlyField_KeyPress(object sender, KeyPressEventArgs e) {
if (!(char.IsDigit(e.KeyChar) || e.KeyChar == (char)Keys.Back || e.KeyChar == '.')) {
e.Handled = true;
}
TextBox txtDecimal = sender as TextBox;
if (e.KeyChar == '.' && txtDecimal.Text.Contains(".")) {
e.Handled = true;
}
}
private void IntegerOnlyField_KeyPress(object sender, KeyPressEventArgs e) {
if (!(char.IsDigit(e.KeyChar) || (e.KeyChar == (char)Keys.Back))) {
e.Handled = true;
}
}
Добро пожаловать, и я надеюсь, что это поможет.