Improve performance of Excel file creation - c#

I have an application with a TabControl. It gets some TabPages, all having a DataGridView that gets filled with a DataTable.
Once the TabControl is filled, I want to be able to export all of the DataGridViews (or rather their DataSources, which are all DataTables) into one Excel file.
I have the following code for that. It works but takes almost a minute.
Button gets clicked:
private void exportBtn_Click(object sender, EventArgs e)
{
var result = new List<DataTable>();
foreach (TabPage page in tabControl1.TabPages)
{
var dgv = page.Controls[0] as DataGridView;
if (dgv == null) continue;
var dt = dgv.DataSource as DataTable;
if (dt == null) continue;
dt.TableName = page.Text;
result.Add(dt);
}
ExportServices.ToExcel(result);
}
ExportServices looks like this:
internal static class ExportServices
{
public static void ToExcel(List<DataTable> tables)
{
var excelApp = new Microsoft.Office.Interop.Excel.Application();
excelApp.Workbooks.Add();
foreach (var table in tables)
{
table.AddSheetToExcelApp(excelApp);
}
excelApp.Visible = true;
}
}
The extension method for DataTable, taken from this question:
public static void AddSheetToExcelApp(this DataTable Tbl, Microsoft.Office.Interop.Excel.Application excelApp)
{
try
{
if (Tbl == null || Tbl.Columns.Count == 0)
throw new Exception("ExportToExcel: Null or empty input table!\n");
// single worksheet
_Worksheet workSheet = (_Worksheet)excelApp.Sheets.Add();
workSheet.Name = Tbl.TableName.Remove(5,1);
// column headings
for (int i = 0; i < Tbl.Columns.Count; i++)
{
workSheet.Cells[1, (i + 1)] = Tbl.Columns[i].ColumnName;
}
// rows
for (int i = 0; i < Tbl.Rows.Count; i++)
{
for (int j = 0; j < Tbl.Columns.Count; j++)
{
workSheet.Cells[(i + 2), (j + 1)] = Tbl.Rows[i][j];
}
}
}
catch (Exception ex)
{
throw new Exception("ExportToExcel: \n" + ex.Message);
}
}
As I said, this code works. But it takes forever to do so. Pausing the execution of the program during random times showed me that most of the time it's doing work in the loop below // rows.
Any way to accelerate this? It would really be not much fun for the user to wait for a minute for just one Excel file.
EDIT: Forgot to mention I can't use any other libraries than the ones that I have installed. Our company is working with very sensitive data so we are not allowed to run any code from the "outside world".

To set cells one by one is very inefficient. I advise you to set the whole table at once:
object[,] array = new object[Tbl.Rows.Count,Tbl.Columns.Count]
// Fill the array with values then
workSheet.Range[workSheet.Cells[1, 1], workSheet.Cells[Tbl.Rows.Count, Tbl.Columns.Count]].Value = array;
Or, at least, to use bigger pieces.

It is normal that using COM dll to Excel it's take time to do this
Try this. You don't neeed excel on machine. I tested and I used this approach to export data to excel 1 000 000 rows
https://github.com/jsegarra1971/SejExcelExport
Full example how to use it:
http://www.codeproject.com/Tips/829389/Fast-Excel-Import-and-Export

I have used three methods over time:
1) write out a csv file, then import that into the excel sheet.
2) put the data into the clipboard, separating values with tabs, then pasting it into Excel (probably using paste special -> no formatting). Whether you can do this obviously depends a lot on the environment in which the programs are running.
3) learn about and use OpenXmlWriter. This is far more flexible than any of the other options, but also has quite a learning curve to get right.

You can generate the Excel file without executing Excel Application
You can use EPPlus library. It's a mature library with lots of functionality

Related

How to get the row number of the last row when you are printing an excel sheet (EPPlus)

