namespace WinFormsApp4
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private void Load_Click(object sender, EventArgs e)
{
using (OpenFileDialog ofd = new OpenFileDialog())
{
ofd.Filter = "Excel Files only | *.xlsx; *.xls; *.csv;" ;
ofd.Title = "Choose the file";
if (ofd.ShowDialog() == DialogResult.OK)
label1.Text = ofd.FileName;
}
}
private void Import_Click_1(object sender, EventArgs e)
{
Microsoft.Office.Interop.Excel.Application xlapp;
Microsoft.Office.Interop.Excel.Workbook xlworkbook;
Microsoft.Office.Interop.Excel.Worksheet xlworksheet;
Microsoft.Office.Interop.Excel.Range xlrange;
try
{
xlapp = new Microsoft.Office.Interop.Excel.Application();
xlworkbook = xlapp.Workbooks.Open(label1.Text);
xlworksheet = xlworkbook.Worksheets["List1"];
xlrange = xlworksheet.UsedRange;
DataMore.ColumnCount = xlrange.Columns.Count;
DataMore.ColumnCount = 6;
DataMore.Columns[0].HeaderText = "Datum";
DataMore.Columns[1].HeaderText = "Energia";
DataMore.Columns[2].HeaderText = "AC výkon";
DataMore.Columns[3].HeaderText = "napetie siete";
DataMore.Columns[4].HeaderText = "AC prud";
DataMore.Columns[5].HeaderText = "DC napetie";
foreach (DataGridViewRow row in DataMore.Rows)
{
}
// for (int xlrow = 1; xlrow <= xlrange.Rows.Count; xlrow++)
// {
// DataMore.Rows.Add(xlrange.Cells[xlrow, 1].Text, xlrange.Cells[xlrow, 2].Text, xlrange.Cells[xlrow, 3].Text,
// xlrange.Cells[xlrow, 4].Text);
// }
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
}
}
}
Hi, first of all im totally beginner in C# programing.
I try import numbers from excel into datagridview. All numbers are in 1 Columns and looks like this:
1,2,3,4,5,
6,7,8,9,10
11,12,13,14,15
16,17,18,19,20
All numbers are not real because original excel has numbers that he collect 24/7 past 5 years and its tons of numbers so i create own small excel...
Im just trying to create program where i import numbers from excel, then i delete Number "0" and then trying to create graphs... But im stuck at start, can you help me please?
Your question is about using Microsoft.Office.Interop.Excel to import Excel data to a DataGridView control and say there is a problem where your view stays blank. Looking at your code, I also notice that some of your Excel objects are not being disposed properly, which can lead to multiple instances of Excel running (look in Task Manager).
So, I would like to start from the beginning and look at how to go about this properly in four steps.
Record class
Make a class named Record to represent a row of data.
class Record
{
[DisplayName("Datum")]
public DateTime Datum { get; set; }
[DisplayName("Energia")]
public double Energia { get; set; }
[DisplayName("AC výkon")]
public double ACvýkon { get; set; }
[DisplayName("napetie siete")]
public double napetiesiete { get; set; }
[DisplayName("AC prud")]
public double ACprud { get; set; }
[DisplayName("DC napetie")]
public double DCnapetie { get; set; }
}
Auto-configure DataGridView
Make a BindingList<Record> and attach it to the DataSource property of the data grid view.
public partial class MainForm : Form
{
BindingList<Record> Records { get; } = new BindingList<Record>();
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
dataGridView.DataSource = Records;
#region F O R M A T C O L U M N S
Records.Add(new Record()); // <- Auto-generate columns
foreach (DataGridViewColumn column in dataGridView.Columns)
{
column.AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill;
if (column.Index > 1) column.DefaultCellStyle.Format = "F2";
}
Records.Clear();
#endregion F O R M A T C O L U M N S
}
.
.
.
}
Configure Excel Interop create and dispose
public MainForm()
{
InitializeComponent();
// Create
_xlApp = new Microsoft.Office.Interop.Excel.Application();
// When in the future the main form closes, dispose the Excel interop.
Disposed += (sender, e) =>
{
_xlBook?.Close();
_xlApp.Quit();
};
buttonImport.Click += Import_Click_1;
}
private readonly Microsoft.Office.Interop.Excel.Application _xlApp;
private Workbook _xlBook = null;
Import Data (in this case from a predetermined file location)
The pieces come together here. Because of the data source binding of Records when you add to that collection it displays in the data grid view without having to deal with the control itself.
So after opening the workbook and sheet in Excel, capture the range of all used cells and parse that information to make Record instances and add them to the Records collection.
private void Import_Click_1(object sender, EventArgs e)
{
Records.Clear();
string filePath = Path.Combine(
AppDomain.CurrentDomain.BaseDirectory,
"Excel",
"testdata.xlsx");
_xlBook = _xlApp.Workbooks.Open(filePath);
Worksheet
xlSheet = _xlBook.Sheets[1];
Range
xlRange = xlSheet.UsedRange,
range;
List<string>
headers = new List<string>(),
line = new List<string>();
for (int i = 1; i <= xlRange.Rows.Count; i++)
{
if (i.Equals(1))
{
for (int j = 1; j <= xlRange.Columns.Count; j++)
{
range = xlRange.Cells[i, j];
headers.Add(range.Value2);
}
}
else
{
var record = new Record();
for (int j = 1; j <= xlRange.Columns.Count; j++)
{
range = xlRange.Cells[i, j];
var name = headers[j - 1];
switch(name)
{
case "Datum": record.Datum = DateTime.FromOADate(range.Value2); break;
case "Energia": record.Energia = range.Value2; break;
case "AC výkon": record.ACvýkon = range.Value2; break;
case "napetie siete": record.napetiesiete = range.Value2; break;
case "AC prud": record.ACprud = range.Value2; break;
case "DC napetie": record.DCnapetie = range.Value2; break;
default:
Debug.Assert(false, $"Not recognized: '{name}'");
break;
}
}
Records.Add(record);
}
}
}
Related
I'm just a student trying to get better.
Right now I am developing an application which reads from Excel. For that I have a class, DataReader.
public class DataReader : IDataReader
{
Workbook workbook;
public DataReader()
{
workbook = Globals.Factory.GetVstoObject(Globals.ThisAddIn.Application.ActiveWorkbook);
}
public Worksheet GetWorksheetByName(string name)
{
Worksheet sheet = (Worksheet)Globals.Factory.GetVstoObject(workbook.Worksheets[name]);
return sheet;
}
}
and this class I am calling from multiple places. Here's an example of how I'm I'm instantiating it in one class
public List<ConfigModel> Instances { get; set; }
public IDataReader dr;
public Worksheet sheet;
public PopulateConfigModel()
{
Instances = new List<ConfigModel>();
dr = new DataReader();
sheet = dr.GetWorksheetByName("Your_Data_Sheet");
}
and here is another example
public class RangeCreator : IRangeCreator
{
IDataReader dr;
Worksheet sheet;
public Xcl.Range GetDestinationRange()
{
dr = new DataReader();
DataHandler dh = new DataHandler();
sheet = dr.GetWorksheetByName("Your_CSV_File");
string range = "A" + ((dh.GetLastRow(sheet) + 1) + ":A" + (dh.GetLastRow(sheet) + 3));
Xcl.Range rng = sheet.Range[range];
return rng;
}
Note that I also calls new DataHandler in that method.
I don't think this is good practice but I don't have a better solution so what's the work around for instantiating this class multiple places? I guess I could go with DI but then I'd have to DI both the DataReader and DataHandler in multiple places and I'm not sure that's a better solution.
I don't know,
UPDATE:
public void BeginProcess()
{
dh = new DataHandler1();
//Returns a List<List<int>> with all used boxsizes {{ 5,1 }, { 15, 3 }} & startCell for each of those boxes eg {{ "B6" }, { "B27" }}
ListHandler lh = dh.GetListHandlerData();
//Holds a list of instances
IPopulateConfigModel pc = new PopulateConfigModel();
//For hver boks vi har i dokumentet
for (int i = 0; i < lh.cellAddresses.Count; i++)
{
//Adds list of instances to list of models
models.Add(pc.PopulateInstances(lh.boxSizes[i], lh.cellAddresses[i]));
}
ConfigModelHandler cm = new ConfigModelHandler(models);
cm.StartModelProcessing(dh);
}
Now I am injecting the DataHandler as a parameter in StartModelProcessing
public void StartModelProcessing(IDataHandler1 dh)
{
IFileSaver fileSaver = new FileSaver();
Worksheet sheet = dr.GetWorksheetByName("Your_CSV_File");
//Each model - each with their own sheet
for (int i = 0; i < models.Count; i++)
{
//Contains all instances of an object
string[][] instances = new string[models[i].Count][];
string filePath = Directory.GetCurrentDirectory() + #"\" + models[i][0].TemplateName + ".csv";
//Clears our sheet so we're ready for a new set of instances
dh.ClearWorksheet(sheet);
//Each instance - sharing their worksheet
for (int j = 0; j < models[i].Count; j++)
{
Xcl.Range rngPopulated = GetPopulatedSourceRange(models[i][j]);
string[] rows = new string[3];
//Get our data from our populated range to our string array. It's in the form of:
/*
* one string array = one instance
* rows[0] = TEMPLATE
* rows[1] = Properties
* rows[2] = Variables
*/
for (int ti = 0; ti < rngPopulated.Rows.Count; ti++)
{
for (int tu = 1; tu < rngPopulated.Rows[ti + 1].Cells.Count; tu++)
{
rows[ti] += rngPopulated.Rows[ti + 1].Cells[tu].Value2 + ",";
}
}
instances[j] = rows;
Xcl.Range testRange = rangeGetter.GetDestinationRange(dh);
//We aint using this sheet - it's only if the user will change and save it manually but only last instance of last document is showing
di.PopulateCsvFile(rows, testRange);
}
fileSaver.SaveFile(instances, filePath);
}
}
and as you can see, injecting it as a parameter again in the rangeGetter.GetDestRange method.
and here's the method
public Xcl.Range GetDestinationRange(IDataHandler1 dh)
{
dr = new DataReader1();
sheet = dr.GetWorksheetByName("Your_CSV_File");
string range = "A" + ((dh.GetLastRow(sheet) + 1) + ":A" + (dh.GetLastRow(sheet) + 3));
Xcl.Range rng = sheet.Range[range];
return rng;
}
One way is to make DataReader class as singleton pattern like:
class DataReader
{
private static DataReader instance = null;
private DataReader()
{
//workbook goes here...
}
public static DataReader Instance
{
get
{
if (instance == null)
instance = new DataReader();
return instance;
}
}
}
And then you can call the method like:
...
sheet = DataReader.Instance.GetWorksheetByName("Your_CSV_File");
Here you not need to use the new keyword every time
please understand
this is very "sudo code" and just here to illiterate that what is more important than trying to avoid "new" is how code is consumed by other code.
your code is hard to comment on as the whole picture cannot be seen... (what belongs to what)
If you don't understand what I'm trying to show below then i will delete, but if you do then great! I have ommited a lot of code to make it easier to understand... well hopefully
class Main
{
DataReader1 _dr;
DataHandler _dh;
public Main()
{
_dr = new DataReader1();
_dh = new DataHandler();
BeginProcess();
}
public void BeginProcess()
{
//...code ommitted
ConfigModelHandler cm = new ConfigModelHandler(models);
cm.StartModelProcessing();
}
public void StartModelProcessing(IDataHandler1 dh)
{
Worksheet sheet = dr.GetWorksheetByName("Your_CSV_File");
//Each model - each with their own sheet
for (int i = 0; i < models.Count; i++)
{
for (int j = 0; j < models[i].Count; j++)
{
Worksheet sheetInner = dr.GetWorksheetByName("Your_CSV_File");
//do stuff with sheet
var range = GetDestinationRange(sheetInner);
}
fileSaver.SaveFile(instances, filePath);
}
}
public Xcl.Range GetDestinationRange(Worksheet sheet)
{
var lastRow = dh.GetLastRow(sheet);
string range = "A" + ((lastRow + 1) + ":A" + (lastRow + 3));
Xcl.Range rng = sheet.Range[range];
return rng;
}
}
Building an application that reads a CSV file and converts it into xls with c# in VS 2017.
I'm using CsvHelper and Microsoft.Office.Interop.Excel to accomplish this.
The application can read a CSV file in a windows form, have the program set up a template and insert into that formatted template all the values in the correct cells HOWEVER the very first page created no matter which file is used is not formatted and gets inserted in a un-formatted excel page.
I have attempted:
changing the arguments of the "Workbook.Sheets.Add"
where in the logic the sheets get added
changing the arguments in the SaveAs function
Changing various indexes from 1 to 0 and visa versa
I am new to working with the (Interop.Excel) namespace and have spent much time reading the DOCS on the MS web-page yet I still cannot resolve this issue.
Here is how I am adding the pages to the workbook:
if (!backgroundWorker.CancellationPending)
{
backgroundWorker.ReportProgress(index++ * 100 / pageCount);
Thread.Sleep(delay);
wb.Sheets.Add(missing,After:wb.Sheets[wb.Sheets.Count],Count:missing,Type:template);
Worksheet ws = (Worksheet)wb.Sheets[wb.Sheets.Count];
}
Here is how I am saving the pages:
wb.SaveAs(fileName, XlFileFormat.xlWorkbookDefault, missing, missing, true, false, XlSaveAsAccessMode.xlNoChange,
XlSaveConflictResolution.xlLocalSessionChanges,
missing, missing);
excel.Quit();
Here is a reference to the whole method:
namespace csvReader
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
struct DataParameter
{
public List<material> materialList;
public List<material> smallMats;
public Workbook wbData;
public string Filename { get; set; }
public int Delay;
}
DataParameter _inputParameter;
private void btnWrite_Click(object sender, EventArgs e)
{
if (backgroundWorker.IsBusy)
return;
using (SaveFileDialog sfd = new SaveFileDialog() { Filter = "Excel Workbook|*.xls" })
{
if (sfd.ShowDialog() == DialogResult.OK)
{
_inputParameter.Filename = sfd.FileName;
_inputParameter.materialList = materialBindingSource2.DataSource as List<material>;
_inputParameter.Delay = 100;
progressBar.Minimum = 0;
progressBar.Value = 0;
backgroundWorker.RunWorkerAsync(_inputParameter);
}
}
}
private void backgroundWorker_DoWork(object sender, DoWorkEventArgs e)
{
object missing = Type.Missing;
List<material> list = ((DataParameter)e.Argument).materialList;
List<material> cellM = ((DataParameter)e.Argument).smallMats;
string fileName = ((DataParameter)e.Argument).Filename;
int pageCount = 1;
int process = list.Count;
int setRows = 19;
int delay = 100;
if (list.Count > setRows)
{
pageCount = process / setRows;
}
Microsoft.Office.Interop.Excel.Application excel = new
Microsoft.Office.Interop.Excel.Application();
string template = "(mytemplatefilepath)";
Workbook wb = ((DataParameter)e.Argument).wbData;
wb = excel.Workbooks.Add();
excel.Visible = false;
int index = 1;
try
{
for (int i = 1; i < pageCount; i++)
{
if (!backgroundWorker.CancellationPending)
{
backgroundWorker.ReportProgress(index++ * 100 / pageCount);
Thread.Sleep(delay);
wb.Sheets.Add(missing,After:wb.Sheets[wb.Sheets.Count],Count:missing,Type:template);
Worksheet ws = (Worksheet)wb.Sheets[wb.Sheets.Count];
}
}
int range = 1;
int sheetIndex = 1;
foreach (Worksheet w in wb.Sheets)
{
w.Name = "Sheet" + sheetIndex++;
//w.Cells["L", 3] = tbSpecial.Text;
cellM = list.GetRange(range, 19);
int startCell = 7;
foreach (material m in cellM)
{
if (!backgroundWorker.CancellationPending)
{
backgroundWorker.ReportProgress(index++ * 100 / process);
Thread.Sleep(delay);
Microsoft.Office.Interop.Excel.Range newInput = w.get_Range("C" + startCell, "L" + startCell) as Microsoft.Office.Interop.Excel.Range;
w.Cells[startCell, 2] = m.Qty.ToString();
w.Cells[startCell, 3] = m.Section.ToString();
w.Cells[startCell, 4] = m.Length.ToString();
w.Cells[startCell, 5] = m.Camber.ToString();
w.Cells[startCell, 6] = m.Ends.ToString();
w.Cells[startCell, 7] = m.Grade.ToString();
w.Cells[startCell, 8] = m.Seq.ToString();
w.Cells[startCell, 9] = m.Member.ToString();
//w.Cells["L", 3] ="700";
startCell++;
}
}
range = range + 19;
}
wb.SaveAs(fileName, XlFileFormat.xlWorkbookDefault, missing, missing, true, false, XlSaveAsAccessMode.xlNoChange,
XlSaveConflictResolution.xlLocalSessionChanges,
missing, missing);
excel.Quit();
}
catch (Exception ex)
{
backgroundWorker.CancelAsync();
MessageBox.Show(ex.Message, "Message", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
Here is the first page of a data set of 203 pages
https://imgur.com/t6dd2ip
and the other 202 pages appear as so
https://imgur.com/jMuNMeb
I feel like it's syntax error I'm making somewhere.
Although it is easy to fill in one excel page I'd rather have the program complete 100% of this work with the technologies I am using.
The reason the first sheet did not have the template layout and that if 'i = 0' bombs is that by default when instantiating a workbook with Workbooks.Add() a new Worksheet is added. This sheet will not be formatted however a template can be added in the parenthesis of the Add method to give that first sheet that format.
string template = "C:/whereEverMyTemplateIs/template"
wb = excel.Workbooks.Add(template);
Then just remember that it is added and make as many more as needed
for (int i = 1; i < pageCount; i++)
{
if (!backgroundWorker.CancellationPending)
{
backgroundWorker.ReportProgress(index++ * 100 / pageCount);
Thread.Sleep(delay);
wb.Sheets.Add(missing,After:wb.Sheets[wb.Sheets.Count],Count:missing,
Type:template);
//Worksheet ws = (Worksheet)wb.Sheets[wb.Sheets.Count];
}
}
I'm using C# and Interop to develop an Excel add-in. I wrote code which inserts formulas into cells one-by-one which works fine:
private static void SetFormulasCellwise(Range dataRange,
IEnumerable<string> columnFormulas,
int columnIndex)
{
var rowIndex = 0;
foreach (var columnFormula in columnFormulas)
{
var dataColumnCell = dataRange.Cells[rowIndex + 1, columnIndex + 1];
try
{
dataColumnCell.FormulaLocal = columnFormula;
}
catch (COMException)
{
MessageBox.Show("Failed to set column formula\n"
+ columnFormula + "\n" +
"to row " + (rowIndex + 1) + " and column "
+ (columnIndex + 1) + ".\n" +
"Please make sure the formula is valid\n" +
"and that the workbook is writable.",
"Error");
throw;
}
rowIndex++;
}
}
However, performance wise this isn't ideal. Even with disabling formula calculations, setting all formulas and re-enabling formula calculations this is kinda slow:
Excel.Application.Calculation = enable ? xlCalculationManual : xlCalculationAutomatic;
I wrote another version which sets the formulas column wise as follows:
private static void SetFormulasColumnwise(Range dataRange,
int columnIndex,
IReadOnlyCollection<string> columnFormulas)
{
var columnNumber = columnIndex + 1;
var dataColumnCell = dataRange.Range[dataRange.Cells[1, columnNumber],
dataRange.Cells[dataRange.Rows.Count, columnNumber]];
var columnFormulas2Dimensional = ToColumn2DimensionalArray(columnFormulas);
try
{
dataColumnCell.Formula = columnFormulas2Dimensional;
}
catch (COMException)
{
MessageBox.Show("Failed to set column formulas "
+ "to column " + columnNumber + ".\n" +
"Please make sure the formula is valid\n" +
"and that the workbook is writable.",
"Error");
throw;
}
}
private static string[,] ToColumn2DimensionalArray(IReadOnlyCollection<string> columnFormulas)
{
var columnFormulas2Dimensional = new string[columnFormulas.Count, 1];
var columnFormulasIndex = 0;
foreach (var columnFormula in columnFormulas)
{
columnFormulas2Dimensional[columnFormulasIndex, 0] = columnFormula;
columnFormulasIndex++;
}
return columnFormulas2Dimensional;
}
With the column version the formulas are inserted into the range but shifted downwards by an offset like 6 and they are not calculated automatically. If I select the formula bar and press Enter just that one formula is calculated but I need all of them automatically calculated. Why is this happening? Both code snippets should more or less be identical since they modify the cells in the same way. The column version just combines operations to improve performance. I tried forcing Excel to recalculate the range by calling e.g. dataRange.Calculate() but it did nothing.
What's wrong with the column version? How can I make it behave like the first version but more efficiently than the first version?
dataColumnCell.Formula = columnFormulas2Dimensional;
columnFormulas2Dimensional is of type: string[,], but dataColumnCell.Formula is waiting for a type: string
Just to try, this should display a value:
dataRange.FormulaLocal = columnFormulas2Dimensional[0,0];
(I've tested on Visual Studio 2017 and it seems to work. I've just used a basic formula.)
ThisAddIn.SetFormulasColumnwise(Globals.ThisAddIn.GetTestCells(), 1, new List<string>() { "=SOMME(A1:A10)", "=SOMME(A1:A10)", "=SOMME(A1:A10)", "=SOMME(A1:A10)" });
Here is my full code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml.Linq;
using Excel = Microsoft.Office.Interop.Excel;
using Office = Microsoft.Office.Core;
using Microsoft.Office.Tools.Excel;
using System.Windows.Forms;
using System.Runtime.InteropServices;
using Microsoft.Office.Interop.Excel;
namespace ExcelAddIn1
{
public partial class ThisAddIn
{
private void ThisAddIn_Startup(object sender, System.EventArgs e)
{
}
private void ThisAddIn_Shutdown(object sender, System.EventArgs e)
{
}
public void Calc(Range dataRange)
{
dataRange.Calculate();
}
public void SetSource()
{
Excel.Worksheet ws = Application.ActiveSheet as Excel.Worksheet;
Excel.Range c1 = ws.Cells[1, 1];
Excel.Range c2 = ws.Cells[10, 1];
var oRangeSource = (Excel.Range)ws.get_Range(c1, c2);
for (int i = 1; i <= 10; i++)
{
oRangeSource.Cells[i, 1].Value = i;
}
}
public Excel.Range GetTestCells()
{
Excel.Worksheet ws = Application.ActiveSheet as Excel.Worksheet;
Excel.Range c1 = ws.Cells[1, 2];
Excel.Range c2 = ws.Cells[10, 2];
//Get the range using number index
var oRange = (Excel.Range)ws.get_Range(c1, c2);
return oRange;
}
public static void SetFormulasCellwise(Range dataRange,IEnumerable<string> columnFormulas,int columnIndex)
{
var rowIndex = 0;
foreach (var columnFormula in columnFormulas)
{
var dataColumnCell = dataRange.Cells[rowIndex+1 , columnIndex];
try
{
dataColumnCell.FormulaLocal = columnFormula;
}
catch (COMException)
{
MessageBox.Show("Failed to set column formula\n"
+ columnFormula + "\n" +
"to row " + (rowIndex + 1) + " and column "
+ (columnIndex + 1) + ".\n" +
"Please make sure the formula is valid\n" +
"and that the workbook is writable.",
"Error");
throw;
}
rowIndex++;
}
}
public static void SetFormulasColumnwise(Range dataRange, int columnIndex, IReadOnlyCollection<string> columnFormulas)
{
var columnNumber = columnIndex;
var dataColumnCell = dataRange;
var columnFormulas2Dimensional = ToColumn2DimensionalArray(columnFormulas);
try
{
dataRange.FormulaLocal = columnFormulas2Dimensional[0,0];
}
catch (COMException)
{
MessageBox.Show("Failed to set column formulas "
+ "to column " + columnNumber + ".\n" +
"Please make sure the formula is valid\n" +
"and that the workbook is writable.",
"Error");
throw;
}
}
public static string[,] ToColumn2DimensionalArray(IReadOnlyCollection<string> columnFormulas)
{
var columnFormulas2Dimensional = new string[columnFormulas.Count, 1];
var columnFormulasIndex = 0;
foreach (var columnFormula in columnFormulas)
{
columnFormulas2Dimensional[columnFormulasIndex, 0] = columnFormula;
columnFormulasIndex++;
}
return columnFormulas2Dimensional;
}
#region Code généré par VSTO
/// <summary>
/// Méthode requise pour la prise en charge du concepteur - ne modifiez pas
/// le contenu de cette méthode avec l'éditeur de code.
/// </summary>
private void InternalStartup()
{
this.Startup += new System.EventHandler(ThisAddIn_Startup);
this.Shutdown += new System.EventHandler(ThisAddIn_Shutdown);
}
#endregion
}
}
and in my test ribbon button click:
private void testxav_Click(object sender, RibbonControlEventArgs e)
{
Globals.ThisAddIn.SetSource();
ThisAddIn.SetFormulasColumnwise(Globals.ThisAddIn.GetTestCells(), 1, new List<string>() { "=SOMME(A1:A10)", "=SOMME(A1:A10)", "=SOMME(A1:A10)", "=SOMME(A1:A10)" });
//ThisAddIn.SetFormulasCellwise(Globals.ThisAddIn.GetTestCells(), new List<string>() { "=SOMME(A1:A10)", "=SOMME(A1:A10)", "=SOMME(A1:A10)", "=SOMME(A1:A10)" },1);
Globals.ThisAddIn.Calc(Globals.ThisAddIn.GetTestCells());
}
Reconsidering a different solution according to your comments:
Is that what you need ? (I've used modulo to insert each formula in columnFormulas collection in every cell of the desired range)
Because you have 1 different formula per cell, I cannot figure out how you can do it without iterating on each cell
public static void SetFormulasColumnwise(Range dataRange, IReadOnlyCollection<string> columnFormulas)
{
try
{
foreach (Range c in dataRange.Cells)
//c.Row.ToString() returns the current row index
c.FormulaLocal = columnFormulas.ElementAt((int.Parse(c.Row.ToString())-1) % (columnFormulas.Count()));
}
catch (COMException)
{
MessageBox.Show("Failed to set column formulas "
+
"Please make sure the formula is valid\n" +
"and that the workbook is writable.",
"Error");
throw;
}
}
In the Test Ribbon, I've called SetFormulasColunWise with 5 different formulas to be sure they are inserted properly
ThisAddIn.SetFormulasColumnwise(Globals.ThisAddIn.GetTestCells(), new List<string>() { "=SOMME(A1:A10)", "=SOMME(A1:A2)", "=SOMME(A3:A4)", "=SOMME(A5:A8)", "=SOMME(A3:A8)" });
whereas GetTestCells is
public Excel.Range GetTestCells()
{
Excel.Worksheet ws = Application.ActiveSheet as Excel.Worksheet;
Excel.Range c1 = ws.Cells[1, 2];
Excel.Range c2 = ws.Cells[20, 2];
//Get the range using number index
var oRange = (Excel.Range)ws.get_Range(c1, c2);
return oRange;
}
in this screenshot below you can see Excel results (Column C and D added manually just so you can see which formula is computed in B)
Currently, I am able to read data from multiple CSV file and plot line graph using windows form application. However, now I need to plot a line graph based on a CSV file's section name (3rd column of csv file).
Modified/New CSV file: (Added the Section Name column)
Values,Sector,Name
5.55,1024,red
5.37,1536,red
5.73,2048,blue
5.62,2560,.blue
5.12,3072,.yellow
...
Based on the Section Name column, my line graph need to be plotted accordingly in a Single line and different sections must be plotted with different colors, including the legends shown at the side of the graph must be shown based on the different section names.
1 csv file = 1 Series. But there are same section names in a csv file (csv file example shown above, e.g. red for the 1st 20lines). Same section names = same color. If I open 2 or more csv files = 2 Series. Each Series will have different colors due to different section names in the csv file.
I am quite new with programming, and would really appreciate someone could help me by editing from my code.
Thank you.
Updated code:
GraphDemo (Form):
List<Read> rrList = new List<Read>();
void openToolStripMenuItem_Click(object sender, EventArgs e)
{
OpenFileDialog ff = new OpenFileDialog();
Read rr;
ff.InitialDirectory = Environment.GetFolderPath(Environment.SpecialFolder.Desktop); //"C:\\";
ff.Filter = "csv files (*.csv)|*.csv|All files (*.*)|*.*";
ff.Multiselect = true;
ff.FilterIndex = 1;
ff.RestoreDirectory = true;
if (ff.ShowDialog() == DialogResult.OK)
{
try
{
rrList.Clear();
foreach (String file in ff.FileNames) //if ((myStream = ff.OpenFile()) != null)
{
rr = new Read(file);
rrList.Add(rr);
}
//Populate the ComboBoxes
if (rrList.Count > 0)
{
string[] header = rrList[0].header; //header of first file
xBox.DataSource = header;
yBox.DataSource = header.Clone(); //without Clone the 2 comboboxes link together!
}
if (yBox.Items.Count > 1) yBox.SelectedIndex = 1; //select second item
}
catch (Exception err)
{
//Inform the user if we can't read the file
MessageBox.Show(err.Message);
}
}
}
private void button1_Click(object sender, EventArgs e)
{
Plot.Draw(rrList, xBox, yBox, chart);
}
class Read:
public class Read
{
public int nLines { get; private set; }
public int nColumns { get; private set; }
public string[] header { get; private set; }
public float[,] data { get; private set; }
public string fileName { get; set; }
public string[] section { get; private set; }
public Read(string file)
{
string[] pieces;
fileName = Path.GetFileName(file);
string[] lines = File.ReadAllLines(file); // read all lines
if (lines == null || lines.Length < 2) return; //no data in file
header = lines[0].Split(','); //first line is header
nLines = lines.Length - 1; //first line is header
nColumns = header.Length;
//read the numerical data and section name from the file
data = new float[nLines, nColumns - 1]; // *** 1 less than nColumns as last col is sectionName
section = new string[nLines]; // ***
for (int i = 0; i < nLines; i++)
{
pieces = lines[i + 1].Split(','); // first line is header
if (pieces.Length != nColumns) { MessageBox.Show("Invalid data at line " + (i + 2) + " of file " + fileName); return; }
for (int j = 0; j < nColumns - 1; j++)
{
float.TryParse(pieces[j], out data[i, j]); //data[i, j] = float.Parse(pieces[j]);
}
section[i] = pieces[nColumns - 1]; //last item is section
}
}
}
class Plot:
public class Plot
{
//public Plot() { } //no constructor required as we use a static class to be called
public static void Draw(List<Read> rrList, ComboBox xBox, ComboBox yBox, Chart chart) //***
{
int indX = xBox.SelectedIndex;
int indY = yBox.SelectedIndex;
chart.Series.Clear(); //ensure that the chart is empty
chart.Legends.Clear();
Legend myLegend = chart.Legends.Add("myLegend");
myLegend.Title = "myTitle";
//define a set of colors to be used for sections
Color[] colors = new Color[] { Color.Black, Color.Blue, Color.Red, Color.Green, Color.Magenta, Color.DarkCyan, Color.Chocolate, Color.DarkMagenta };
//use a Dictionary to keep iColor of each section
// key=sectionName, value=iColor (color index in our colors array)
var sectionColors = new Dictionary<string, int>();
int i = 0;
int iColor = -1, maxColor = -1;
foreach (Read rr in rrList)
{
float[,] data = rr.data;
int nLines = rr.nLines;
int nColumns = rr.nColumns;
string[] header = rr.header;
chart.Series.Add("Series" + i);
chart.Series[i].ChartType = SeriesChartType.Line;
//chart.Series[i].LegendText = rr.fileName;
chart.Series[i].IsVisibleInLegend = false; //hide default item from legend
chart.ChartAreas[0].AxisX.LabelStyle.Format = "{F2}";
chart.ChartAreas[0].AxisX.Title = header[indX];
chart.ChartAreas[0].AxisY.Title = header[indY];
for (int j = 0; j < nLines; j++)
{
int k = chart.Series[i].Points.AddXY(data[j, indX], data[j, indY]);
string curSection = rr.section[j];
if (sectionColors.ContainsKey(curSection))
{
iColor = sectionColors[curSection];
}
else
{
maxColor++;
iColor = maxColor; sectionColors[curSection] = iColor;
}
chart.Series[i].Points[k].Color = colors[iColor];
}
i++; //series#
} //end foreach rr
//fill custom legends based on sections/colors
foreach (var x in sectionColors)
{
string section = x.Key;
iColor = x.Value;
myLegend.CustomItems.Add(colors[iColor], section); //new LegendItem()
}
}
}
You can separate the data by the section column and use the section names as index into the Series collection instead of using i.
Best use the section name as the Series.Name. I suggest using a data class containing the two numbers and the string and collect them in a List<Dataclass>. Then create Series for the distinct sections. Then loop over them..
Here are a few code examples:
Define a class for your data:
public class Data3
{
public int N1 { get; set;}
public double N2 { get; set;}
public string S1 { get; set;}
public Data3(double n2, int n1, string s1)
{
N1 = n1; N2 = n2; S1 = s1;
}
}
Pick your own names! Optional but always recommended: Add a nice ToString() overload!
Declare a class level varible:
List<Data3> data = new List<Data3>();
During the read collect the data there:
data.Add(new Data3(Convert.ToDouble(pieces[1]), Convert.ToInt32(pieces[0]), pieces[2]));
To plot the chart first create the Series:
var sections= data.Select(x => x.S1).Distinct<string>();
foreach (string s in sections)
chart.Series.Add(new Series(s) { ChartType = SeriesChartType.Line });
Then plot the data; the series can be indexed by their Names:
foreach (var d in data) chart.Series[d.S1].Points.AddXY(d.N1, d.N2);
I left out the nitty gritty of integrating the code into your application; if you run into issues, do show the new code by editing your question!
A few notes:
When in doubt always create a class to hold your data
When in doubt always choose classes over structures
When in doubt always choose List<T> over arrays
Always try to break your code down to small chunks with helpful names.
Example: To read all the data in a csv file create a function to do so:
public void AppendCsvToDataList(string file, List<Data3> list)
{
if (File.Exists(file))
{
var lines = File.ReadAllLines(file);
for (int l = 1; l < lines.Length; l++)
{
var pieces = lines[l].Split(',');
list.Add(new Data3(Convert.ToInt32(pieces[1]),
Convert.ToDouble(pieces[0]), pieces[2]));
}
}
}
I think I'm running into a case of "the easiest answers are the hardest ones to find" and I haven't come across any searches that give this to me in a straightforward way. This is for Excel 2010 and VS 2010 within an existing VSTO (C#) project.
I have an Excel worksheet that contains 4 columns of data that I would like to use as a source for a DataGridView. Can someone please provide C# code snippets for (1) getting the data from a particular worksheet and populating a custom object with it? (2) binding the object (like an IEnumerable list) to a Datagridview and (3) some snippets for the update and delete functionality that would be inherent to the grid and feed back to the source worksheet.
I know I'm asking for a lot here, but so much of the VSTO information seems to be dis-jointed and not always easy to find. Thanks!
Edit: Great, I just noticed that I missed a big part of your question, getting updates and deletes back to the worksheet. I have absolutely no idea if that is possible but I think that makes my solution worthless. I'll leave it here anyway, maybe it can help in any way.
Why do you need VSTO? As far as I know VSTO is used for Office Add-Ins. But since you want to show the data in a DataGridView I assume that you have a WinForms application that should just access a workbook. In this case you can simply open the workbook by using Office Interop. Just add a reference to Microsoft.Office.Interop.Excel to your project and add a using Microsoft.Office.Interop.Excel; statement.
MSDN reference documentation for Excel Interop can be found here: http://msdn.microsoft.com/en-us/library/ms262200%28v=office.14%29.aspx
I'll give you the Excel part, maybe someone else can do the rest.
First, open Excel and the workbook:
Application app = new Application();
// Optional, but recommended if the user shouldn't see Excel.
app.Visible = false;
app.ScreenUpdating = false;
// AddToMru parameter is optional, but recommended in automation scenarios.
Workbook workbook = app.Workbooks.Open(filepath, AddToMru: false);
Then somehow get the correct worksheet. You have a few possiblities:
// Active sheet (should be the one which was active the last time the workbook was saved).
Worksheet sheet = workbook.ActiveSheet;
// First sheet (notice that the first is actually 1 and not 0).
Worksheet sheet = workbook.Worksheets[1];
// Specific sheet.
// Caution: Default sheet names differ for different localized versions of Excel.
Worksheet sheet = workbook.Worksheets["Sheet1"];
Then get the correct range. You didn't specify how you know where the needed data is, so I'll assume it is in fixed columns.
// If you also know the row count.
Range range = sheet.Range["A1", "D20"];
// If you want to get all rows until the last one that has some data.
Range lastUsedCell = sheet.Cells.SpecialCells(XlCellType.xlCellTypeLastCell);
string columnName = "D" + lastUsedCell.Row;
Range range = sheet.Range["A1", columnName];
Get the values:
// Possible types of the return value:
// If a single cell is in the range: Different types depending on the cell content
// (string, DateTime, double, ...)
// If multiple cells are in the range: Two dimensional array that exactly represents
// the range from Excel and also has different types in its elements depending on the
// value of the Excel cell (should always be that one in your case)
object[,] values = range.Value;
That two dimensional object array can then be used as a data source for your DataGridView. I haven't used WinForms for years so I don't know if you can bind it directly or first need to get the data into some specific format.
Finally close Excel again:
workbook.Close(SaveChanges: false);
workbook = null;
app.Quit();
app = null;
// Yes, we really want to call those two methods twice to make sure all
// COM objects AND all RCWs are collected.
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
GC.WaitForPendingFinalizers();
Correctly closing Excel after using Interop is a task itself because you have to make sure that all references to COM objects have been released. The easiest way I have found to do this is to do all the work except opening and closing Excel and the workbook (so my first and last code block) in a seperate method. This ensures that all COM objects used in that method are out of scope when Quit is called.
UPDATE:
I replaced my previous method with newer code for faster approach. System.Array is quite efficient and faster way to read and bind data to the excel. You can download the demo from this link.
I have developed VSTO application in Excel 2003 Workbook. There is no big differences in terms of syntax,so you can use it in 2007 / 2010 with no efforts.
I didn't know which event you will be using to open the window showing data so i am assuming that you will be using.
SheetFollowHyperlink
I am going to use Static workbook object declared in Showdata.cs. Here's the code for your Thisworkbook.cs
private void ThisWorkbook_Startup(object sender, System.EventArgs e)
{
ShowData._WORKBOOK = this;
}
private void ThisWorkbook_SheetFollowHyperlink(object Sh, Microsoft.Office.Interop.Excel.Hyperlink Target)
{
System.Data.DataTable dTable = GenerateDatatable();
showData sh = new showData(dTable);
sh.Show(); // You can also use ShowDialog()
}
I have added a Link on the current sheet and it will pop up the window with a datagridview.
private System.Data.DataTable GenerateDatatable()
{
Range oRng = null;
// It takes the current activesheet from the workbook. You can always pass any sheet as an argument
Worksheet ws = this.ActiveSheet as Worksheet;
// set this value using your own function to read last used column, There are simple function to find last used column
int col = 4;
// set this value using your own function to read last used row, There are simple function to find last used rows
int row = 5;
//lets assume its 4 and 5 returned by method
string strRange = "A1";
string andRange = "D5";
System.Array arr = (System.Array)ws.get_Range(strRange, andRange).get_Value(Type.Missing);
System.Data.DataTable dt = new System.Data.DataTable();
for (int cnt = 1;
cnt <= col; cnt++)
dt.Columns.Add(cnt.Chr(), typeof(string));
for (int i = 1; i <= row; i++)
{
DataRow dr = dt.NewRow();
for (int j = 1; j <= col; j++)
{
dr[j - 1] = arr.GetValue(i, j).ToString();
}
dt.Rows.Add(dr);
}
return dt;
}
Here's the form which will allow user to display and edit values. I have added extension methods and Chr() to convert numerical into respective alphabets which will come handy.
public partial class ShowData : Form
{
//use static workbook object to access Worksheets
public static ThisWorkbook _WORKBOOK;
public ShowData(System.Data.DataTable dt)
{
InitializeComponent();
// binding value to datagrid
this.dataGridView1.DataSource = dt;
}
private void RefreshExcel_Click(object sender, EventArgs e)
{
Worksheet ws = ShowData._WORKBOOK.ActiveSheet as Worksheet;
System.Data.DataTable dTable = dataGridView1.DataSource as System.Data.DataTable;
// Write values back to Excel sheet
// you can pass any worksheet of your choice in ws
WriteToExcel(dTable,ws);
}
private void WriteToExcel(System.Data.DataTable dTable,Worksheet ws)
{
int col = dTable.Columns.Count; ;
int row = dTable.Rows.Count;
string strRange = "A1";
string andRange = "D5";
System.Array arr = Array.CreateInstance(typeof(object),5,4);
for (int i = 0; i < row; i++)
{
for (int j = 0; j < col; j++)
{
try
{
arr.SetValue(dTable.Rows[i][j].ToString(), i, j);
}
catch { }
}
}
ws.get_Range(strRange, andRange).Value2 = arr;
this.Close();
}
public static class ExtensionMethods
{
static string alphabets = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
public static string Chr(this int p_intByte)
{
if (p_intByte > 0 && p_intByte <= 26)
{
return alphabets[p_intByte - 1].ToString();
}
else if (p_intByte > 26 && p_intByte <= 700)
{
int firstChrIndx = Convert.ToInt32(Math.Floor((p_intByte - 1) / 26.0));
int scndIndx = p_intByte % 26;
if (scndIndx == 0) scndIndx = 26;
return alphabets[firstChrIndx - 1].ToString() + alphabets[scndIndx - 1].ToString();
}
return "NA";
}
}
this is one of the most ugly codes I've written but it will work as a proof of concept :)
I've created an example workbook like that
Column1 Column2 Column3 Column4
------------------------------------------------------
Data-1-1 Data-2-1 Data-3-1 Data-4-1
Data-1-2 Data-2-2 Data-3-2 Data-4-2
....
Excel file contains exactly 50 lines, this explains the hard-coded range selectors.
After writing that part of code rest is easy, just create a form, add a dataviewgrid, create a data source for MyExcelData, create an instance of MyExcelData like var data = new MyExcelData(pathToExcelFile); and bind it to grid.
Code is ugly, and has many assumptions but it implements your requirements. If you open excel and program you can see updates on grid are reflected on excel after cell edited. deleted row is also removed from excel. since I did not know whether you have primary keys of your excel or not, I used row index as ID.
BTW, I'm really bad when it comes to VSTO. so if you know a better way open/edit/save please notify me.
public class MyExcelDataObject
{
private readonly MyExcelData owner;
private readonly object[,] realData;
private int RealId;
public MyExcelDataObject(MyExcelData owner, int index, object[,] realData)
{
this.owner = owner;
this.realData = realData;
ID = index;
RealId = index;
}
public int ID { get; set; }
public void DecrementRealId()
{
RealId--;
}
public string Column1
{
get { return (string)realData[RealId, 1]; }
set
{
realData[ID, 1] = value;
owner.Update(ID);
}
}
public string Column2
{
get { return (string)realData[RealId, 2]; }
set
{
realData[ID, 2] = value;
owner.Update(ID);
}
}
public string Column3
{
get { return (string)realData[RealId, 3]; }
set
{
realData[ID, 3] = value;
owner.Update(ID);
}
}
public string Column4
{
get { return (string)realData[RealId, 4]; }
set
{
realData[ID, 4] = value;
owner.Update(ID);
}
}
}
public class MyExcelData : BindingList<MyExcelDataObject>
{
private Application excel;
private Workbook wb;
private Worksheet ws;
private object[,] values;
public MyExcelData(string excelFile)
{
excel = new ApplicationClass();
excel.Visible = true;
wb = excel.Workbooks.Open(excelFile);
ws = (Worksheet)wb.Sheets[1];
var range = ws.Range["A2", "D51"];
values = (object[,])range.Value;
AllowEdit = true;
AllowRemove = true;
AllowEdit = true;
for (var index = 0; index < 50; index++)
{
Add(new MyExcelDataObject(this, index + 1, values));
}
}
public void Update(int index)
{
var item = this[index - 1];
var range = ws.Range["A" + (2 + index - 1), "D" + (2 + index - 1)];
range.Value = new object[,]
{
{item.Column1, item.Column2, item.Column3, item.Column4}
};
}
protected override void RemoveItem(int index)
{
var range = ws.Range[string.Format("A{0}:D{0}", (2 + index)), Type.Missing];
range.Select();
range.Delete();
base.RemoveItem(index);
for (int n = index; n < Count; n++)
{
this[n].DecrementRealId();
}
}
}
PS: I'd like to use lightweight objects but it adds unnecessary complications.
So in the Sheet1_Startup event
Excel.Range range1 = this.Range["A1", missing];
var obj = range1.Value2.ToString();
You would need to move to the next cell then
range1 = this.Range["A2", missing];
obj = New list(range1.Value2.ToString());