I'm looking for some advice. I'm building on an additional feature to a C# project that someone else wrote. The solution of the project consists of an MVC web application, with a few class libraries.
What I'm editing is the sales reporting function. In the original build, a summary of the sales reports were generated on the web application. When the user generates the sales report, a Reporting class is called in one of the C# class libraries. I'm trying to make the sales reports downloadable in an excel file when the user selects a radio button.
Here is a snippet of code from the Reporting class:
public AdminSalesReport GetCompleteAdminSalesReport(AdminSalesReportRequest reportRequest)
{
AdminSalesReport report = new AdminSalesReport();
string dateRange = null;
List<ProductSale> productSales = GetFilteredListOfAdminProductSales(reportRequest, out dateRange);
report.DateRange = dateRange;
if (titleSales.Count > 0)
{
report.HasData = true;
report.Total = GetTotalAdminSales(productSales);
if (reportRequest.Type == AdminSalesReportRequest.AdminSalesReportType.Complete)
{
report.ProductSales = GetAdminProductSales(productSales);
report.CustomerSales = GetAdminCustomerSales(productSales);
report.ManufacturerSales = GetAdminManufacturerSales(productSales);
if (reportRequest.Download)
{
FileResult ExcelDownload = GetExcelDownload(productSales);
}
}
}
return report;
}
So as you can see, if reportRequest.Download == true, the class should start up the process of creating the excel file. All the GetAdminSales functions do it use linq queries to sort out the sales if they are being displayed on the webpage.
So I have added this along with the GetAdminSales functions:
private FileResult GetExcelDownload(List<TitleSale> titleSales)
{
CustomisedSalesReport CustSalesRep = new CustomisedSalesReport();
Stream SalesReport = CustSalesRep.GenerateCustomisedSalesStream(productSales);
return new FileStreamResult(SalesReport, "application/ms-excel")
{
FileDownloadName = "SalesReport" + DateTime.Now.ToString("MMMM d, yyy") + ".xls"
};
}
and to format the excel sheet, I'm using the NPOI library, and my formatter class is laid out like so:
public class CustomisedSalesReport
{
public Stream GenerateCustomisedSalesStream(List<ProductSale> productSales)
{
return GenerateCustomisedSalesFile(productSales);
}
private Stream GenerateCustomisedSalesFile(List<ProductSale> productSales)
{
MemoryStream ms = new MemoryStream();
HSSFWorkbook templateWorkbook = new HSSFWorkbook();
HSSFSheet sheet = templateWorkbook.CreateSheet("Sales Report");
HSSFRow dataRow = sheet.CreateRow(0);
HSSFCell cell = dataRow.CreateCell(0);
cell = dataRow.CreateCell(0);
cell.SetCellValue(DateTime.Now.ToString("MMMM yyyy") + " Sales Report");
dataRow = sheet.CreateRow(2);
string[] colHeaders = new string[] {
"Product Code",
"Product Name",
"Qty Sold",
"Earnings",
};
int colPosition = 0;
foreach (string colHeader in colHeaders)
{
cell = dataRow.CreateCell(colPosition++);
cell.SetCellValue(colHeader);
}
int row = 4;
var adminTotalSales = GetAdminProductSales(productSales);
foreach (SummaryAdminProductSale t in adminTotalSales)
{
dataRow = sheet.CreateRow(row++);
colPosition = 0;
cell = dataRow.CreateCell(colPosition++);
cell.SetCellValue(t.ProductCode);
cell = dataRow.CreateCell(colPosition++);
cell.SetCellValue(t.ProductName);
cell = dataRow.CreateCell(colPosition++);
cell.SetCellValue(t.QtySold);
cell = dataRow.CreateCell(colPosition++);
cell.SetCellValue(t.Total.ToString("0.00"));
}
}
templateWorkbook.Write(ms);
ms.Position = 0;
return ms;
}
Again like before, the GetAdminSales (GetAdminProductSales, etc) are contained in the bottom of the class, and are just linq queries to gather the data.
So when I run this, I don't get any obvious errors. The summary sales report appears on screen as normal but no excel document downloads. What I have done, which may be putting this off is in my class library I have referened the System.Web.Mvc dll in order to download the file (I have not done it any other way before - and after reading up on the net I got the impression I could use it in a class library).
When I debug through the code to get a closer picture of what's going on, everything seems to be working ok, all the right data is being captured but I found that from the very start - the MemoryStream ms = new Memory Stream declaration line in my formatter class shows up this (very hidden mind you) :
ReadTimeout '((System.IO.Stream)(ms)).ReadTimeout'
threw an exception of type
'System.InvalidOperationException' int
{System.InvalidOperationException}
+{"Timeouts are not supported on this stream."} System.SystemException
{System.InvalidOperationException}
I get the same for 'WriteTimeout'...
Apologies for the long windedness of the explaination. I'd appreciate it if anyone could point me in the right direction, either to solve my current issue, or an alternative way of making this work.
Without getting bogged down in the details, the obvious error is that in GenerateCustomisedSalesFile you create a MemoryStream ms, do nothing with it, then return it.
Related
Background: I'm trying to write a program to insert an image into a cell of a spreadsheet. LibreOffice recently changed how this is done, and all the samples I could find use the old method which no longer works.
Technically I know that you can't "insert" an image into a cell and that such an image is an overlay on a DrawPage that sits on top of the spreadsheet to "decorate" it.
One of the first steps in doing this (the new way) is to create an XGraphic object which contains the image. The process is to create an XGraphicProvider and call it with MediaProperties that specify the image file URL to be loaded. I have a program that is supposed to do this but the resulting XGraphic is null. The LO SDK gives pretty much no information when you do something wrong; it just doesn't work.
Here is the code I have, with all the headers removed:
// addpic
// add picture to spreadsheet - debug version
class OpenOfficeApp {
[STAThread]
static void Main(string[] args) {
bool lreadonly;
string pqfile;
string pqURL;
string pqpic;
pqfile = "file:///D:/Documents/NSexeye/ODS%20File%20Access/"+
"addpix/addpic.ods";
pqpic = "addpic2";
pqURL = pqpic+".jpg";
lreadonly = false;
Console.WriteLine("Using: "+pqfile);
// get the desktop
XComponentContext XCC = uno.util.Bootstrap.bootstrap();
XMultiComponentFactory XMCF =
(XMultiComponentFactory)XCC.getServiceManager();
XMultiServiceFactory XMSF = (XMultiServiceFactory)XCC.getServiceManager();
XComponentLoader XCL =
(XComponentLoader)XMSF.createInstance("com.sun.star.frame.Desktop");
// open the spreadsheet
PropertyValue[] pPV = new PropertyValue[2];
pPV[0] = new PropertyValue();
pPV[0].Name = "Hidden";
pPV[0].Value = new uno.Any(true);
pPV[1] = new PropertyValue();
pPV[1].Name = "ReadOnly";
if (lreadonly) pPV[1].Value = new uno.Any(true);
else pPV[1].Value = new uno.Any(false);
XComponent XCo = XCL.loadComponentFromURL(pqfile,"_blank",0,pPV);
// create graphic object containing image
object oGP = XMCF.createInstanceWithContext(
"com.sun.star.graphic.GraphicProvider",XCC);
if (oGP == null) {
Console.WriteLine("oGP is null. Aborting.");
return;
}
XGraphicProvider XGP = (XGraphicProvider)oGP;
if (XGP == null) {
Console.WriteLine("XGP is null. Aborting.");
return;
}
pPV = new PropertyValue[1];
pPV[0] = new PropertyValue();
pPV[0].Name = "URL";
pPV[0].Value = new uno.Any(pqURL);
Console.WriteLine("Creating XGraphic containing "+pqURL);
XGraphic XG = XGP.queryGraphic(pPV);
// *** XG is null here
if (XG == null) {
Console.WriteLine("XG is null. Aborting.");
return;
}
// ... lots of stuff to be added here
// save and close the spreadsheet
XModifiable XM = (XModifiable)XCo;
XM.setModified(true);
XStorable XSt = (XStorable)XCo;
XSt.store();
XCloseable XCl = (XCloseable)XCo;
XCl.close(true);
// terminate LibreOffice
// *** I want this to not terminate it if something else is open
XDesktop XD = (XDesktop)XCL;
if (XD != null) XD.terminate();
}
}
I get a null for the XGraphic, in the place indicated in the comments. I don't know if the call to create it is failing, or if one of the earlier steps of the process are incorrect.
My goal here, in addition to getting my program working, is to create a sample program showing how to add an image to a Calc spreadsheet cell, and to manipulate such images. There are a fair number of people asking questions about this and none of the examples I've found will work. I think a good working sample will be of value.
I've spent a lot of time searching for information and code samples for this, with nothing that helps. I've tried to find ways to verify the validity of the XGraphicProvider interface with no luck. I've run out of things to try.
I'm hoping someone who knows about the LibreOffice SDK can take a look and maybe see what I'm doing wrong.
Update: I figured out what I was doing wrong: I was passing a bare filename in the "URL" property to XGraphicProvider. It has to be the same format (starting with "file:///") as the spreadsheet's file name specification.
Now I'm stuck with another property problem. The XGraphic has to be specified as a parameter to the GraphicObjectShape's Graphic property, but the setPropertyValue() function requires that it be a uno.Any type. I can't figure out how to specify an interface name like XGraphic as a uno.Any.
Here is the piece of code that won't compile, complaining that it can't convert an XGraphic to a uno.Any, in the first setPropertyValue call:
// set image XGraphic
XPropertySet XPS = (XPropertySet)XS;
XPS.setPropertyValue("Graphic",XG);
XPS.setPropertyValue("Name",new uno.Any(pqpic));
XG is an XGraphic type. Using "new uno.Any(XG)" doesn't work either, giving a similar compiler error.
After trying unsuccessfully for a few hours to get the latest LO SDK up and running, let me offer some untested ideas.
First of all, here is some working Basic code, no doubt similar to what you're translating from. The important line is oShape.Graphic = oProvider.queryGraphic(Props()).
oDoc = ThisComponent
oSheet = oDoc.CurrentController.ActiveSheet
pqURL = "file:///C:/Users/JimK/Desktop/addpic.jpg"
oProvider = createUnoService("com.sun.star.graphic.GraphicProvider")
oShape = oDoc.createInstance("com.sun.star.drawing.GraphicObjectShape")
Dim Props(0) as new com.sun.star.beans.PropertyValue
Props(0).Name= "URL"
Props(0).Value = pqURL
oShape.Graphic = oProvider.queryGraphic(Props())
oCell = oSheet.getCellByPosition(5,5)
oShape.Name = oCell.AbsoluteName + "##" + Props(0).Value
oShape.Anchor = oCell
oSheet.DrawPage.add(oShape)
'Resize
w = oShape.Graphic.Size.Width
h = oShape.Graphic.Size.Height
wcl = oCell.Size.Width
hcl = oCell.Size.Height
If w<>0 and h<>0 then
oCell.String=""
Dim Size as new com.sun.star.awt.Size
Size.Width = wcl
Size.Height = h*wcl/w
If Size.Height > hcl then
Size.Width = hcl*w/h
Size.Height = hcl
Endif
oShape.setSize(Size)
oShape.setPosition(oCell.Position)
erase oShape
Else
oShape.dispose()
Endif
Now, how to translate this to C#? It looks like you may need to explicitly specify the type. In the SDK example, there are calls like this.
xFieldProp.setPropertyValue(
"Orientation",
new uno.Any(
typeof (unoidl.com.sun.star.sheet.DataPilotFieldOrientation),
unoidl.com.sun.star.sheet.DataPilotFieldOrientation.DATA ) );
So in your case, something like this:
XPS.setPropertyValue(
"Graphic"
new uno.Any(
typeof(unoidl.com.sun.star.graphic.XGraphic),
XG));
Alternatively, follow the suggestion here: set GraphicURL, which should load the image and set Graphic for you.
So I finally was able to create a XML and change it as I want but now I needed to add the contents of a DataGridView to it. I thought that's quite easy as I saw the options to place it into a DataSet and use XmlWrite, but that was a mistake of me. Note that I'm still trying to learn C# so probably I make a silly mistake here. It is still not working maybe someone is willing to point me out what I am doing wrong?
I actually have two issues with this:
It ForEach loop doesn't get the existing column names
It doesn't add the table and its contents to the XML file
private void CreateClientFile()
{
string filename;
filename = Company + "_" + SiteName + ".xml";
XmlDocument doc = new XmlDocument();
XmlElement root = doc.CreateElement("CompanyProfile");
doc.AppendChild(root);
//Save document on Harddisk
doc.Save(#"C:\Users\NLRAGIL\Documents\10 - VibroManager\" + filename);
//Need to save first and than load again????
//Load document into program
doc.Load(#"C:\Users\NLRAGIL\Documents\10 - VibroManager\" + filename);
XmlNode main = doc.SelectSingleNode("CompanyProfile");
//Create Company name element
XmlElement companyname = doc.CreateElement("CompanyName");
companyname.InnerText = CompanyName;
main.AppendChild(companyname);
//Create sitename element
XmlElement sitename = doc.CreateElement("Sitename");
sitename.InnerText = SiteName;
main.AppendChild(sitename);
//Create IMO element
XmlElement imo = doc.CreateElement("IMO");
imo.InnerText = IMO;
main.AppendChild(imo);
DataTable dt = new DataTable();
for (int i = 0; i < dataGridView1.Columns.Count; i++)
{
dt.Columns.Add("column" + i.ToString());
}
foreach (DataGridViewRow row in dataGridView1.Rows)
{
DataRow dr = dt.NewRow();
for (int j = 0; j < dataGridView1.Columns.Count; j++)
{
dr["column" + j.ToString()] = row.Cells[j].Value ;
}
dt.Rows.Add(dr);
}
//Create DataSet and add the datatable
DataSet ds = new DataSet();
ds.Tables.Add(dt);
//Give the file name for where to write to.
ds.WriteXml(#"C:\Users\NLRAGIL\Documents\10 - VibroManager\" + filename);
//Show example for debugging
doc.Save(#"C:\Users\NLRAGIL\Documents\10 - VibroManager\" + filename);
System.Console.WriteLine(doc.InnerXml);
}
EXTRA CLARIFICATION:
The form I have looks as below:
The Textbox in the groupbox "Client Information" I'm able to save in a XML file. By altering the value of the numeric control I can express how much machine the particular client has. And the DataGridView gets more or less rows. But the information from the DataGridView I'm unable to append to the created XML file.
So the information from "Machine Name", "Serial No" etc I can't add to the XML file.
This is what I wanted to do, so later on in the program I can add certain measurements of each machine to it and store also in the same file.
But whatever I do my XML file looks like this:
I hope I explained it better now sorry for the confusion
Your question is Add the contents of a DataGridView to an existing XML file and you say your first issue is that your ForNext loop is not giving you the column names and your second issue is that the code fails to serialize the record to an XML file on disk. These two goals can be simplified by using Data Binding. This decouples your data from the view, making it easier to process. I would like to give you some insight if you wanted to try it out using the CompanyProfile in your code.
First, a CompanyProfile class declares the intended public properties:
public class CompanyProfile
{
public string CompanyName { get; set; }
public string SiteName { get; set; }
public string IMO { get; set; } = "Some Value";
}
Next, in your MainForm class a BindingList<CompanyProfile> is declared and attached to the DataGridView like this:
BindingList<CompanyProfile> DataSource = new BindingList<CompanyProfile>();
protected override void OnHandleCreated(EventArgs e)
{
base.OnHandleCreated(e);
if(!DesignMode)
{
// Attach the data source to the view. Now changes to source records refresh in the view.
dataGridView1.DataSource = this.DataSource;
// Adding one or more records will generate the columns.
DataSource.Add(new CompanyProfile { CompanyName = "Linear Technology", SiteName = "Colorado Design Center"});
DataSource.Add(new CompanyProfile { CompanyName = "Analog Devices", SiteName = "1-1-2"});
// Use string indexer to get a column
dataGridView1.Columns[nameof(CompanyProfile.CompanyName)].AutoSizeMode = dataGridViewAutoSizeColumnMode.Fill;
dataGridView1.Columns[nameof(CompanyProfile.SiteName)].AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill;
DataGridView1.AllowUserToAddRows = false;
}
}
The resulting DataGridView now looks like this:
This method makes a single file from a CompanyProfile record using XmlSerializer (but this is just one approach - and you could also serialize the entire list at one time if you choose).
private void CreateClientFile(CompanyProfile companyProfile, string fileName)
{
System.Xml.Serialization.XmlSerializer x = new System.Xml.Serialization.XmlSerializer(typeof(CompanyProfile));
using (var writer = new StreamWriter(fileName))
{
x.Serialize(writer, companyProfile);
}
// Open the file to view the result
Process.Start("notepad.exe", fileName);
}
Now, iterate a ForNext loop on the DataSource not the DataGridView. You no longer need to worry about columns because you have the bound properties instead.
private void btnSerialize_Click(object sender, EventArgs e)
{
var appData = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"datagridview_to_xml");
Directory.CreateDirectory(appData);
// Iterate the datasource list, not the DataGridView.
foreach (CompanyProfile companyProfile in DataSource)
{
CreateClientFile(
companyProfile,
fileName: Path.Combine(appData,
$"{companyProfile.CompanyName}_{companyProfile.SiteName}.xml")
);
}
}
Clicking the [Serialize] button reveals the two files.
I'm writing a C# app with some Google Spreadsheets integration. I'm in a situation where I have some data in a worksheet that needs to be moved into a different spreadsheet. This worksheet contains a huge amount of data, so I want to avoid iterating through its contents.
The API guide gives an example of how to create a new worksheet within a spreadsheet. I modified it to add an existing worksheet to the spreadsheet:
using System;
using Google.GData.Client;
using Google.GData.Spreadsheets;
namespace MySpreadsheetIntegration
{
class Program
{
static void Main(string[] args)
{
SpreadsheetsService service = new SpreadsheetsService("MySpreadsheetIntegration-v1");
SpreadsheetEntry destinationSpreadsheet = fetchGoogleSpreadSheetEntry(service, "some_title");
SpreadsheetEntry originSpreadsheet = fetchGoogleSpreadSheetEntry(service, "some_other_title");
// Create a local representation of the new worksheet.
WorksheetEntry originWorksheet = fetchGoogleWorksheet( originSpreadsheet, "some_worksheet_title" );
// Send the local representation of the worksheet to the API for
// creation. The URL to use here is the worksheet feed URL of our
// spreadsheet.
WorksheetFeed wsFeed = destinationSpreadsheet.Worksheets;
service.Insert(wsFeed, originWorksheet);
}
}
}
For clarity, the above code attempts to take the "some_worksheet_title" worksheet in the "some_other_title" spreadsheet, and put it into the "some_title" spreadsheet. Below are the functions referenced in the above code.
public static WorksheetEntry fetchGoogleWorksheet( SpreadsheetEntry spreadsheet, string worksheet_title )
{
WorksheetFeed wsFeed = spreadsheet.Worksheets;
WorksheetEntry worksheet = null;
foreach (WorksheetEntry entry in wsFeed.Entries)
{
worksheet = entry;
if (entry.Title.Text == worksheet_title)
{
Console.WriteLine(DateTime.Now.ToString("HH:mm") + ": Worksheet found on Google Drive.");
break;
}
}
if (worksheet.Title.Text != worksheet_title)
{
return null;
}
return worksheet;
}
public static SpreadsheetEntry fetchGoogleSpreadSheetEntry( SpreadsheetsService service, string spreadsheet_title )
{
Console.WriteLine(DateTime.Now.ToString("HH:mm") + ": Looking for spreadsheet on Google Drive.");
SpreadsheetQuery query = new SpreadsheetQuery();
SpreadsheetFeed feed;
feed = service.Query(query);
SpreadsheetEntry spreadsheet = null;
// Iterate through all of the spreadsheets returned
foreach (SpreadsheetEntry entry in feed.Entries)
{
// Print the title of this spreadsheet to the screen
spreadsheet = entry;
if (entry.Title.Text == spreadsheet_title)
{
Console.WriteLine(DateTime.Now.ToString("HH:mm") + ": Spreadsheet found on Google Drive.");
Console.WriteLine(DateTime.Now.ToString("HH:mm") + ": Looking for worksheet in spreadsheet.");
break;
}
}
if (spreadsheet.Title.Text != spreadsheet_title)
{
return null;
}
return spreadsheet;
}
I expected to be able to fetch to worksheet I want to add to the spreadsheet, and just add it to the spreadsheet. It does not work. The above code creates a (correctly titled) worksheet in the destination spreadsheet, but does not transfer any of the content of the worksheet.
Is there any way to have it transfer the content correctly?
After trying a few different ways of doing this, the most reliable way turned out to be Google Apps Scripting. In general terms, my solution involves a Google Apps script that is being called by my C# application via the execution API. Below are some code samples demonstrating how all of this works together.
So here's the Google Apps script that moves content from one worksheet to another:
function copyWorksheet( destinationSpreadsheetId, destinationWorksheetTitle, originSpreadsheetId, originWorksheetTitle ) {
// Spreadsheet where new data will go:
var dss = SpreadsheetApp.openById(destinationSpreadsheetId);
// Spreadsheet where new data is coming from:
var oss = SpreadsheetApp.openById(originSpreadsheetId);
// Worksheet containing new data:
var dataOriginWorksheet = oss.getSheetByName(originWorksheetTitle);
// Worksheet whose data will be 'overwritten':
var expiredWorksheet = dss.getSheetByName(destinationWorksheetTitle);
// If a spreadsheet only has one worksheet, deleting that worksheet causes an error.
// Thus we need to know whether the expired worksheet is the only worksheet in it's parent spreadsheet.
var expiredWorksheetIsAlone = dss.getNumSheets() == 1 && expiredWorksheet != null;
// Delete the expired worksheet if there are other worksheets:
if (expiredWorksheet != null && !expiredWorksheetIsAlone)
dss.deleteSheet(expiredWorksheet);
// Otherwise, rename it to something guaranteed not to clash with the new sheet's title:
if(expiredWorksheetIsAlone)
expiredWorksheet.setName(dataOriginWorksheet.getName() + destinationWorksheetTitle);
// Copy the new data into it's rightful place, and give it it's rightful name.
dataOriginWorksheet.copyTo(dss).setName(destinationWorksheetTitle);
// Since there are now definitely 2 worksheets, it's safe to delete the expired one.
if(expiredWorksheetIsAlone)
dss.deleteSheet(expiredWorksheet);
// Make sure our changes are applied ASAP:
SpreadsheetApp.flush();
return "finished";
}
This is a severely stripped down version of the code I ended up using, which is why there are two spreadsheet ID fields. This means that it does not matter whether or not the the two worksheets are in the same spreadsheet.
The C# part of the solution looks like this:
// We need these for the method below
using Google.Apis.Script.v1;
using Google.Apis.Script.v1.Data;
...
public static bool copyWorksheet(ScriptService scriptService, string destinationSpreadsheetId, string destinationWorksheetTitle, string originSpreadsheetId, string originWorksheetTitle)
{
// You can get the script ID by going to the script in the
// Google Apps Scripts Editor > Publish > Deploy as API executable... > API ID
string scriptId = "your-apps-script-id";
ExecutionRequest request = new ExecutionRequest();
request.Function = "copyWorksheet";
IList<object> parameters = new List<object>();
parameters.Add(destinationSpreadsheetId);
parameters.Add(destinationWorksheetTitle);
parameters.Add(originSpreadsheetId);
parameters.Add(originWorksheetTitle);
request.Parameters = parameters;
ScriptsResource.RunRequest runReq = scriptService.Scripts.Run(request, scriptId);
try
{
Operation op = runReq.Execute();
if (op.Error != null)
{
Console.WriteLine(DateTime.Now.ToString("HH:mm:ss") + " The Apps script encountered an error");
// The API executed, but the script returned an error.
IDictionary<string, object> error = op.Error.Details[0];
Console.WriteLine( "Script error message: {0}", error["errorMessage"]);
if ( error.ContainsKey("scriptStackTraceElements") )
{
// There may not be a stacktrace if the script didn't
// start executing.
Console.WriteLine("Script error stacktrace:");
Newtonsoft.Json.Linq.JArray st = (Newtonsoft.Json.Linq.JArray)error["scriptStackTraceElements"];
foreach (var trace in st)
{
Console.WriteLine(
"\t{0}: {1}",
trace["function"],
trace["lineNumber"]);
}
}
}
else
{
// The result provided by the API needs to be cast into
// the correct type, based upon what types the Apps
// Script function returns. Here, the function returns
// an Apps Script Object with String keys and values.
// It is most convenient to cast the return value as a JSON
// JObject (folderSet).
return true;
}
}
catch (Google.GoogleApiException e)
{
// The API encountered a problem before the script
// started executing.
Console.WriteLine(DateTime.Now.ToString("HH:mm:ss") + " Could not call Apps Script");
}
return false;
}
...
The above 2 pieces of code, when used together, solved the problem perfectly. Execution time does not differ greatly between different volumes of data, and there has been no data corruption with transfers.
I am creating an Excel workbook in code behind and save it as both XLSX and PDF. I use a template workbook for this that has formatting and formulae to be evaluated after the generating is done. When I open the Excel file, the formulae evaluate only when I set ForceFormulaRecalculation to true. When I do the same with the PDF file, I get #VALUE! where the results should be. My relevant code:
ReportGenerator generator = new ReportGenerator();
List<Activity> activities = GetActivitiesForItemCollection(items);
generator.CreateWorkbook(templatePath);
generator.Year = int.Parse(year);
generator.Month = int.Parse(month);
activities = generator.SortActivitiesByDateTime(activities);
activities = generator.GenerateBreaksForProject(activities);
bool isExternalReport = false;
if (project == "Intern")
isExternalReport = true;
generator.GenerateReports(activities, isExternalReport);
if (pdf && !xlsx)
generator.SaveReportToList(OutputFileType.PDF, generator.AssembleFileName(UserProperties.FullName, project, month, year, OutputFileType.PDF));
else if (xlsx && !pdf)
generator.SaveReportToList(OutputFileType.XLSX, generator.AssembleFileName(UserProperties.FullName, project, month, year, OutputFileType.XLSX));
else
{
generator.SaveReportToList(OutputFileType.XLSX, generator.AssembleFileName(UserProperties.FullName, project, month, year, OutputFileType.XLSX));
generator.SaveReportToList(OutputFileType.PDF, generator.AssembleFileName(UserProperties.FullName, project, month, year, OutputFileType.PDF));
}
Here is my code where I do the evaluation:
public void SaveReportToList(OutputFileType outputType, string filename)
{
string siteUrl = SPContext.Current.Web.Url;
SPSecurity.RunWithElevatedPrivileges(delegate()
{
using (MemoryStream mStream = new MemoryStream())
{
sheet.PrintSetup.Landscape = true;
sheet.PrintSetup.FitWidth = 1;
sheet.PrintSetup.FitHeight = 1;
XSSFFormulaEvaluator.EvaluateAllFormulaCells(workbook);
workbook.Write(mStream);
using (MemoryStream crutchStream = new MemoryStream(mStream.ToArray()))
{
using (SPSite spsite = new SPSite(siteUrl))
{
using (SPWeb spweb = spsite.OpenWeb())
{
....
I have also tried it this way:
private void ForceCalculateSheet()
{
XSSFFormulaEvaluator helper = (XSSFFormulaEvaluator)workbook.GetCreationHelper().CreateFormulaEvaluator();
for(int i = 0; i<sheet.LastRowNum; i++)
{
XSSFRow row = (XSSFRow)sheet.GetRow(i);
for(int j = 0; j< row.LastCellNum; j++)
{
XSSFCell cell = (XSSFCell)row.GetCell(j);
if(cell != null && cell.CellType == CellType.Formula)
{
helper.EvaluateFormulaCell(cell);
}
}
}
}
Here are a couple of formulae I try to evaluate (some of the cell values are 24-hour time values):
=IF(D37-C37-E37>0;D37-C37-E37;0)
=INT(J39/60)
=J39-C39*60
The weird thing is, when I trace the evaluation in Excel, it goes through until the last step. Then suddenly the value becomes #VALUE!.
Anybody got an idea what's going on here?
Just to be careful, I updated NPOI to it's latest .NET Version (2.1.3.1). This still didn't fix the problem.
When I click a cell that's in the formula, change its value, then confirm it with pressing Enter, the formula evaluates correctly. Meaning there shouldn't be anything wrong with the formulae themselves.
I solved it by manually doing the calculations. This isn't my long-term solution as users should be able to insert their own calculations, but it's the only solution I can come up with right now besides using another Excel framework (bad planning ;)).
I know this question exists, because it's mine and I put up 500 bounty points on it:
Exporting C# report to Excel when there are more than 5K lines
The answer got me over the hump (to some degree) but we're sort of at the point where we just accept that abnormally large datasets just can't be exported via our ASP front end, so we ship those requests off to our SQL Server DBs, who then run the appropriate stored procedures and copy/paste to Excel spreadsheets.
My question here is; can someone definitively answer whether or not it's absolutely impossible to export a large dataset to an Excel spreadsheet via a ASP front end? Once a particular report hits about 8K records or something, it just can't seem to be done. I'm just trying to determine whether any other potential tweak can be made, or if that much data is just more than ASP can handle?
Well... since I've streamed gigabytes of data directly from ASP.NET, I'm pretty sure you're doing something wrong. Try to isolate the problem first - is it in putting the data into the session, is it request / response limits, is it request timeouts? Figure out where the problem is, and then go ahead and solve it! :)
In general terms, there's no reason why you should put the data in a DataSet first. Instead, use a SqlDataReader and write the data to output in chunks. This way you'll avoid having the whole data set in memory; the same way, you can just directly write to the output stream, without buffering the generated HTML in memory. Why do you keep data in Session? Wouldn't it be better to just hold the parameters necessary to retrieve it from the DB as needed, using the DataReader?
If you're having trouble with timeouts, periodical Flushes help. This also helps reduce the memory footprint on the ASP.NET side.
Saving the output data to a file on the server first also helps, and it allows you to wire up partial file downloads too - just make sure you actually have enough space on the drive.
EDIT:
Ok, so you've got an SqlCommand. Instead of using it in a SqlDataAdapter, you can do something like this (cmd being your SqlCommand instance):
HtmlTextWriter wr = new HtmlTextWriter(Response.Output);
using (var rdr = cmd.ExecuteReader())
{
int index = 0;
wr.WriteBeginTag("table");
wr.WriteLine("<tr><td>Column 1</td><td>Column 2</td></tr>");
while (rdr.Read())
{
wr.WriteBeginTag("tr");
wr.WriteBeginTag("td");
wr.Write(rdr["Column1"]);
wr.WriteEndTag("td");
wr.WriteBeginTag("td");
wr.Write(rdr["Column2"]);
wr.WriteEndTag("td");
wr.WriteEndTag("tr");
if (index++ % 1000 == 0) Response.Flush();
}
wr.WriteEndTag("table");
}
I have not tested it, so it might need some tweaking, but the idea should be pretty obvious.
It is possible to do this as I have actually just finished some code specifically to do this as part of a reporting project that I am working on where we have in-excess of 20K records that need to be pulled back and exported into excel.
I will pull out the code and stick it on github for you to look at.
I am actually using NPOI's excel processing package and then using my custom code I am able to process any List of classes dynamically into a dataset and then dump it into the worksheets.
I need to tidy up the code for you but I should have something ready for you this evening.
This code will work for both desktop and web apps.
To give you an idea my code has been able to process a dataset of over 30K relatively quickly. I have to resolve an issue with datasets over the limit of 65536 records first before it is ready for you.
The nice thing with this solution means it doesn't rely on excel being installed on the machine hosting the solution.
EDIT
I have loaded a project onto github here:
https://github.com/JellyMaster/ExcelHelper
but here is the main bit that does all the excel processing:
public static MemoryStream CreateExcelSheet(DataSet dataToProcess)
{
MemoryStream stream = new MemoryStream();
if (dataToProcess != null)
{
var excelworkbook = new HSSFWorkbook();
foreach (DataTable table in dataToProcess.Tables)
{
var worksheet = excelworkbook.CreateSheet();
var headerRow = worksheet.CreateRow(0);
foreach (DataColumn column in table.Columns)
{
headerRow.CreateCell(table.Columns.IndexOf(column)).SetCellValue(column.ColumnName);
}
//freeze top panel.
worksheet.CreateFreezePane(0, 1, 0, 1);
int rowNumber = 1;
foreach (DataRow row in table.Rows)
{
var sheetRow = worksheet.CreateRow(rowNumber++);
foreach (DataColumn column in table.Columns)
{
sheetRow.CreateCell(table.Columns.IndexOf(column)).SetCellValue(row[column].ToString());
}
}
}
excelworkbook.Write(stream);
}
return stream;
}
public static DataSet CreateDataSetFromExcel(Stream streamToProcess, string fileExtentison = "xlsx")
{
DataSet model = new DataSet();
if (streamToProcess != null)
{
if (fileExtentison == "xlsx")
{
XSSFWorkbook workbook = new XSSFWorkbook(streamToProcess);
model = ProcessXLSX(workbook);
}
else
{
HSSFWorkbook workbook = new HSSFWorkbook(streamToProcess);
model = ProcessXLSX(workbook);
}
}
return model;
}
private static DataSet ProcessXLSX(HSSFWorkbook workbook)
{
DataSet model = new DataSet();
for (int index = 0; index < workbook.NumberOfSheets; index++)
{
ISheet sheet = workbook.GetSheetAt(0);
if (sheet != null)
{
DataTable table = GenerateTableData(sheet);
model.Tables.Add(table);
}
}
return model;
}
private static DataTable GenerateTableData(ISheet sheet)
{
DataTable table = new DataTable(sheet.SheetName);
for (int rowIndex = 0; rowIndex <= sheet.LastRowNum; rowIndex++)
{
//we will assume the first row are the column names
IRow row = sheet.GetRow(rowIndex);
//a completely empty row of data so break out of the process.
if (row == null)
{
break;
}
if (rowIndex == 0)
{
for (int cellIndex = 0; cellIndex < row.LastCellNum; cellIndex++)
{
string value = row.GetCell(cellIndex).ToString();
if (string.IsNullOrEmpty(value))
{
break;
}
else
{
table.Columns.Add(new DataColumn(value));
}
}
}
else
{
//get the data and add to the collection
//now we know the number of columns to iterate through lets get the data and fill up the table.
DataRow datarow = table.NewRow();
object[] objectArray = new object[table.Columns.Count];
for (int columnIndex = 0; columnIndex < table.Columns.Count; columnIndex++)
{
try
{
ICell cell = row.GetCell(columnIndex);
if (cell != null)
{
objectArray[columnIndex] = cell.ToString();
}
else
{
objectArray[columnIndex] = string.Empty;
}
}
catch (Exception error)
{
Debug.WriteLine(error.Message);
Debug.WriteLine("Column Index" + columnIndex);
Debug.WriteLine("Row Index" + row.RowNum);
}
}
datarow.ItemArray = objectArray;
table.Rows.Add(datarow);
}
}
return table;
}
private static DataSet ProcessXLSX(XSSFWorkbook workbook)
{
DataSet model = new DataSet();
for (int index = 0; index < workbook.NumberOfSheets; index++)
{
ISheet sheet = workbook.GetSheetAt(index);
if (sheet != null)
{
DataTable table = GenerateTableData(sheet);
model.Tables.Add(table);
}
}
return model;
}
}
This does require the NPOI nuget package to be installed in your project.
Any questions give me a shout. The github project does a bit more but this is enough to get you going hopefully.