I want to add one picture (displaying "DRAFT") by printable Excel worksheet in C# EPPlus.
I need to know if there is a way to find the last visible row of each page of a worksheet when you are printing it. I can't pretend that it will always be a fix number of row per page because it depends on the content of the cells.
Here is my current code that use a fix number of row per page (30) to insert image. This result in approximately one image per printable page except that in each new page the image is not at the same place. (Slightly off, depending on content of cells.)
public void InsertDraftImage(ExcelWorksheet worksheet, FileInfo draft_image)
{
int maxRowNumber = worksheet.Dimension.End.Row;
int rowByPage = 30;
int numberOfPage = (maxRowNumber / rowByPage) + 1;
ExcelPicture picture = null;
for(int i = 0; i < numberOfPage; i++)
{
if(draft_image != null)
{
picture = worksheet.Drawings.AddPicture(i.ToString(), draft_image);
picture.SetSize(609, 545); //original image size
picture.SetPosition(i * rowByPage, 0, 1, 0);
picture.EditAs = eEditAs.Absolute;
}
}
After trying to implement the missing code in 'ExcelHeaderFooter.cs' from the EPPlus with a workmate without success, we finally did it by following Ernie suggestion!!
There is my final code to insert a picture into each page of a printable excel file generate with EPPlus in C#.
It is done by adding the picture in the footer and setting the Boolean ScaleWithDoc to false (default = true).
public void InsertDraftImage(ExcelWorksheet worksheet, FileInfo draft_image)
{
ExcelHeaderFooterText footer = worksheet.HeaderFooter.OddFooter; //all page have same footer
footer.InsertPicture(draft_image, PictureAlignment.Centered);
}
Added this code in my method to create the ExcelWorksheet (all the other excel style, populate, settings).
XmlAttribute temp = worksheet.WorksheetXml.CreateAttribute("scaleWithDoc");
temp.Value = "0";
worksheet.WorksheetXml.GetElementsByTagName("headerFooter")[0].Attributes.Append(temp);
package.Save();

Lightswitch export all rows to CSV

I am using c# and VS2012 on a lightswitch web-application,
I wish to export my data to CSV (on a search screen!), but can't reach any POC,
As i understand there are 2 main problems - a savefiledialog must be caused directly from a user button and in it must happened in the main dispatcher,
I used this code :
partial void mySearchScreen_Created()
{
var CSVButton = this.FindControl("ExportToCSV");
CSVButton.ControlAvailable += ExportCSV_ControlAvailable;
}
private void ExportCSV_ControlAvailable(object sender, ControlAvailableEventArgs e)
{
this.FindControl("ExportToCSV").ControlAvailable -= ExportCSV_ControlAvailable;
Button Button = (Button)e.Control;
Button.Click += ExportCSV_Click;
}
private void ExportCSV_Click(object sender, System.Windows.RoutedEventArgs e)
{
Microsoft.LightSwitch.Details.Client.IScreenCollectionProperty collectionProperty = this.Details.Properties.mySearch;
var intPageSize = collectionProperty.PageSize;
//Get the Current PageSize and store to variable
collectionProperty.PageSize = 0;
var dialog = new SaveFileDialog();
dialog.Filter = "CSV (*.csv)|*.csv";
if (dialog.ShowDialog() == true) {
using (StreamWriter stream = new StreamWriter(dialog.OpenFile())) {
string csv = GetCSV();
stream.Write(csv);
stream.Close();
this.ShowMessageBox("Excel File Created Successfully. NOTE: When you open excel file and if you receive prompt about invalid format then just click yes to continue.", "Excel Export", MessageBoxOption.Ok);
}
}
collectionProperty.PageSize = intPageSize;
//Reset the Current PageSize
}
private string GetCSV()
{
StringBuilder csv = new StringBuilder();
int i = 0;
foreach (var orderRow_loopVariable in mySearch) {
var orderRow = orderRow_loopVariable;
////HEADER
if (i == 0) {
int c = 0;
foreach (var prop_loopVariable in orderRow.Details.Properties.All().OfType<Microsoft.LightSwitch.Details.IEntityStorageProperty>()) {
var prop = prop_loopVariable;
if (c > 0) {
csv.Append(",");//Constants.vbTab
}
c = c + 1;
csv.Append(prop.DisplayName);
}
}
csv.AppendLine("");
////DATA ROWS
int c1 = 0;
foreach (var prop_loopVariable in orderRow.Details.Properties.All().OfType<Microsoft.LightSwitch.Details.IEntityStorageProperty>()) {
var prop = prop_loopVariable;
if (c1 > 0) {
csv.Append(",");//Constants.vbTab
}
c1 = c1 + 1;
csv.Append(prop.Value);
}
i = i + 1;
}
if (csv.Length > 0) {
return csv.ToString(0, csv.Length - 1);
} else {
return "";
}
}
This works, but it only get's me the first page items,
On another thing i had to do i solved that problem by using this code :
this.DataWorkspace.myDataContextData.MySearch(...).Execute();
Yet trying that instead of just using 'MySearch' gives me the following error :
t is not valid to call Execute() on a different Dispatcher than the ExecutableObject's Logic Dispatcher.
Why is it so difficult to do such a basic thing related to data (export to csv/excel) on a system build for handling data ?
Any ideas ?
The simplest workaround if this is the only use of the search screen would be to turn off paging. To do this go to the screen designer, highlight the query on the left, and in properties uncheck 'support paging.'
I'm not sure what the limitations are, but you can run some code in a different dispatcher using:
this.Details.Dispatcher.BeginInvoke(() =>
{
//This runs on main dispatcher
});
I don't think there's anything wrong with your code, but I've noticed that it takes a while to reset the page size on a large collection, in which time the rest of your code continues to execute. I think that's why you only get the first page. The only solution I've found is to wait.
When the "File Download - Security Warning" dialog pops up, keep an eye on the 'busy' indicator on the screen's tab and also the 'Page x of y' status at the bottom of the grid if you can see it. Only when the busy indicator has gone and the status just says 'Page' should you click OK to continue.
I haven't figured out a way of doing this programmatically so it's not a very helpful feature unless you have a very tightly controlled user population. But if it's just you and a couple of power users, it is workable. I'm also not sure if this has been improved on in versions after VS2012.
There can be a downside to the other answer of taking the paging off the query entirely. I've tried that workaround when the grid collection was being displayed in a modal window and the window became uncloseable if there were too many rows in the grid.
Phil

Color non-consecutive cells in an Excel sheet

This is what happens:
xlValues is set as an Excel.Range object.
I have tried the following as well, all giving me the same error:
//xlValueRange = xlSheet...
.get_Range("A1:A5,A15:A25,A50:A65");
.UsedRange.Range["A1:A5,A15:A25,A50:A65"];
.Range["A1:A5,A15:A25,A50:A65"];
xlApp.ActiveWorkbook.ActiveSheet.Range["A1:A5,A15:A25,A50:A65"];
//I have also tried these alternatives with ".Select()" after the brackets and
//", Type.Missing" inside the brackets
//This works though...
xlSheet.Range["A1:A5"];
I'm trying to recolor specific cells in an excel sheet, I have found a solution by using two loops but it's simply too slow. Running through a column of 30 000 cells takes minutes.
I have never done anything like this before and I used this tutorial to get me started.
This solution uses a bool array with cells to be colored set to true.(recolored)
//using Excel = Microsoft.Office.Interop.Excel;
xlApp = new Excel.Application();
xlApp.Visible = true;
xlBook = xlApp.Workbooks.Add(Type.Missing);
xlSheet = (Excel.Worksheet)xlBook.Sheets[1];
for (int i = 1; i < columns + 1; i++)
{
for (int j = 1; j < rows + 1; j++)
{
if (recolored[j, i])
xlSheet.Cells[j+1, i+1].Interior.Color = Excel.XlRgbColor.rgbRed;
}
}
}
What I would like to do is something like this:
Excel.XlRgbColor[,] color;
//Loop to fill color with Excel.XlRgbColor.rgbRed at desired cells.
var startCell = (Excel.Range)xlSheet.Cells[1, 1];
var endCell = (Excel.Range)xlSheet.Cells[rows, columns];
var xlRange = xlSheet.Range[startCell, endCell];
xlRange.Interior.Color = color;
This one gives me an error on the final line though;
Type mismatch. (Exception from HRESULT: 0x80020005 (DISP_E_TYPEMISMATCH))
My first guess would be to make an Excel.Range object that covers the cells I want to have red and use that object in place of xlRange, something like this:
RangeObject.Interior.Color = Excel.XlRgbColor.rgbRed;
I don't know if it's possible to make an Excel.Range object with gaps like that though, I could use some help on this one.
You can select non-consecutive cells by using comma-separated list of ranges like this:
this.Application.ActiveWorkbook.ActiveSheet.Range["A2:A4,B3:B16"].Select();
You can then re-color the selection using:
Selection.Interior.Color = ColorTranslator.ToOle(Color.Yellow);
This will get rid of the coloring loop you're having trouble with.
Also, in a VSTO add-in, you should normally never need to do new Excel.Application() in your code. this.Application in the Add-in class should give you access to the active instance of Excel.
UPDATE
Here's a piece of code that should help you pin-point your problem. I added a Ribbon to my add-in and a simple button to the Ribbon. Behind the click event of this button, I have added the following code:
private void button1_Click(object sender, RibbonControlEventArgs e)
{
try
{
var App = Globals.ThisAddIn.Application;
if (App == null)
System.Windows.Forms.MessageBox.Show("App is null");
else
{
var Sheet = App.ActiveSheet;
if (Sheet == null)
System.Windows.Forms.MessageBox.Show("Sheet is null");
else
{
var Rng = Sheet.Range["A1:A5,A15:A25,A50:A65"];
if (Rng == null)
System.Windows.Forms.MessageBox.Show("Rng is null");
else
{
Rng.Select();
}
}
}
}
catch (Exception ee)
{
System.Windows.Forms.MessageBox.Show("Exception: " + ee.Message);
}
}
On my end this code runs successfully and selects the non-contiguous range of cells. Try this on your end and let me know what you see.
UPDATE 2
The same code works for me in a WinForms application with reference to Excel 14.0 (will hopefully work with Excel 12.0 too). Just a couple of minor changes are required. Here's the full code.
private void button1_Click(object sender, RibbonControlEventArgs e)
{
try
{
var App = new Microsoft.Office.Interop.Excel.Application();
if (App == null)
System.Windows.Forms.MessageBox.Show("App is null");
else
{
App.Workbooks.Add();
var Sheet = App.ActiveSheet;
if (Sheet == null)
System.Windows.Forms.MessageBox.Show("Sheet is null");
else
{
Microsoft.Office.Interop.Excel.Range Rng = Sheet.get_Range("A1");
Rng.Select();
Rng = Sheet.get_Range("A1:A5,A15:A25,A50:A65");
if (Rng == null)
System.Windows.Forms.MessageBox.Show("Rng is null");
else
{
Rng.Select();
App.Selection.Interior.Color = Microsoft.Office.Interop.Excel.XlRgbColor.rgbYellow;
App.ActiveWorkbook.SaveAs("testtest.xlsx");
App.Quit();
}
}
}
}
catch (Exception ee)
{
System.Windows.Forms.MessageBox.Show("Exception: " + ee.Message);
}
}
I had the same problem and it turned out that it was a bad list separator - in my case instead of comma there should be a semicolon.
So instead of
.Range["A1:A5,A15:A25,A50:A65"];
try:
private string listSep = System.Globalization.CultureInfo.CurrentCulture.TextInfo.ListSeparator;
.Range["A1:A5" + listSep + "A15:A25" + listSep + "A50:A65"];
[RangeObject].Interior.Color changes the cell background color. Use this
[RangeObject].Interior.Color = System.Drawing.ColorTranslator.ToOle(System.Drawing.Color.Red);
For cell text, use
[RangeObject].Font.Color
I've struggled with this issue for a long time, too. But today I believe I finally found the solution (and the cause).
The problem is that Excel uses the current regional settings to determine the comma operator, ie. the separator between two ranges (don't ask me why - to me it's as insane as localizing the function names).
Anyway, on my computer I have the Czech locale set so the separator to use is a semicolon, not a comma! If I use it in the parameter for the Range method, it works perfectly.
From the discussion I got the impression that you are Swedish so you likely have the Swedish locale set. Its default list separator is also a semicolon, so it seems likely to me that this might solve your problem, too. You can always check the separator set in the computer regional settings by calling
System.Globalization.CultureInfo.InstalledUICulture.TextInfo.ListSeparator
Hope this helps!
The maximum length of the range address string is 255. So, you need to chunk your list so that the combined range address of each section is less than 255 long.

