Как связать открытый CSV-файл с ранее созданной таблицей в DataGridView, чтобы разрешить его редактирование? - PullRequest
0 голосов
/ 01 февраля 2020

У меня есть следующий код. Это программа бюджетного ассистента. Пользователь редактирует таблицу с помощью текстовых полей и кнопки «Добавить строку», сохраняет ее как .csv, а затем открывает ее в следующий раз, чтобы продолжить редактирование. Сейчас программа прекрасно сохраняет и открывает файлы .csv, но проблема в том, что ... она не позволяет редактировать после загрузки .csv. Насколько я понимаю, проблема в том, что он создает новую таблицу (набор данных) при загрузке файла .csv, но я не совсем уверен. Буду признателен, если вы дадите мне какой-нибудь совет о том, как действовать.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Data.OleDb;
using System.Data.SqlClient;
using System.Drawing;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Xml;
using System.Xml.Linq;
using CsvHelper;
using ExcelDataReader;
using Microsoft.Office.Interop.Excel;
using DataTable = System.Data.DataTable;
using Excel = Microsoft.Office.Interop.Excel;



namespace WindowsFormsApp2
{
    public partial class Form1 : Form 
    {

        DataSet ds = new DataSet();
        DataTable budgetTable = new DataTable();


        public Form1()
        {
            InitializeComponent();

            DataTable budgetTable = ds.Tables.Add("MainTable");
            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(Int32));
            budgetTable.Columns.Add("income", typeof(Int32));
            budgetTable.Columns.Add("saldo", typeof(Int32));

            var date = DateTime.ParseExact("29MAR18", "ddMMMyy", CultureInfo.InvariantCulture);

            DataRow row = budgetTable.NewRow();
            row["id"] = "01";
            row["date"] = date;
            row["type"] = cbbxType.Text;
            row["name"] = nameField.Text;
            row["expenses"] = expenseField.Text;
            row["income"] = incomeField.Text;
            row["saldo"] = 0;
            budgetTable.Rows.Add(row);
            DtgTable.DataSource = budgetTable;
            budgetTable.Rows.Clear();
        }


        //adds a row to the table
        private void button1_Click(object sender, EventArgs e)
        {
            if (string.IsNullOrWhiteSpace(cbbxType.Text) ||
                string.IsNullOrWhiteSpace(expenseField.Text))
            {
                MessageBox.Show("'Type','Expence','Income' fields cannot be empty!");
            }
            else
              budgetTable.Rows.Add(null, dateTime.Text, cbbxType.Text, nameField.Text, expenseField.Text, incomeField.Text);
        }

        //deletes everything from the table
        private void btnDeleteItem_Click(object sender, EventArgs e)
        {
            budgetTable.Rows.Clear();
        }

        //deletes selected row from the table
        private void button1_Click_1(object sender, EventArgs e)
        {
            foreach (DataGridViewRow row in DtgTable.SelectedRows)
            {
                budgetTable.Rows.RemoveAt(row.Index);
            }
        }

        //enumerates ID values
        private void dtgTable_RowPostPaint(object sender, DataGridViewRowPostPaintEventArgs e)
        {

        }

        //calculates saldo cell on a specified row (you have to click the saldo cell)
        private void dtgTable_CellValidated(object sender, DataGridViewCellEventArgs e)
        {

        }

        //calculates overall balance
        private void btnCalcBalance_Click(object sender, EventArgs e)
        {

        }

        //_______________________MenuStrip__________________________________________//
        //Opening file      WORKS                                                   //
        //__________________________________________________________________________//
        private void openToolStripMenuItem_Click(object sender, EventArgs e)
        {
            string FileName;
            OpenFileDialog dialog = new OpenFileDialog();
            dialog.Title = "Open CSV File";
            dialog.Filter = "CSV Files (*.csv)|*.csv";
            if (dialog.ShowDialog() == DialogResult.OK)
            {
                FileName = dialog.FileName;
            }
            else
            {
                return;
            }

                OleDbConnection conn = new OleDbConnection
                       ("Provider=Microsoft.Jet.OleDb.4.0; Data Source = " +
                         Path.GetDirectoryName(FileName) +
                         "; Extended Properties = \"Text;HDR=YES;FMT=Delimited\"");

                conn.Open();

                OleDbDataAdapter adapter = new OleDbDataAdapter
                       ("SELECT * FROM " + Path.GetFileName(FileName), conn);

                DataSet ds = new DataSet("Temp");
                adapter.Fill(ds);

                conn.Close();

                DtgTable.DataSource = ds;
                DtgTable.DataMember = "Table";

        }


        //__________________________________________________________________________________
        //Saving file to .csv   WORKS
        //___________________________________________________________________________________
        public void writeCSV(DataGridView gridIn, string outputFile)
        {
            //test to see if the DataGridView has any rows
            if (gridIn.RowCount > 0)
            {
                string value = "";
                DataGridViewRow dr = new DataGridViewRow();
                StreamWriter swOut = new StreamWriter(outputFile);

                //write header rows to csv
                for (int i = 0; i <= gridIn.Columns.Count - 1; i++)
                {
                    if (i > 0)
                    {
                        swOut.Write(",");
                    }
                    swOut.Write(gridIn.Columns[i].HeaderText);
                }

                swOut.WriteLine();

                //write DataGridView rows to csv
                for (int j = 0; j <= gridIn.Rows.Count - 1; j++)
                {
                    if (j > 0)
                    {
                        swOut.WriteLine();
                    }

                    dr = gridIn.Rows[j];

                    for (int i = 0; i <= gridIn.Columns.Count - 1; i++)
                    {
                        if (i > 0)
                        {
                            swOut.Write(",");
                        }

                        value = dr.Cells[i].Value.ToString();
                        //replace comma's with spaces
                        value = value.Replace(',', ' ');
                        //replace embedded newlines with spaces
                        value = value.Replace(Environment.NewLine, " ");

                        swOut.Write(value);
                    }
                }
                swOut.Close();
            }
        }
        private void saveToolStripMenuItem_Click_1(object sender, EventArgs e)
        {
            writeCSV(DtgTable, "result.csv");
            MessageBox.Show("Converted successfully to *.csv format");
        }

        //___________________________________________________________________________________
        //about
        //___________________________________________________________________________________
        private void aboutToolStripMenuItem_Click(object sender, EventArgs e)
            {
                MessageBox.Show("");
            }


    }
}

1 Ответ

0 голосов
/ 04 февраля 2020

Ваш вопрос (ы) достаточно широк по объему, и SO на самом деле не ориентированы на это. Например, из ваших многочисленных вопросов кажется, что вы хотите:

  1. Чтение и запись файла CSV без использования специальной библиотеки,

  2. Во время чтения CSV-файл, в котором вы хотите сохранить данные в некоторой структуре DATA, такой как DataTable или List<T>,

  3. со структурой DATA, заполненной данными, которые вы хотите привязать к DataGridView ,
  4. Затем вы хотите иметь возможность «удалить» выбранные строки из сетки,
  5. Удалить ВСЕ строки из сетки,
  6. Затем с некоторыми дополнительными текстовыми полями, поля со списком, средство выбора даты… вы хотите, чтобы пользователь мог вводить значения в эти элементы управления, затем нажмите кнопку «ДОБАВИТЬ», чтобы «добавить» эти значения в качестве новой строки в сетке.
  7. , которую нужно проверить данные, которые пользователь вводит,
  8. В сетке вы хотите создать столбец, который «вычисляет» значение на основе других ячеек в этой строке…

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

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;
  }
}

Добро пожаловать, и я надеюсь, что это поможет.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...