I have a simple csv reader that I wrote, it receives data in the format List<string[]> . I need to show this data in wpf and edit it. I use mvvm. If I write just like that, it won't do anything.
<DataGrid AutoGenerateColumns="True" ItemsSource="{Binding Table}"> </DataGrid>
How can I do this? Is it possible for me to somehow output and edit data in a convenient format in the form of List<string[]>. A csv table can have an arbitrary number of columns, so I can't make any particular class inside the code. I tried to find an answer to this question on the Internet, looked at several similar projects, but something never figured it out.
Let's say I get the data this way
var _fileName = #"F:\test.csv";
CsvWriter reader = new CsvWriter(_fileName);
foreach(var item in reader.Read())
{
Table.Add(item);
}
There is a library called CSVReader.
It converts any csv files to DataTables
Example:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void OpenCSVButton_Click(object sender, RoutedEventArgs e)
{
DataTable dt = LoadCSVFileFromPath();
dataGrid.ItemsSource = dt.DefaultView;
//Simple method to convert DataTable to a List of strings
AddToList(dt);
}
private List<string> AddToList(DataTable table)
{
List<string> strTable = new List<string>();
foreach(DataRow rows in table.Rows)
{
foreach (DataColumn cols in table.Columns)
strTable.Add(rows[cols].ToString());
}
return strTable;
}
private DataTable LoadCSVFileFromPath()
{
//Use this method to convert your CSV file to DataTable
DataTable table = CSVReader.ReadCSVFile("FILEPATHGOESHERE", true);
return table;
}
}
You can play around and achieve this in MVVM by binding the DefaultView of the DataTable table to DataGrid
Dont use Binding.
XAML:
<DataGrid x:Name="dataGrid1" AutoGenerateColumns="True" />
Read the CSV data and conver it to DataTable, and place it into ItemsSource:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
LoadData();
}
DataTable GetDataTable(CsvReader csv)
{
var dt = new DataTable();
csv.Read();
csv.ReadHeader();
foreach (var header in csv.HeaderRecord)
dt.Columns.Add(header);
var cols = dt.Columns.Count;
while (csv.Read())
{
var row = dt.NewRow();
for (int i = 0; i < cols; i++)
row[i] = csv[i];
dt.Rows.Add(row);
}
return dt;
}
DataTable table;
string _fileName = #"F:\test.csv";
void LoadData()
{
using (var reader = new StreamReader(_fileName ))
using (var csvReader = new CsvReader(reader, new CsvConfiguration(CultureInfo.InvariantCulture) { Delimiter = "," }))
{
table = GetDataTable(csvReader);
table.AcceptChanges();
dataGrid1.ItemsSource = table.AsDataView();
}
}
void SaveData()
{
if (table.GetChanges() != null)
{
//write DataTable to csv...
}
}
}
Related
I have a string like this:
"Product,Price,Condition
Cd,13,New
Book,9,Used
"
Which is being passed like this:
"Product,Price,Condition\r\Cd,13,New\r\nBook,9,Used"
How could I convert it to DataTable?
Trying to do it with this helper function:
DataTable dataTable = new DataTable();
bool columnsAdded = false;
foreach (string row in data.Split(new string[] { "\r\n" }, StringSplitOptions.None))
{
DataRow dataRow = dataTable.NewRow();
foreach (string cell in row.Split(','))
{
string[] keyValue = cell.Split('~');
if (!columnsAdded)
{
DataColumn dataColumn = new DataColumn(keyValue[0]);
dataTable.Columns.Add(dataColumn);
}
dataRow[keyValue[0]] = keyValue[1];
}
columnsAdded = true;
dataTable.Rows.Add(dataRow);
}
return dataTable;
However I don't get that "connecting cells with appropriate columns" part - my cells don't have ~ in string[] keyValue = cell.Split('~'); and I obviously get an IndexOutOfRange at DataColumn dataColumn = new DataColumn(keyValue[0]);
Based on your implementation, I have written the code for you, I have not tested it. But you can use the concept.
DataRow dataRow = dataTable.NewRow();
int i = 0;
foreach (string cell in row.Split(','))
{
if (!columnsAdded)
{
DataColumn dataColumn = new DataColumn(cell);
dataTable.Columns.Add(dataColumn);
}
else
{
dataRow[i] = cell;
}
i++;
}
if(columnsAdded)
{
dataTable.Rows.Add(dataRow);
}
columnsAdded = true;
You can do that simply with Linq (and actually there is LinqToCSV on Nuget, maybe you would prefer that):
void Main()
{
string data = #"Product,Price,Condition
Cd,13,New
Book,9,Used
";
var table = ToTable(data);
Form f = new Form();
var dgv = new DataGridView { Dock = DockStyle.Fill, DataSource = table };
f.Controls.Add(dgv);
f.Show();
}
private DataTable ToTable(string CSV)
{
DataTable dataTable = new DataTable();
var lines = CSV.Split(new char[] { '\n' }, StringSplitOptions.RemoveEmptyEntries);
foreach (var colname in lines[0].Split(','))
{
dataTable.Columns.Add(new DataColumn(colname));
}
foreach (var row in lines.Where((r, i) => i > 0))
{
dataTable.Rows.Add(row.Split(','));
}
return dataTable;
}
You can split given string into flattened string array in one call. Then you can iterate through the array and populate list of objects.
That part is optional, since you can immediately populate DataTable but I think it's way easier (more maintainable) to work with strongly-typed objects when dealing with DataTable.
string input = "Product,Price,Condition\r\nCd,13,New\r\nBook,9,Used";
string[] deconstructedInput = input.Split(new string[] { "\r\n", "," }, StringSplitOptions.None);
List<Product> products = new List<Product>();
for (int i = 3; i < deconstructedInput.Length; i += 3)
{
products.Add(new Product
{
Name = deconstructedInput[i],
Price = Decimal.Parse(deconstructedInput[i + 1]),
Condition = deconstructedInput[i + 2]
});
}
public class Product
{
public string Name { get; set; }
public decimal Price { get; set; }
public string Condition { get; set; }
}
So, products collection holds 2 objects which you can easily iterate over and populate your DataTable.
Note: This requires further checks to avoid possible runtime exceptions, also it is not dynamic. That means, if you have differently structured input it won't work.
DataTable dataTable = new DataTable();
dataTable.Columns.Add(new DataColumn(nameof(Product.Name)));
dataTable.Columns.Add(new DataColumn(nameof(Product.Price)));
dataTable.Columns.Add(new DataColumn(nameof(Product.Condition)));
foreach (var product in products)
{
var row = dataTable.NewRow();
row[nameof(Product.Name)] = product.Name;
row[nameof(Product.Price)] = product.Price;
row[nameof(Product.Condition)] = product.Condition;
dataTable.Rows.Add(row);
}
When I add a new row to a DataGridView, the row is not saved to the DataView or DataTable. But when I add another row after that, that row is added. The first new row is completely ignored. I have no clue why. Please help.
private string _filePath;
private DataTable _dataTable;
private DataView _defaultView;
public StringRecords(string fileName)
{
InitializeComponent();
_dataTable = FileManager.Populate(filePath);
_dataTable.TableName = filePath;
_defaultView = new DataView(_dataTable);
dataGridRecords.DataSource = _defaultView;
}
private void dataGridRecords_CellEndEdit(object sender, DataGridViewCellEventArgs e)
{
FileManager.SaveDataTableToCsv(_dataTable, filePath);
}
public static void SaveDataTableToCsv(DataTable source, string fileName)
{
using (var stream = new StreamWriter(fileName, false))
{
var config = new CsvConfiguration { Delimiter = "|", HasHeaderRecord = false };
using (var writer = new CsvWriter(stream, config))
{
var records = ToStringRecordList(source);
writer.WriteRecords(records);
}
}
}
Adding the first new row produces this debug output only:
Loaded 'Anonymously Hosted DynamicMethods Assembly'.
I've dumped the DataTable and DataView objects. The first new row isn't added to either object.
What am I doing wrong? Or how can I find out what's happening to the first new row?
I have a csv file delimited with pipe(|). I am reading it using the following line of code:
IEnumerable<string[]> lineFields = File.ReadAllLines(FilePath).Select(line => line.Split('|'));
Now, I need to bind this to a GridView. So I am creating a dynamic DataTable as follows:
DataTable dt = new DataTable();
int i = 0;
foreach (string[] order in lineFields)
{
if (i == 0)
{
foreach (string column in order)
{
DataColumn _Column = new DataColumn();
_Column.ColumnName = column;
dt.Columns.Add(_Column);
i++;
//Response.Write(column);
//Response.Write("\t");
}
}
else
{
int j = 0;
DataRow row = dt.NewRow();
foreach (string value in order)
{
row[j] = value;
j++;
//Response.Write(column);
//Response.Write("\t");
}
dt.Rows.Add(row);
}
//Response.Write("\n");
}
This works fine. But I want to know if there is a better way to convert IEnumerable<string[]> to a DataTable. I need to read many CSVs like this, so I think the above code might have performance issues.
Starting from .Net 4:
use ReadLines.
DataTable FileToDataTable(string FilePath)
{
var dt = new DataTable();
IEnumerable<string[]> lineFields = File.ReadLines(FilePath).Select(line => line.Split('|'));
dt.Columns.AddRange(lineFields.First().Select(i => new DataColumn(i)).ToArray());
foreach (var order in lineFields.Skip(1))
dt.Rows.Add(order);
return dt;
}
(edit: instead this code, use the code of #Jodrell answer, This prevents double charging of the Enumerator).
Before .Net 4:
use streaming:
DataTable FileToDataTable1(string FilePath)
{
var dt = new DataTable();
using (var st = new StreamReader(FilePath))
{
// first line procces
if (st.Peek() >= 0)
{
var order = st.ReadLine().Split('|');
dt.Columns.AddRange(order.Select(i => new DataColumn(i)).ToArray());
}
while (st.Peek() >= 0)
dt.Rows.Add(st.ReadLine().Split('|'));
}
return dt;
}
since, in your linked example, the file has a header row.
const char Delimiter = '|';
var dt = new DataTable;
using (var m = File.ReadLines(filePath).GetEnumerator())
{
m.MoveNext();
foreach (var name in m.Current.Split(Delimiter))
{
dt.Columns.Add(name);
}
while (m.MoveNext())
{
dt.Rows.Add(m.Current.Split(Delimiter));
}
}
This reads the file in one pass.
I'm trying to bind list to gridview. Situation is like this: I take data from .txt file, later I put it inside first list List<Mycolumns>. I have data in list (with 3 separated columns) that I created. I am taking data from one of the columns called System_Description. Now I would like to show this data in gridview, but only thing that I get is length of each row. How should I fix it? Here is my code.
private void button7_Click(object sender, EventArgs e)
{
List<MyColumns> list = new List<MyColumns>();
OpenFileDialog openFile1 = new OpenFileDialog();
openFile1.Multiselect = true;
if (openFile1.ShowDialog() != DialogResult.Cancel)
{
foreach (string filename in openFile1.FileNames)
{
using (StreamReader sr = new StreamReader(filename))
{
string line;
while ((line = sr.ReadLine()) != null)
{
string[] _columns = line.Split(",".ToCharArray());
MyColumns mc = new MyColumns();
mc.Time = _columns[0];
mc.System_Description = _columns[1];
mc.User_Description = _columns[2];
list.Add(mc);
}
}
}
DataTable ListAsDataTable = BuildDataTable<MyColumns>(list);
DataView ListAsDataView = ListAsDataTable.DefaultView;
this.dataGridView1.DataSource = view = ListAsDataView;
this.dataGridView1.AllowUserToAddRows = false;
dataGridView1.ClearSelection();
}
List<string> description = list.Select(x => x.System_Description).ToList<string>();
this.dataGridView2.DataSource = description;
}
class MyColumns
{
public string Time { get; set; }
public string System_Description { get; set; }
public string User_Description { get; set; }
}
EDIT:
I've read that DataBind() works for Web form, my app is desktop app. What should I do now?
I managed to solve this problem. I did it this way, maybe it will help someone. You can use DataTable and then bind DT to gridview.
DataTable dt = new DataTable();
dt.Columns.Add("values");
foreach(string items in description)
{
DataRow row = dt.NewRow();
dt.Rows.Add(items);
}
this.dataGridView2.DataSource = dt;
this.dataGridView1.DataSource = new BindingSource(list);
You are only selecting the System_Description field using the following statement:
List<string> description = list.Select(x => x.System_Description).ToList<string>();
Then you are binding this list to the gridview:
this.dataGridView2.DataSource = description;
If you want to bind the whole data to the gridview; just bind the list as a datasource.
this.dataGridView2.DataSource = list;
this.dataGridView2.DataBind();
Use this
this.dataGridView1.DataSource = description;
and add Bound field in gridview on aspx.
<asp:BoundField DataField="System_Description" HeaderText="System_Description"></asp:BoundField>
I am using C# to import a CSV file into my application
Currently I had a 1 field CSV file. It worked great but now I wanted to add a 3 field CSV file into the same application.
Once the data is stored into the List, I'm binding it to my DataGridView
Here is the relevent code I've written. If you see any issue(s) that aren't part of my problem but can be a problem, please feel free to shout them out. Im always looking to learn and improve my code.
BindingList<StringValue> data = new BindingList<StringValue>();
private void importExcelFile()
{
TextFieldParser parser = new TextFieldParser(fileName);
parser.TextFieldType = FieldType.Delimited;
parser.SetDelimiters(",");
while (!parser.EndOfData)
{
//Processing row
string[] fields = parser.ReadFields();
foreach (string field in fields)
{
StringValue s = new StringValue(field);
// Issue is here. It adds it to a single dimension array. What can I do to make it multi-dimension?
data.Add(s);
}
}
parser.Close();
}
private void OnBackgroundWorkerDoWork(object sender, DoWorkEventArgs e)
{
importExcelFile();
}
private void OnBackgroundWorkerRunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
dataGridView1.DataSource = data;
dataGridView1.Columns[1].Name = "URL";
dataGridView1.Columns[1].HeaderText = "URL";
dataGridView1.Columns[1].Width = 300;
dataGridView1.Columns[1].ReadOnly = true;
dataGridView1.AutoResizeColumns();
toolStripStatusLabel1.Text = dataGridView1.RowCount.ToString() + " Number Of Websites";
}
class StringValue
{
string day, time, url;
public StringValue(string s)
{
_value = s;
}
public StringValue(string[] s)
{
day = s[0];
time = s[1];
url = s[2];
}
public string Value { get { return _value; } set { _value = value; } }
string _value;
}
I think I should modify my StringValue class to hold the multiple fields that I'm importing from my CSV file. I'm not sure how to modify the Value part to return the data I need when I bind it to my dataGridView
Thank you for your input and help/
Why not juste put the csv into a datatable like so?
private DataTable GetDataTableFromCsv(string path)
{
DataTable dataTable = new DataTable();
String[] csv = File.ReadAllLines(path);
foreach (string csvrow in csv)
{
var fields = csvrow.Split(','); // csv delimiter
var row = dataTable.NewRow();
row.ItemArray = fields;
dataTable.Rows.Add(row);
}
return dataTable;
}
after that juste import the datatable into your datagridview.
In your entity (StringValue) you can add as many properties as you want, containing as many values as your want.
You can bind each column of your dataGridView by setting the columns DataPropertyName with the name of the property you are binding to.
For example, your entity has two properties:
class MyValues
{
public string FirstName {get;set;}
public string LastName {get;set;}
}
You add a collection of this to yuur data-collection, which you bind to your grid.
Your grid can be configures as:
dataGridView1.Columns[0].Name = "FirstName";
dataGridView1.Columns[0].HeaderText = "FirstName";
dataGridView1.Columns[0].DataPropertyName = "FirstName";
dataGridView1.Columns[1].Name = "LastName";
dataGridView1.Columns[1].HeaderText = "LastName";
dataGridView1.Columns[1].DataPropertyName = "LastName";