C# - Saving a DataGridView to file and loading

To start off, what I have is a simple Winforms app, with just a save and load button, and with a datagridview control to hold data. What I am looking to do is input some data into the control, hit the save button, and it save all the data to a file locally on the computer, and when I hit load, it loads the file and populates the control appropriately, keeping all rows, columns, and data the same as when saved.
Although it sounds fairly simple to me, I cant seem to figure a good way to save and load the data. Can I get a few pointers or examples to get myself started?
Thank you.
Bind the DataGridView to a DataTable, and use the DataTable
ReadXml() and WriteXml() methods to read and write the data to a file.
If you ever have multiple grids bound to multiple related tables, you can represent the schema with a DataSet and use the ReadXml() and WriteXml() methods of DataSet to read and write the whole schema.
There is an example on the MSDN page for DataTable.WriteXml() that you might find helpful.
I have tested a simple way to save datagridview to a file :
//DataGridView dgv=...
string file= "c:\\mygrid.bin";
using (BinaryWriter bw = new BinaryWriter(File.Open(file, FileMode.Create)))
{
bw.Write(dgv.Columns.Count);
bw.Write(dgv.Rows.Count);
foreach (DataGridViewRow dgvR in dgv.Rows)
{
for (int j = 0; j < dgv.Columns.Count; ++j)
{
object val=dgvR.Cells[j].Value;
if (val == null)
{
bw.Write(false);
bw.Write(false);
}
else
{
bw.Write(true);
bw.Write(val.ToString());
}
}
}
and for loading such a file into a datagridview:
//DataGridView dgv = ...
dgv.Rows.Clear();
string file="c:\\mygrid.bin";
using (BinaryReader bw = new BinaryReader(File.Open(file, FileMode.Open)))
{
int n=bw.ReadInt32();
int m=bw.ReadInt32();
for(int i=0;i<m;++i)
{
dgv.Rows.Add();
for (int j = 0; j < n; ++j)
{
if (bw.ReadBoolean())
{
dgv.Rows[i].Cells[j].Value = bw.ReadString();
}
else bw.ReadBoolean();
}
}
}
Consider that I have assumed that the datagridview control has fixed columns, in you specific situation you should add some codes to insert new columns or create a new gridview.

Save a datatable to excel sheet in vb.net winform application

A year ago I saw a beautiful simple code that gets a data table and saves it in an excel file.
The trick was to use the web library (something with http) and I'm almost sure it was a stream.
I find a lot of code with response but I can't make it work in a win-form environment.
There is also a cell by cell code - not interested -too slow.
I want to paste it as a range or something close.
Thanks
I believe this is the code you're looking for:
DataTable to Excel
It uses an HtmlTextWriter.
There are many component libraries out there that will provide this kind of functionality.
However, you could probably, most simply output the data as a CSV file and the load that into Excel.
What I like to do is put the datatable in a grid allowing the user to sort and filter. Then they can use the clipboard to copy/paste to Excel.
Private Sub mnuCopy_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles mnuCopy.Click
If dgvDisplaySet.GetClipboardContent Is Nothing Then
MsgBox("Nothing selected to copy to clipboard.")
Else
Clipboard.SetDataObject(dgvDisplaySet.GetClipboardContent)
End If
End Sub
Thanks all especially Jay
my old code just as you suggested is:
at least the next time it will wait for me here ;)
private void cmdSaveToExcel_Click(object sender, EventArgs e)
{
saveFileDialog1.Filter = "Excel (*.xls)|*.xls";
if (saveFileDialog1.ShowDialog() == DialogResult.OK)
{
txtPath.Text = saveFileDialog1.FileName;
}
// create the DataGrid and perform the databinding
System.Web.UI.WebControls.DataGrid grid = new System.Web.UI.WebControls.DataGrid();
grid.HeaderStyle.Font.Bold = true;
if (connDBs != null && rtxtCode.Text != "")
{
DataTable dt;
dt = connDBs.userQuery(rtxtCode.Text); // getting a table with one column of the databases names
//grdData.DataSource = dt;
grid.DataSource = dt;
// grid.DataMember = data.Stats.TableName;
grid.DataBind();
// render the DataGrid control to a file
using (StreamWriter sw = new StreamWriter(txtPath.Text))
{
using (HtmlTextWriter hw = new HtmlTextWriter(sw))
{
grid.RenderControl(hw);
}
}
MessageBox.Show("The excel file was created successfully");
}
else
{
MessageBox.Show("Missing connection or query");
}
}
You need to convert your datatable into a ADO recordset, and then you can use the Range object's CopyFromRecordset method. See http://www.codeproject.com/KB/database/DataTableToRecordset.aspx

Categories