You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
709 lines
24 KiB
709 lines
24 KiB
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Reflection;
|
|
using System.Text;
|
|
using System.IO;
|
|
using System.Xml.Linq;
|
|
using System.Collections;
|
|
using System.Data;
|
|
|
|
namespace FastExcel
|
|
{
|
|
/// <summary>
|
|
/// Excel Worksheet
|
|
/// </summary>
|
|
public class Worksheet
|
|
{
|
|
/// <summary>
|
|
/// Collection of rows in this worksheet
|
|
/// </summary>
|
|
public IEnumerable<Row> Rows { get; set; }
|
|
|
|
/// <summary>
|
|
/// Heading Names
|
|
/// </summary>
|
|
public IEnumerable<string> Headings { get; set; }
|
|
|
|
/// <summary>
|
|
/// Index of this worksheet (Starts at 1)
|
|
/// </summary>
|
|
public int Index { get; internal set; }
|
|
|
|
/// <summary>
|
|
/// Name of this worksheet
|
|
/// </summary>
|
|
public string Name { get; set; }
|
|
/// <summary>
|
|
/// Is there any existing heading rows
|
|
/// </summary>
|
|
public int ExistingHeadingRows { get; set; }
|
|
private int? InsertAfterIndex { get; set; }
|
|
|
|
/// <summary>
|
|
/// Template
|
|
/// </summary>
|
|
public bool Template { get; set; }
|
|
|
|
internal string Headers { get; set; }
|
|
internal string Footers { get; set; }
|
|
|
|
/// <summary>
|
|
/// Fast Excel
|
|
/// </summary>
|
|
public FastExcel FastExcel { get; private set; }
|
|
|
|
internal string FileName
|
|
{
|
|
get
|
|
{
|
|
return GetFileName(Index);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get the internal file name of this worksheet
|
|
/// </summary>
|
|
internal static string GetFileName(int index)
|
|
{
|
|
return string.Format("xl/worksheets/sheet{0}.xml", index);
|
|
}
|
|
|
|
private const string DEFAULT_HEADERS = "<?xml version=\"1.0\" encoding=\"UTF-8\" ?><worksheet xmlns=\"http://schemas.openxmlformats.org/spreadsheetml/2006/main\"><sheetData>";
|
|
private const string DEFAULT_FOOTERS = "</sheetData></worksheet>";
|
|
|
|
/// <summary>
|
|
/// Constructor
|
|
/// </summary>
|
|
public Worksheet() { }
|
|
|
|
/// <summary>
|
|
/// Constructor
|
|
/// </summary>
|
|
public Worksheet(FastExcel fastExcel)
|
|
{
|
|
FastExcel = fastExcel;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Populate rows
|
|
/// </summary>
|
|
public void PopulateRows<T>(IEnumerable<T> rows, int existingHeadingRows = 0, bool usePropertiesAsHeadings = false)
|
|
{
|
|
if ((rows.FirstOrDefault() as IEnumerable<object>) == null)
|
|
{
|
|
PopulateRowsFromObjects(rows, existingHeadingRows, usePropertiesAsHeadings);
|
|
}
|
|
else
|
|
{
|
|
PopulateRowsFromIEnumerable(rows as IEnumerable<IEnumerable<object>>, existingHeadingRows);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Populate rows from datatable
|
|
/// </summary>
|
|
public void PopulateRowsFromDataTable(DataTable table, int existingHeadingRows = 0)
|
|
{
|
|
var rowNumber = existingHeadingRows + 1;
|
|
var headingColumnNumber = 1;
|
|
|
|
var headingCells = (from c in table.Columns.OfType<DataColumn>() select new Cell(headingColumnNumber++, c.ColumnName)).ToArray();
|
|
var headingRow = new Row(rowNumber++, headingCells);
|
|
var newRows = new List<Row>() { headingRow };
|
|
|
|
for (var rowIndex = 0; rowIndex < table.Rows.Count; rowIndex++)
|
|
{
|
|
var cells = new List<Cell>();
|
|
|
|
for (var columnIndex = 0; columnIndex < table.Columns.Count; columnIndex++)
|
|
{
|
|
object value = table.Rows[rowIndex].ItemArray[columnIndex];
|
|
if (value == null) continue;
|
|
|
|
var cell = new Cell(columnIndex + 1, value);
|
|
cells.Add(cell);
|
|
}
|
|
|
|
var row = new Row(rowNumber++, cells);
|
|
newRows.Add(row);
|
|
}
|
|
|
|
Rows = newRows;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get Header Column Name from [ExcelColumn(Name="column1")] or the property name
|
|
/// </summary>
|
|
private string GetHeaderName(PropertyInfo propertyInfo)
|
|
{
|
|
var descriptionAttribute = propertyInfo.GetCustomAttribute<ExcelColumnAttribute>();
|
|
if (descriptionAttribute != null && !string.IsNullOrWhiteSpace(descriptionAttribute.Name))
|
|
{
|
|
return descriptionAttribute.Name;
|
|
}
|
|
return propertyInfo.Name;
|
|
}
|
|
|
|
private void PopulateRowsFromObjects<T>(IEnumerable<T> rows, int existingHeadingRows = 0, bool usePropertiesAsHeadings = false)
|
|
{
|
|
int rowNumber = existingHeadingRows + 1;
|
|
|
|
// Get all properties
|
|
IEnumerable<PropertyInfo> properties = typeof(T).GetRuntimeProperties();
|
|
var newRows = new List<Row>();
|
|
|
|
if (usePropertiesAsHeadings)
|
|
{
|
|
Headings = properties.Select(GetHeaderName);
|
|
|
|
int headingColumnNumber = 1;
|
|
IEnumerable<Cell> headingCells = (from h in Headings
|
|
select new Cell(headingColumnNumber++, h)).ToArray();
|
|
|
|
Row headingRow = new Row(rowNumber++, headingCells);
|
|
|
|
newRows.Add(headingRow);
|
|
}
|
|
|
|
foreach (T rowObject in rows)
|
|
{
|
|
var cells = new List<Cell>();
|
|
|
|
int columnNumber = 1;
|
|
|
|
// Get value from each property
|
|
foreach (PropertyInfo propertyInfo in properties)
|
|
{
|
|
object value = propertyInfo.GetValue(rowObject, null);
|
|
if (value != null)
|
|
{
|
|
Cell cell = new Cell(columnNumber, value);
|
|
cells.Add(cell);
|
|
}
|
|
columnNumber++;
|
|
}
|
|
|
|
var row = new Row(rowNumber++, cells);
|
|
newRows.Add(row);
|
|
}
|
|
|
|
Rows = newRows;
|
|
}
|
|
|
|
private void PopulateRowsFromIEnumerable(IEnumerable<IEnumerable<object>> rows, int existingHeadingRows = 0)
|
|
{
|
|
int rowNumber = existingHeadingRows + 1;
|
|
|
|
var newRows = new List<Row>();
|
|
|
|
foreach (IEnumerable<object> rowOfObjects in rows)
|
|
{
|
|
var cells = new List<Cell>();
|
|
|
|
int columnNumber = 1;
|
|
|
|
foreach (object value in rowOfObjects)
|
|
{
|
|
if (value != null)
|
|
{
|
|
var cell = new Cell(columnNumber, value);
|
|
cells.Add(cell);
|
|
}
|
|
columnNumber++;
|
|
}
|
|
|
|
Row row = new Row(rowNumber++, cells);
|
|
newRows.Add(row);
|
|
}
|
|
|
|
Rows = newRows;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Add a row using a collection of value objects
|
|
/// </summary>
|
|
/// <param name="cellValues">Collection of objects</param>
|
|
public void AddRow(params object[] cellValues)
|
|
{
|
|
if (Rows == null)
|
|
{
|
|
Rows = new List<Row>();
|
|
}
|
|
|
|
var cells = new List<Cell>();
|
|
|
|
int columnNumber = 1;
|
|
foreach (object value in cellValues)
|
|
{
|
|
if (value != null)
|
|
{
|
|
var cell = new Cell(columnNumber++, value);
|
|
cells.Add(cell);
|
|
}
|
|
else
|
|
{
|
|
columnNumber++;
|
|
}
|
|
}
|
|
|
|
var row = new Row(Rows.Count() + 1, cells);
|
|
(Rows as List<Row>).Add(row);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Note: This method is slow
|
|
/// </summary>
|
|
public void AddValue(int rowNumber, int columnNumber, object value)
|
|
{
|
|
if (Rows == null)
|
|
{
|
|
Rows = new List<Row>();
|
|
}
|
|
|
|
Row row = (from r in Rows
|
|
where r.RowNumber == rowNumber
|
|
select r).FirstOrDefault();
|
|
Cell cell = null;
|
|
|
|
if (row == null)
|
|
{
|
|
cell = new Cell(columnNumber, value);
|
|
row = new Row(rowNumber, new List<Cell> { cell });
|
|
(Rows as List<Row>).Add(row);
|
|
}
|
|
|
|
if (cell == null)
|
|
{
|
|
cell = (from c in row.Cells
|
|
where c.ColumnNumber == columnNumber
|
|
select c).FirstOrDefault();
|
|
|
|
if (cell == null)
|
|
{
|
|
cell = new Cell(columnNumber, value);
|
|
(row.Cells as List<Cell>).Add(cell);
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
/// <summary>
|
|
/// Merges the parameter into the current DatSet object, the parameter takes precedence
|
|
/// </summary>
|
|
/// <param name="data">A DataSet to merge</param>
|
|
public void Merge(Worksheet data)
|
|
{
|
|
// Merge headings
|
|
if (Headings == null || !Headings.Any())
|
|
{
|
|
Headings = data.Headings;
|
|
}
|
|
|
|
// Merge rows
|
|
Rows = data.MergeRows(Rows);
|
|
// data.Rows = MergeRows(data.Rows);
|
|
|
|
}
|
|
|
|
private IEnumerable<Row> MergeRows(IEnumerable<Row> rows)
|
|
{
|
|
foreach (var row in Rows.Union(rows).GroupBy(r => r.RowNumber))
|
|
{
|
|
int count = row.Count();
|
|
if (count == 1)
|
|
{
|
|
yield return row.First();
|
|
}
|
|
else
|
|
{
|
|
row.First().Merge(row.Skip(1).First());
|
|
|
|
yield return row.First();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Does the file exist
|
|
/// </summary>
|
|
public bool Exists
|
|
{
|
|
get
|
|
{
|
|
return !string.IsNullOrEmpty(FileName);
|
|
}
|
|
}
|
|
|
|
internal void Read(int? sheetNumber = null, string sheetName = null, int existingHeadingRows = 0)
|
|
{
|
|
GetWorksheetProperties(FastExcel, sheetNumber, sheetName);
|
|
Read(existingHeadingRows);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Read the worksheet
|
|
/// </summary>
|
|
/// <param name="existingHeadingRows"></param>
|
|
public void Read(int existingHeadingRows = 0)
|
|
{
|
|
FastExcel.CheckFiles();
|
|
FastExcel.PrepareArchive();
|
|
|
|
ExistingHeadingRows = existingHeadingRows;
|
|
|
|
IEnumerable<Row> rows = null;
|
|
|
|
var headings = new List<string>();
|
|
using (Stream stream = FastExcel.Archive.GetEntry(FileName).Open())
|
|
{
|
|
var document = XDocument.Load(stream);
|
|
int skipRows = 0;
|
|
|
|
var rowElement = document.Descendants().FirstOrDefault(d => d.Name.LocalName == "row");
|
|
if (rowElement != null)
|
|
{
|
|
var possibleHeadingRow = new Row(rowElement, this);
|
|
if (ExistingHeadingRows == 1 && possibleHeadingRow.RowNumber == 1)
|
|
{
|
|
foreach (var headerCell in possibleHeadingRow.Cells)
|
|
{
|
|
headings.Add(headerCell.Value.ToString());
|
|
}
|
|
}
|
|
}
|
|
rows = GetRows(document.Descendants().Where(d => d.Name.LocalName == "row").Skip(skipRows));
|
|
}
|
|
|
|
Headings = headings;
|
|
Rows = rows;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns cells using provided range
|
|
/// </summary>
|
|
/// <param name="cellRange">Definition of range to use</param>
|
|
/// <returns></returns>
|
|
public IEnumerable<Cell> GetCellsInRange(CellRange cellRange)
|
|
{
|
|
IEnumerable rows;
|
|
|
|
if (cellRange.RowEnd.HasValue)
|
|
{
|
|
rows = (from row in Rows
|
|
where row.RowNumber >= cellRange.RowStart && row.RowNumber <= cellRange.RowEnd
|
|
select row);
|
|
}
|
|
else
|
|
{
|
|
rows = (from row in Rows
|
|
where row.RowNumber >= cellRange.RowStart
|
|
select row);
|
|
}
|
|
|
|
var rangeResult = new List<Cell>();
|
|
foreach (Row row in rows)
|
|
{
|
|
rangeResult.InsertRange(rangeResult.Count,
|
|
(from cell in row.Cells
|
|
where cell.ColumnNumber >= Cell.GetExcelColumnNumber(cellRange.ColumnStart) && cell.ColumnNumber <= Cell.GetExcelColumnNumber(cellRange.ColumnEnd)
|
|
select cell).ToList());
|
|
}
|
|
|
|
return rangeResult;
|
|
}
|
|
|
|
private IEnumerable<Row> GetRows(IEnumerable<XElement> rowElements)
|
|
{
|
|
foreach (var rowElement in rowElements)
|
|
{
|
|
yield return new Row(rowElement, this);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Read the existing sheet and copy some of the existing content
|
|
/// </summary>
|
|
/// <param name="stream">Worksheet stream</param>
|
|
/// <param name="worksheet">Saves the header and footer to the worksheet</param>
|
|
internal void ReadHeadersAndFooters(StreamReader stream, ref Worksheet worksheet)
|
|
{
|
|
var headers = new StringBuilder();
|
|
var footers = new StringBuilder();
|
|
|
|
bool headersComplete = false;
|
|
bool rowsComplete = false;
|
|
|
|
int existingHeadingRows = worksheet.ExistingHeadingRows;
|
|
|
|
while (stream.Peek() >= 0)
|
|
{
|
|
string line = stream.ReadLine();
|
|
int currentLineIndex = 0;
|
|
|
|
if (!headersComplete)
|
|
{
|
|
if (line.Contains("<sheetData/>"))
|
|
{
|
|
currentLineIndex = line.IndexOf("<sheetData/>");
|
|
headers.Append(line.Substring(0, currentLineIndex));
|
|
//remove the read section from line
|
|
line = line.Substring(currentLineIndex, line.Length - currentLineIndex);
|
|
|
|
headers.Append("<sheetData>");
|
|
|
|
// Headers complete now skip any content and start footer
|
|
headersComplete = true;
|
|
footers = new StringBuilder();
|
|
footers.Append("</sheetData>");
|
|
|
|
//There is no rows
|
|
rowsComplete = true;
|
|
}
|
|
else if (line.Contains("<sheetData>"))
|
|
{
|
|
currentLineIndex = line.IndexOf("<sheetData>");
|
|
headers.Append(line.Substring(0, currentLineIndex));
|
|
//remove the read section from line
|
|
line = line.Substring(currentLineIndex, line.Length - currentLineIndex);
|
|
|
|
headers.Append("<sheetData>");
|
|
|
|
// Headers complete now skip any content and start footer
|
|
headersComplete = true;
|
|
footers = new StringBuilder();
|
|
footers.Append("</sheetData>");
|
|
}
|
|
else
|
|
{
|
|
headers.Append(line);
|
|
}
|
|
}
|
|
|
|
if (headersComplete && !rowsComplete)
|
|
{
|
|
if (existingHeadingRows == 0)
|
|
{
|
|
rowsComplete = true;
|
|
}
|
|
|
|
if (!rowsComplete)
|
|
{
|
|
while (!string.IsNullOrEmpty(line) && existingHeadingRows != 0)
|
|
{
|
|
if (line.Contains("<row"))
|
|
{
|
|
if (line.Contains("</row>"))
|
|
{
|
|
int index = line.IndexOf("<row");
|
|
currentLineIndex = line.IndexOf("</row>") + "</row>".Length;
|
|
headers.Append(line.Substring(index, currentLineIndex - index));
|
|
|
|
//remove the read section from line
|
|
line = line.Substring(currentLineIndex, line.Length - currentLineIndex);
|
|
existingHeadingRows--;
|
|
}
|
|
else
|
|
{
|
|
int index = line.IndexOf("<row");
|
|
headers.Append(line.Substring(index, line.Length - index));
|
|
line = string.Empty;
|
|
}
|
|
}
|
|
else if (line.Contains("</row>"))
|
|
{
|
|
currentLineIndex = line.IndexOf("</row>") + "</row>".Length;
|
|
headers.Append(line.Substring(0, currentLineIndex));
|
|
|
|
//remove the read section from line
|
|
line = line.Substring(currentLineIndex, line.Length - currentLineIndex);
|
|
existingHeadingRows--;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (existingHeadingRows == 0)
|
|
{
|
|
rowsComplete = true;
|
|
}
|
|
}
|
|
|
|
if (rowsComplete)
|
|
{
|
|
if (line.Contains("</sheetData>"))
|
|
{
|
|
int index = line.IndexOf("</sheetData>") + "</sheetData>".Length;
|
|
var foot = line.Substring(index, line.Length - index);
|
|
footers.Append(foot);
|
|
}
|
|
else if (line.Contains("<sheetData/>"))
|
|
{
|
|
int index = line.IndexOf("<sheetData/>") + "<sheetData/>".Length;
|
|
footers.Append(line.Substring(index, line.Length - index));
|
|
}
|
|
else
|
|
{
|
|
footers.Append(line);
|
|
}
|
|
}
|
|
}
|
|
worksheet.Headers = headers.ToString();
|
|
worksheet.Footers = footers.ToString();
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Get worksheet file name from xl/workbook.xml
|
|
/// </summary>
|
|
internal void GetWorksheetProperties(FastExcel fastExcel, int? sheetNumber = null, string sheetName = null)
|
|
{
|
|
GetWorksheetPropertiesAndValidateNewName(fastExcel, sheetNumber, sheetName);
|
|
}
|
|
|
|
private bool GetWorksheetPropertiesAndValidateNewName(FastExcel fastExcel, int? sheetNumber = null, string sheetName = null, string newSheetName = null)
|
|
{
|
|
FastExcel = fastExcel;
|
|
bool newSheetNameExists = false;
|
|
|
|
FastExcel.CheckFiles();
|
|
FastExcel.PrepareArchive();
|
|
|
|
//If index has already been loaded then we can skip this function
|
|
if (Index != 0)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (!sheetNumber.HasValue && string.IsNullOrEmpty(sheetName))
|
|
{
|
|
throw new Exception("No worksheet name or number was specified");
|
|
}
|
|
|
|
using (Stream stream = FastExcel.Archive.GetEntry("xl/workbook.xml").Open())
|
|
{
|
|
var document = XDocument.Load(stream);
|
|
|
|
if (document == null)
|
|
{
|
|
throw new Exception("Unable to load workbook.xml");
|
|
}
|
|
|
|
var sheetsElements = document.Descendants().Where(d => d.Name.LocalName == "sheet").ToList();
|
|
|
|
XElement sheetElement = null;
|
|
|
|
if (sheetNumber.HasValue)
|
|
{
|
|
if (sheetNumber.Value <= sheetsElements.Count)
|
|
{
|
|
sheetElement = sheetsElements[sheetNumber.Value - 1];
|
|
}
|
|
else
|
|
{
|
|
throw new Exception(string.Format("There is no sheet at index '{0}'", sheetNumber));
|
|
}
|
|
}
|
|
else if (!string.IsNullOrEmpty(sheetName))
|
|
{
|
|
sheetElement = (from sheet in sheetsElements
|
|
from attribute in sheet.Attributes()
|
|
where attribute.Name == "name" && attribute.Value.Equals(sheetName, StringComparison.OrdinalIgnoreCase)
|
|
select sheet).FirstOrDefault();
|
|
|
|
if (sheetElement == null)
|
|
{
|
|
throw new Exception(string.Format("There is no sheet named '{0}'", sheetName));
|
|
}
|
|
|
|
if (!string.IsNullOrEmpty(newSheetName))
|
|
{
|
|
newSheetNameExists = (from sheet in sheetsElements
|
|
from attribute in sheet.Attributes()
|
|
where attribute.Name == "name" && attribute.Value.Equals(newSheetName, StringComparison.OrdinalIgnoreCase)
|
|
select sheet).Any();
|
|
|
|
if (FastExcel.MaxSheetNumber == 0)
|
|
{
|
|
FastExcel.MaxSheetNumber = (from sheet in sheetsElements
|
|
from attribute in sheet.Attributes()
|
|
where attribute.Name == "sheetId"
|
|
select int.Parse(attribute.Value)).Max();
|
|
}
|
|
}
|
|
}
|
|
|
|
Index = sheetsElements.IndexOf(sheetElement) + 1;
|
|
|
|
Name = (from attribute in sheetElement.Attributes()
|
|
where attribute.Name == "name"
|
|
select attribute.Value).FirstOrDefault();
|
|
}
|
|
|
|
if (!Exists)
|
|
{
|
|
throw new Exception("No worksheet was found with the name or number was specified");
|
|
}
|
|
|
|
if (string.IsNullOrEmpty(newSheetName))
|
|
{
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
return !newSheetNameExists;
|
|
}
|
|
}
|
|
|
|
internal void ValidateNewWorksheet(FastExcel fastExcel, int? insertAfterSheetNumber = null, string insertAfterSheetName = null)
|
|
{
|
|
if (string.IsNullOrEmpty(Name))
|
|
{
|
|
// TODO possibly could calulcate a new worksheet name
|
|
throw new Exception("Name for new worksheet is not specified");
|
|
}
|
|
|
|
// Get worksheet details
|
|
var previousWorksheet = new Worksheet(fastExcel);
|
|
bool isNameValid = previousWorksheet.GetWorksheetPropertiesAndValidateNewName(fastExcel, insertAfterSheetNumber, insertAfterSheetName, Name);
|
|
InsertAfterIndex = previousWorksheet.Index;
|
|
|
|
if (!isNameValid)
|
|
{
|
|
throw new Exception(string.Format("Worksheet name '{0}' already exists", Name));
|
|
}
|
|
|
|
fastExcel.MaxSheetNumber += 1;
|
|
Index = fastExcel.MaxSheetNumber;
|
|
|
|
if (string.IsNullOrEmpty(Headers))
|
|
{
|
|
Headers = DEFAULT_HEADERS;
|
|
}
|
|
|
|
if (string.IsNullOrEmpty(Footers))
|
|
{
|
|
Footers = DEFAULT_FOOTERS;
|
|
}
|
|
}
|
|
|
|
internal WorksheetAddSettings AddSettings
|
|
{
|
|
get
|
|
{
|
|
if (InsertAfterIndex.HasValue)
|
|
{
|
|
return new WorksheetAddSettings()
|
|
{
|
|
Name = Name,
|
|
SheetId = Index,
|
|
InsertAfterSheetId = InsertAfterIndex.Value
|
|
};
|
|
}
|
|
else
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} |