Using oledb to create excel file

You can use a library called ExcelLibrary. It’s a free, open source library posted on Google Code:

ExcelLibrary

This looks to be a port of the PHP ExcelWriter that you mentioned above. It will not write to the new .xlsx format yet, but they are working on adding that functionality in.

It’s very simple, small and easy to use. Plus it has a DataSetHelper that lets you use DataSets and DataTables to easily work with Excel data.

ExcelLibrary seems to still only work for the older Excel format (.xls files), but may be adding support in the future for newer 2007/2010 formats.

You can also use EPPlus, which works only for Excel 2007/2010 format files (.xlsx files). There’s also NPOI which works with both.

There are a few known bugs with each library as noted in the comments. In all, EPPlus seems to be the best choice as time goes on. It seems to be more actively updated and documented as well.

Also, as noted by @АртёмЦарионов below, EPPlus has support for Pivot Tables and ExcelLibrary may have some support (Pivot table issue in ExcelLibrary)

Here are a couple links for quick reference:
ExcelLibrary — GNU Lesser GPL
EPPlus — GNU (LGPL) — No longer maintained
EPPlus 5 — Polyform Noncommercial — Starting May 2020
NPOI — Apache License

Here some example code for ExcelLibrary:

Here is an example taking data from a database and creating a workbook from it. Note that the ExcelLibrary code is the single line at the bottom:

//Create the data set and table
DataSet ds = new DataSet("New_DataSet");
DataTable dt = new DataTable("New_DataTable");

//Set the locale for each
ds.Locale = System.Threading.Thread.CurrentThread.CurrentCulture;
dt.Locale = System.Threading.Thread.CurrentThread.CurrentCulture;

//Open a DB connection (in this example with OleDB)
OleDbConnection con = new OleDbConnection(dbConnectionString);
con.Open();

//Create a query and fill the data table with the data from the DB
string sql = "SELECT Whatever FROM MyDBTable;";
OleDbCommand cmd = new OleDbCommand(sql, con);
OleDbDataAdapter adptr = new OleDbDataAdapter();

adptr.SelectCommand = cmd;
adptr.Fill(dt);
con.Close();

//Add the table to the data set
ds.Tables.Add(dt);

//Here's the easy part. Create the Excel worksheet from the data set
ExcelLibrary.DataSetHelper.CreateWorkbook("MyExcelFile.xls", ds);

Creating the Excel file is as easy as that. You can also manually create Excel files, but the above functionality is what really impressed me.

This article simplifies your work with MS Excel (both xls and xlsx) using Oledb and Microsoft Data Access. Simple demonstration to create/modify/delete Excel for both windows and web is provided.

  • Sample tool in C# with XLSX (Office 2007) support — 24.99 KB
  • Sample tool in C# — 24.61 KB
  • Sample tool in VB.NET with XLSX (Office 2007) Support — 26.88 KB
  • Sample tool for VB.NET — 26.28 KB
  • Sample CreateWorkBook download in ASP.NET — 21.62 KB

Table of Contents

  1. Introduction
  2. Available Ways to work with Excel Workbooks
  3. Background
  4. Working with Excel Workbook
    1. Anatomy of ConnectionString
    2. Creating Excel Workbook
    3. Getting Schema Definition
    4. Retrieve Data By Worksheet Name
    5. Retrieve Data Using Range
    6. Manipulating Data (Insert / Update / Delete)
    7. Drop Excel Worksheet
  5. Description and Usage of Sample Tool
  6. Code Explanation and Usage Info
  7. History

Introduction

Hi folks. It’s been a while since I wrote my last article. Meanwhile, I came across a lot of stuff, and want to share it with you. This article is regarding all we need to work with Excel through our programs.

While searching Google for this topic, I came across some of the links, but none of them gave a clear and concise idea of how to work with data in Excel in the easiest way from .NET. So I decided to jot down everything that may appear with this topic in this article.

To Work With Excel Workbooks, You Can Do Through Three Different Ways

You need 3rd party library which acts as an interface between your program and the Excel.

  1. You can make use of Excel InterOp Objects, but this requires you to have Excel installed in the development environment. This is a binding if you are going to make a product which is to be distributed.
  2. You can use OleDb data providers for Excel which comes for free with Windows. But there is one limitation though, you can access only data using this technique. You cannot do formatting through this technique.
  3. You can use XML to create Excel objects which will open in MSExcel correctly. This is easier, just you need to work with XML through programming. It also supports XML stylesheets. I will also try to discuss this in another article, for the time being, you may look into ExcelXMLDemo.

In this topic, I am going to discuss about the 3rd method which is the most common one that we use while working with Excel.

Background

Excel is the most common and popular format of showing data to the client nowadays. After the most common one (PDF), you need to place another format which may show the reports to the client. Excel would be the right choice for that. Now we often come up with a requirement to generate the data in an Excel Workbook. Recently while developing, I got one requirement to dump some data in Excel sheet. Thus I thought of writing this one.

Another important requirement is to read data from MS Excel 2007 format, which is also an unusual task to learn the entire structure of Excel 2007 objects. Using MDac, one can easily work with both of them without changing any of the code whatsoever.

Working with Excel Workbook

The rows and columns of Excel workbook closely resemble the rows and columns of a database table. We can use MDac (Microsoft Data Access Tool) that comes free with Windows update to work with Excel worksheet. In case of Excel Workbooks, each worksheet acts as a table and each workbook is actually a database. You can create, insert drop Excel objects through OleDb data clients from your program.

Now Let Us See How the connectionstring Will Look Like

Normal ConnectionString: (work for xls files)

Provider=Microsoft.Jet.OLEDB.4.0;Data Source={0};Extended Properties="Excel 8.0;HDR=YES;""

Office 2007 ConnectionString : (work for xlsx files)

Provider=Microsoft.ACE.OLEDB.12.0;Data Source={0};
Extended Properties="Excel 12.0;HDR=YES;""

Here, Data Source will be placed with a proper filename like C:\test.xls or C:\test.xlsx. If you want to create a workbook, just place the one that is not existing and use Create Table to create a workbook.

The connectionstring has some parts:

  1. Provider: It is the main oledb provider that is used to open the Excel sheet. This will be Microsoft.Jet.OLEDB.4.0 for Excel 97 onwards Excel file format and Microsoft.ACE.OLEDB.12.0 for Excel 2007 or higher Excel file format (One with xlsx extension)
  2. Data Source: It is the entire path of the Excel workbook. You need to mention a dospath that corresponds to an Excel file. Thus, it will look like: Data Source=C:\testApp.xls".
  3. Extended Properties (Optional): Extended properties can be applied to Excel workbooks which may change the overall activity of the Excel workbook from your program. The most common ones are the following:
    • HDR: It represents Header of the fields in the Excel table. Default is YES. If you don’t have fieldnames in the header of your worksheet, you can specify HDR=NO which will take the columns of the tables that it finds as f1,f2 etc.
    • ReadOnly: You can also open Excel workbook in readonly mode by specifying ReadOnly=true; By default, Readonly attribute is false, so you can modify data within your workbook.
    • FirstRowHasNames: It is the same as HDR, it is always set to 1 ( which means true) you can specify it as false if you don’t have your header row. If HDR is YES, provider disregards this property. You can change the default behaviour of your environment by changing the Registry Value [HKLMSoftwareMicrosoftJet4.0EnginesExcelFirstRowHasNames] to 00 (which is false)
    • MaxScanRows: Excel does not provide the detailed schema defination of the tables it finds. It need to scan the rows before deciding the data types of the fields. MaxScanRows specifies the number of cells to be scanned before deciding the data type of the column. By default, the value of this is 8. You can specify any value from 1 — 16 for 1 to 16 rows. You can also make the value to 0 so that it searches all existing rows before deciding the data type. You can change the default behaviour of this property by changing the value of [HKLMSoftwareMicrosoftJet4.0EnginesExcelTypeGuessRows] which is 8 by default. Currently, MaxScanRows is ignored, so you need only to depend on TypeGuessRows Registry value. Hope Microsoft fixes this issue to its later versions.
    • IMEX: (A Caution) As mentioned above, Excel will have to guess a number or rows to select the most appropriate data type of the column, a serious problem may occur if you have mixed data in one column. Say you have data of both integer and text on a single column, in that case, Excel will choose its data type based on majority of the data. Thus it selects the data for the majority data type that is selected, and returns NULL for the minority data type. If the two types are equally mixed in the column, the provider chooses numeric over text.

      For example, in your eight (8) scanned rows, if the column contains five (5) numeric values and three (3) text values, the provider returns five (5) numbers and three (3) null values.

      To work around this problem for data, set «IMEX=1» in the Extended Properties section of the connection string. This enforces the ImportMixedTypes=Text registry setting. You can change the enforcement of type by changing [HKLMSoftwareMicrosoftJet4.0EnginesExcelImportMixedTypes] to numeric as well.

      Thus if you look into the simple connectionstring with all of them, it will look like:

      Provider=Microsoft.Jet.OLEDB.4.0;Data Source=c:\testexcel.xls;
      Extended Properties="Excel 8.0;HDR=YES;IMEX=1;MAXSCANROWS=15;READONLY=FALSE""

      or:

      Provider=Microsoft.ACE.OLEDB.12.0;Data Source=C:\testexcel.xlsx;
      Extended Properties="Excel 12.0;HDR=YES;IMEX=1;MAXSCANROWS=15;READONLY=FALSE""

      We need to place extended properties into Quotes(«) as there are multiple number of values.

Can We Create Excel Workbook through this Technique?

If you are eager to know if we can create Excel workbook directly through OleDB, your answer is yes. The only thing that you need to do is to specify a non-existing file in the Data Source of the connectionstring.

string connectionstring = "Provider=Microsoft.Jet.OLEDB.4.0;
                          Data Source=c:\testexcel.xls;
                          Extended Properties"Excel 8.0;HDR=YES"";
string createTableScript = "CREATE TABLE newTable(a1 MEMO,a2 INT,a3 CHAR(255))";
using(conObj = new OleDbConnection(connectionstring))
{
   using (OleDbCommand cmd = new OleDbCommand(createTableScript, conObj)
   {
     if (this.Connection.State != ConnectionState.Open) this.Connection.Open();
     cmd.ExecuteNonQuery();
   }
}

This will create a new workbook with one worksheet if the datasource file (testexcel.xls) is not existing in the location.

To Retrieve Schema Information of Excel Workbook

You can get the worksheets that are present in the Excel workbook using GetOleDbSchemaTable. Use the following snippet:

DataTable dtSchema = null;
dtSchema = conObj.GetOleDbSchemaTable(
OleDbSchemaGuid.Tables, new object[] { null, null, null, "TABLE" });

Here dtSchema will hold the list of all workbooks. Say we have two workbooks: wb1, wb2. The above code will return a list of wb1, wb1$, wb2, wb2$. We need to filter out $ elements.

Selecting Data From a WorkBook (Specifying Range)

You can run a simple query to select Data from an Excel workbook. Say your workbook contains tables like w1, w2. Now, if write SELECT * FROM [w1] or SELECT * FROM 'w1', it will return you the whole datatable with all the data.

You Can Also Specify the Range of Selection, Just Write Query Like

SELECT * FROM [w1$A10:B10]

Thus it will select the data only from Excel Cell A10 : B10 Range.

string cmdText = "SELECT * FROM [w1$A10:B10]";
using(OleDbCommand cmd = new OleDbCommand(cmdText))
{
      cmd.Connection = this.Connection;
      OleDbDataAdapter adpt = new OleDbDataAdapter(cmd);
      DataSet ds = new DataSet();
      adpt.Fill(ds,"w1");
}

NOTE

A caution about specifying worksheets: The provider assumes that your table of data begins with the upper-most, left-most, non-blank cell on the specified worksheet. In other words, your table of data can begin in Row 3, Column C without a problem. However, you cannot, for example, type a worksheeet title above and to the left of the data in cell A1.

A caution about specifying ranges: When you specify a worksheet as your source, the provider adds new records below existing records in the worksheet as space allows. When you specify a range (named or unnamed), Jet also adds new records below the existing records in the range as space allows. However, if you re-query on the original range, the resulting recordset does not include the newly added records outside the range. Using MDAC, you cannot add new rows beyond the defined limits of the range, otherwise, you will receive Exception: "Cannot expand named range"

Running DML Statement

You can run any DML statement in the same way you do for other databases. Samples:

INSERT INTO [w1] VALUES('firsttextcol', 2, '4/11/2009', '10:20');

[We assume First Column is either memo or Char field, 2nd col is int, 3rd is Date, 4th is Time data type]

DELETE FROM [w1] Where secondintcol=2;
UPDATE [w1] SET secondintcol = 3 where firsttextcol = 'firsttextcol';

We can use [] (Square brackets) to allow spaces within columnnames and tablenames as we do for databases.

Droping Excel WorkSheet

To Drop an Excel Worksheet, Just Use

Drop Table [w1]

This will drop the worksheet.

If this is the last worksheet, it will not delete the workbook file. You need to do it yourself.

Using the Sample Tool

I have added one sample application that demonstrates the problem. It includes one class called ExcelObject which allows you to work with Excel. You can use the code to work in your own application easily.

Excel_data_access/cool_image.JPG

  1. Choose Browse and select an xls file. If you want to create the workbook, just click on Create table to Create a table with workbook.

    Excel_data_access/cool_image1.JPG

  2. Click on Retrieve to get the Tables present in the workbook. These are mainly worksheets.

    Excel_data_access/cool_image3.JPG

  3. You can create tables using the window. Just write the column name and click on Insert. Specify Tablename and a new worksheet will be created for you.

    Excel_data_access/cool_image4.JPG

  4. Generate Insert statements from the dynamic screen.

    Excel_data_access/cool_image5.JPG

  5. You can use Go to get the data loaded into the Grid.

NOTE

This is just a demo application. You can use the Class associated with the application call functions to do your job easy.

Using the Code

The code for ExcelObject Class will be like this:

using System.IO;
using System.Data.OleDb;
using System.Text;
using System.Data;
using System.Windows.Forms;

public class ExcelObject
{
     private string excelObject = = "Provider=Microsoft.{0}.OLEDB.{1};Data Source={2};
                                     Extended Properties="Excel {3};HDR=YES"";
     private string filepath = string.Empty;
     private OleDbConnection con = null;

        public delegate void ProgressWork(float percentage);
        private event ProgressWork Reading;
        private event ProgressWork Writeing;
        private event EventHandler connectionStringChange;

        public event ProgressWork ReadProgress
        {
            add
            {
                Reading += value;
            }
            remove
            {
                Reading -= value;
            }
        }

        public virtual void onReadProgress(float percentage)
        {
            if (Reading != null)
                Reading(percentage);
        }

        public event ProgressWork WriteProgress
        {
            add{ Writeing += value; }
            remove{ Writeing -= value; }
        }

        public virtual void onWriteProgress(float percentage)
        {
            if (Writeing != null)
                Writeing(percentage);
        }

        public event EventHandler ConnectionStringChanged
        {
            add{ connectionStringChange += value; }
            remove { connectionStringChange -= value; }
        }

        public virtual void onConnectionStringChanged()
        {
            if (this.Connection != null && 
                !this.Connection.ConnectionString.Equals(this.ConnectionString))
            {
                if (this.Connection.State == ConnectionState.Open)
                    this.Connection.Close();
                this.Connection.Dispose();
                this.con = null;
            }
            if (connectionStringChange != null)
            {
                connectionStringChange(this, new EventArgs());
            }
        }
        
        public string ConnectionString
        {
            get
            {
                if (!(this.filepath == string.Empty))
                {
                   
                    FileInfo fi = new FileInfo(this.filepath);
                    if (fi.Extension.Equals(".xls"))
                    {
                        
                        return string.Format(this.excelObject, 
                                   "Jet", "4.0", this.filepath, "8.0");
                    }
                    else if (fi.Extension.Equals(".xlsx"))
                    {
                        
                        return string.Format(this.excelObject, 
                                   "Ace", "12.0", Me.filepath, "12.0");
                    }
                }
                else
                {
                    return string.Empty;
                }
            }
        }
        
        public OleDbConnection Connection
        {
            get
            {
                if (con == null)
                {
                    OleDbConnection _con = new OleDbConnection { 
                                ConnectionString = this.ConnectionString };
                    this.con = _con;
                }
                return this.con;
            }
        }

        public ExcelObject(string path)
        {
            this.filepath = path;
            this.onConnectionStringChanged();
        }
        
        public DataTable GetSchema()
        {
            DataTable dtSchema = null;
            if (this.Connection.State != ConnectionState.Open) this.Connection.Open();
            dtSchema = this.Connection.GetOleDbSchemaTable(
                   OleDbSchemaGuid.Tables, new object[] { null, null, null, "TABLE" });
            return dtSchema;
        }
        
        public DataTable ReadTable(string tableName)
        {
            return this.ReadTable(tableName, "");
        }

        public DataTable ReadTable(string tableName, string criteria)
        {
            try
            {
                DataTable resultTable = null;
                if (this.Connection.State != ConnectionState.Open)
                {
                    this.Connection.Open();
                    onReadProgress(10);
                    
                }
                string cmdText = "Select * from [{0}]";
                if (!string.IsNullOrEmpty(criteria))
                {
                    cmdText += " Where " + criteria;
                }
                OleDbCommand cmd = new OleDbCommand(string.Format(cmdText, tableName));
                cmd.Connection = this.Connection;
                OleDbDataAdapter adpt = new OleDbDataAdapter(cmd);
                onReadProgress(30);
                
                DataSet ds = new DataSet();
                onReadProgress(50);
                
                adpt.Fill(ds, tableName);
                onReadProgress(100);
                
                if (ds.Tables.Count == 1)
                {
                    return ds.Tables[0];
                }
                else
                {
                    return null;
                }
            }
            catch
            {
                MessageBox.Show("Table Cannot be read");
                return null;
            }
        }
        
        public bool DropTable(string tablename)
        {
            try
            {
                if (this.Connection.State != ConnectionState.Open)
                {
                    this.Connection.Open();
                    onWriteProgress(10);                    
                }
                string cmdText = "Drop Table [{0}]";
                using (OleDbCommand cmd = new OleDbCommand(
                         string.Format(cmdText, tablename), this.Connection))
                {
                    onWriteProgress(30);
                    
                    cmd.ExecuteNonQuery();
                    onWriteProgress(80);                    
                }
                this.Connection.Close();
                onWriteProgress(100);
                
                return true;
            }
            catch (Exception ex)
            {
                onWriteProgress(0);
                
                MessageBox.Show(ex.Message);
                return false;
            }
        }
        
        public bool WriteTable(string tableName, Dictionary<string, string> 
                                                             tableDefination)
        {
            try
            {
                using (OleDbCommand cmd = new OleDbCommand(
                this.GenerateCreateTable(tableName, tableDefination), this.Connection))
                {
                    if (this.Connection.State != ConnectionState.Open)
                    this.Connection.Open();
                    cmd.ExecuteNonQuery();
                    return true;
                }
            }
            catch
            {
                return false;
            }
        }
        
        public bool AddNewRow(DataRow dr)
        {
            using (OleDbCommand cmd = new OleDbCommand(
                          this.GenerateInsertStatement(dr), this.Connection))
            {
               cmd.ExecuteNonQuery();
            }
            return true;
        }
        
        private string GenerateCreateTable(string tableName, 
                            Dictionary<string, string> tableDefination)
        {
            StringBuilder sb = new StringBuilder();
            bool firstcol = true;
            sb.AppendFormat("CREATE TABLE [{0}](", tableName);
            firstcol = true;
            foreach (KeyValuePair<string, string> keyvalue in tableDefination)
            {
                if (!firstcol)
                {
                    sb.Append(",");
                }
                firstcol = false;
                sb.AppendFormat("{0} {1}", keyvalue.Key, keyvalue.Value);
            }

            sb.Append(")");
            return sb.ToString();
        }
        
        private string GenerateInsertStatement(DataRow dr)
        {
            StringBuilder sb = new StringBuilder();
            bool firstcol = true;
            sb.AppendFormat("INSERT INTO [{0}](", dr.Table.TableName);

            foreach (DataColumn dc in dr.Table.Columns)
            {
                if (!firstcol)
                {
                    sb.Append(",");
                }
                firstcol = false;

                sb.Append(dc.Caption);
            }

            sb.Append(") VALUES(");
            firstcol = true;
            for (int i = 0; i <= dr.Table.Columns.Count - 1; i++)
            {
                if (!object.ReferenceEquals(dr.Table.Columns[i].DataType, typeof(int)))
                {
                    sb.Append("'");
                    sb.Append(dr[i].ToString().Replace("'", "''"));
                    sb.Append("'");
                }
                else
                {
                    sb.Append(dr[i].ToString().Replace("'", "''"));
                }
                if (i != dr.Table.Columns.Count - 1)
                {
                    sb.Append(",");
                }
            }

            sb.Append(")");
            return sb.ToString();
        }
    }

After looking through the code you are clear that we are actually generating DDL and DML statements based on the Schema Definition. I know we can easily do this using OleDbCommandBuilder object, but I thought of making them myself. Functions exposed through this class are:

Methods

  • GetSchema: It returns the Schema defination datatable of the currently selected xls file. You can call this if you have connected with an existing Excel Workbook.
  • ReadTable: It automatically generates Select statement on the tablename passed and based on the criteria provided. It returns the DataTable of the currently selected Excel worksheet.
  • DropTable: Drops the table name passed, and which results in actual deletion of one worksheet from the workbook. The Function returns true if successful.
  • AddNewRow: This function creates an Insert statement and inserts a new row based on the DataRow passed in.

Properties

  • ConnectionString: You can get connectionstring of the filepath passed.
  • Connection: Returns OleDbConnection Object.

Events

  • ReadProgress: It generates a callback to the calling procedure on the percentage of Read of the file. You can handle this event to get the percentage progress value.
  • WriteProgress: Same as ReadProgress, only it is called during actual insert of data.
  • ConnectionStringChanged: This event occurs if FileName is changed somehow or a new file is created.

I have also provided the same class in VB.NET for those people who wants it in VB.NET.

You can find both of them from here:

Version 1

  • Download ExcelWrite_Csharp.zip — 24.61 KB
  • Download ExcelWrite_VB.NET.zip — 26.28 KB

Version 2

  • Download ExcelWrite_Csharp_V2.zip — 24.99 KB
  • Download ExcelWrite_VBNET_V2.zip — 26.88 KB
  • I have also added one example for ASP.NET users to dynamically create one Excel file and download it to clients.

You can find that from here:

  • Download ExcelDownload.zip — 21.62 KB

History

  • 7th June, 2009: First release
    • Looking forward to updating the article with new things. Hope you like this article.
  • 10th June, 2009: Second release
    • Support for xlsx files (Office 2007 Files). Hope this would help you.

When working with excel file for data mining or export data to excel many time required to read excel file and also write file. One can use Oledb Provider to connect existing file or create new excel file for report in .net

1. Get Sheets Name from Excel File

      
System.Data.OleDb.OleDbConnection conn = new System.Data.OleDb.OleDbConnection();
try
{
string filePath = @"c:myExcel2012.xlsx";
conn.ConnectionString = String.Format(@"Provider=Microsoft.ACE.OLEDB.12.0;Data Source={0};Extended Properties=""Excel 12.0 Xml;HDR=YES"";",filePath);
conn.Open();
DataTable dtSheets = conn.GetOleDbSchemaTable(System.Data.OleDb.OleDbSchemaGuid.Tables, null);
foreach (DataRow dr in dtSheets.Rows)
{
Console.WriteLine(dr["TABLE_NAME"].ToString()); // Print Sheet Name
}
}
finally
{
if(conn.State == ConnectionState.Open)
conn.Close();
}

2. Read Sheet Data

System.Data.OleDb.OleDbConnection conn = new System.Data.OleDb.OleDbConnection();
try
{
string filePath = @"c:myExcel2012.xlsx";
conn.ConnectionString = String.Format(@"Provider=Microsoft.ACE.OLEDB.12.0;Data Source={0};Extended Properties=""Excel 12.0 Xml;HDR=YES"";", filePath);
conn.Open();
DataTable dtSheets = conn.GetOleDbSchemaTable(System.Data.OleDb.OleDbSchemaGuid.Tables, null);
foreach (DataRow dr in dtSheets.Rows)
{
Console.WriteLine(dr["TABLE_NAME"].ToString());
var cmd = conn.CreateCommand();
cmd.CommandText = "SELECT * FROM " + dr["TABLE_NAME"].ToString();
var reader = cmd.ExecuteReader();
DataTable dt = new DataTable();
dt.Load(reader); // This will load data from excel sheet to datatable.

// your code to work on sheet data.
}
}
finally
{
if (conn.State == ConnectionState.Open)
conn.Close();
}

3. Create and Write Data to Excel sheet

            System.Data.OleDb.OleDbConnection conn = new System.Data.OleDb.OleDbConnection();
try
{
string pathOfFileToCreate = "c:newexcel.xlsx";
conn.ConnectionString = String.Format(@"Provider=Microsoft.ACE.OLEDB.12.0;Data Source={0};Extended Properties=""Excel 12.0 Xml;HDR=YES"";",pathOfFileToCreate);
conn.Open();
var cmd = conn.CreateCommand();
cmd.CommandText = "CREATE TABLE sheet1 (ID INTEGER,NAME NVARCHAR(100))"; // Create Sheet With Name Sheet1
cmd.ExecuteNonQuery();
for (int i = 0; i < 1000; i++) // Sample Data Insert
{
cmd.CommandText = String.Format("INSERT INTO sheet1 (ID,NAME) VALUES({0},'{1}')", i, "Name" + i.ToString());
cmd.ExecuteNonQuery(); // Execute insert query against excel file.
}
}
finally
{
if (conn.State == ConnectionState.Open)
conn.Close();
}

I feel that above sample code is best way to explain article.

Table of Contents

  • Introduction
  • Building the Sample
  • Source code
  • Description
  • Common operations:
    • Microsoft PIA
    • OleDb option for exporting
      • Extended properties
    • Using OpenXML
    • Exception Handling
    • Location of excel operations
  • Conclusions
  • Special Note
  • See also

Introduction

When creating data-centric business applications customers may require their data readily available in one or more formats such as Microsoft Excel or a requirement to import data from one or more formats where one may be Microsoft
Excel. This articles intent is to provide not only options for fulfilling these requirements but also do comparisons between various paths for interacting with Microsoft Excel.

All code runs in a Windows desktop project yet all classes to interact with Excel are in class projects with no dependencies on Windows forms

Building the Sample

  • Requires SQL-Server Express edition or higher
  • NuGet packages have been removed from the solution as the total space is large. Use NuGet «restore packages» if you don’t have restore packages set by default. If you have a slow computer downloading the required packages may take five or more minutes.
  • Scripts to create and populate data is located under ExcelOperations project in the folder SqlScripts. Before running the script check the location of the database to ensure the script runs correctly. As is, the location is the
    default location for SQL-Server installation.
  • Some code makes use of C# 7 but with a little modification will work with earlier versions of C#
  • Source code.

Source code

Description

This article explores working with Excel using automation, Open XML and OleDb for common tasks such as exporting from a DataGridView and much more. There will be farther in this startup series.

Most of the code can be executed in the Windows form project while there are bits of code which are not and when you come across code not presented/callable from the form there will be sample code in method headers on how to
call the method e.g. there are two examples (in ExcelOperations class) for exporting to delimited text files, they are self-contained so simply call them while in Operations class in ExcelOperations class/OleDbWork there is a method ReadPeopleNoHeaderRow which
has sample calls to the method in the method header.

There are examples for working with automation, Open XML and OleDb. Run each one and note time to execute. Every time Open XML will be faster than the other two while OleDb is a close second on some calls. But speed is not everything,
for example, we want a list of sheet names. Using OleDb the sheet names are in A-Z order while the other two methods are in ordinal position. Suppose you need to create a new Excel file, this is not possible with OleDb and similar OleDb has no methods for
formatting data. Knowing this one might use Excel automation and when doing so with a large data set the application may become unresponsive which means accept this or use a backgrounder worker component or use async techniques to overcome a non-responsive
application (async is shown in one code sample). 

There are 14 file formats for Excel were this article will work with one, .xlsx which is the default XML-based file format for Excel 2007 and higher versions while .xls is for Excel 97 through Excel 2003.

The reason the focus is on Excel 2007 file format is this is the industry standard. Excel prior versions are still used yet less and less
as time goes by.

Common operations:

  • Exporting a DataGridView bound to a DataTable or a List on a form with no formatting for column headers or cells.
  • Exporting a DataGridView bound to a DataTable List on a form with formatting for columns and/or cells.
  • Export a DataGridView without the DataSource property set. With or without formatting of cells and/or columns.
  • Export from a container such as a DataTable or List with or without a column or cell formatting.
  • Export from a database table(s) with or without a column or cell formatting.
  • Export from a text file or XML file with or without a column or cell formatting.
  • Import one or more WorkSheets into a database, text file, XML or similar container.

Note Not all of these operations are covered in this article yet the ones which are will provide insight into how to do those task which are not presented. Also there are several code samples not presented in
the main form application, these demos will have in the method headers how to use the method.

Exporting data to Excel without formatting for many the choice is to work with Excel automation or through OleDb manage data provider.

Considerations for working with Excel automation.

Several objects are needed to get started, Excel.Application, Excel.WorkBook and Excel.WorkSheet. First the Excel.Application object is created followed by a ExcelWorkBook which form the base for creating an Excel object in memory.
Next an Excel.WorkSheet is created with the option of providing a sheet name. If no sheet name is provided the sheet name will be the default, Sheet1. Once the Worksheet has been added to the Excel.Application by way of the Excel.WorkBook data can now be inserted
into the sole WorkSheet.

Many a developer will jump onto their favorite search engine and come up with a code sample such as the one shown below in figure 1. At first glance, this looks great so the developer drops the code into their project and then
realizes that there is no save method, easy enough to fix by adding the Save method.  The first issue is how a developer thinks about exception handling, in this case, a try-catch statement is used which displays a message that something failed. Usually when
there is a runtime exception with code as shown below all objects created to interact with Excel will usually (and it’s most of the time) stick around in memory unknown to the client who may make several attempts to use the cool export feature and on each
failure leaves more un-freed objects in memory and may very well require them to restart the machine to clear memory left around from failed export operations.

The next issue is the operation is performed within a form. These operations should be within a class dedicated to this and similar operations but not with data operations such as reading data from a database to populate a DataGridView.
Data operations belong in their own class. The form then calls methods in these classes to read data (and perform add/edit/delete operations) from a database then in the call methods in the Excel class to export to Excel. One way around these issues is to
move away from Excel automation as shown in figure 1A which uses a language extension method. The language extension converts data in a DataGridView to a DataTable were the DataGridView may have been populated by its DataSource or populated by adding rows
manually. The extension method provides an option to include or exclude column headers. Once the DataTable is returned from the extension method it’s passed to a method for a third-party library known as SpreadSheetLight which is free and installable via NuGet
inside Visual Studio. A single method call is used to export/import the DataTable into a new Excel file including options to name the WorkSheet. The benefit of this method is it is fairly universal, not tied to any set of data while the downsides are; requires
a third party library, the data inserted into Excel are all strings (which is the same for when using Excel automation). If we exclude the cells will all be strings and focus on having to use a third-party library the alternate is to create a method that a
DataTable is passed which uses either OpenXML to export data to Excel which in this case will require a considerable code base, no different if OleDb were to be used to export/import data into Excel. No matters which direction is taken there are advantages
and disadvantages. The last option is to go with a third party library paid for or free, two of them are EPPlus and GemBox. Using a paid library makes since only when you need efficient and flexible methods that you don’t have to maintain or that your methods
created in house are not performing as expected and time is critical to completing the application. 

Microsoft PIA

Primary Interop Assembly which are based on Component Object Model (COM). When you call a COM object of Office from managed code, a Runtime Callable Wrapper (RCW) is automatically created. The RCW marshals calls between the .NET
application and the COM object. The RCW keeps a reference count on the COM object. If all references have not been released on the RCW, the COM object of Office does not quit and may cause the Office application not to quit after your automation has finished
or not finished because of an exception.

Figure 1

using
Excel = Microsoft.Office.Interop.Excel;

private
void
btnExportExcel_Click(
object
sender, EventArgs e)

{

    try

    {

        Microsoft.Office.Interop.Excel.Application excel =
new
Microsoft.Office.Interop.Excel.Application();

        excel.Visible =
true;

        Microsoft.Office.Interop.Excel.Workbook workbook = excel.Workbooks.Add(System.Reflection.Missing.Value);

        Microsoft.Office.Interop.Excel.Worksheet sheet1 = (Microsoft.Office.Interop.Excel.Worksheet)workbook.Sheets[1];

        int
StartCol = 1;

        int
StartRow = 1;

        int
j = 0, i = 0;

        //Write Headers

        for
(j = 0; j < dgvSource.Columns.Count; j++)

        {

            Microsoft.Office.Interop.Excel.Range myRange = (Microsoft.Office.Interop.Excel.Range)sheet1.Cells[StartRow, StartCol +
j];

            myRange.Value2 = dgvSource.Columns[j].HeaderText;

        }

        StartRow++;

        //Write datagridview content

        for
(i = 0; i < dgvSource.Rows.Count; i++)

        {

            for
(j = 0; j < dgvSource.Columns.Count; j++)

            {

                try

                {

                    Microsoft.Office.Interop.Excel.Range myRange = (Microsoft.Office.Interop.Excel.Range)sheet1.Cells[StartRow + i,
StartCol + j];

                    myRange.Value2 = dgvSource[j, i].Value ==
null
?
"" : dgvSource[j, i].Value;

                }

                catch

                {

                    ;

                }

            }

        }

    }

    catch
(Exception ex)

    {

        MessageBox.Show(ex.ToString());

    }

}

This alternate method to export data from a DataGridView full source can be found here. The main parts are as follows. A language extension method to convert a DataGridView contents to a DataTable where all values will be strings.
Full source for the following code.

Figure 1A 

using
System; 

using
System.Data; 

using
System.IO; 

using
System.Linq; 

using
System.Windows.Forms; 

namespace
UtilityLibrary 


    public
static
class
DataGridViewExtensionMethods 

    

        /// <summary> 

        /// Given a DataGridView populates without a data source, 

        /// create a DataTable, populate from rows/cells from the 

        /// DataGridView with an option to include/exclude column names. 

        /// </summary> 

        /// <param name="pDataGridView"></param> 

        /// <param name="pColumnNames"></param> 

        /// <returns></returns> 

        /// <remarks> 

        /// There is no attempt made to figure out data types coming 

        /// from data in the DataGridView 

        /// </remarks> 

        public
static
DataTable GetDataTable(
this
DataGridView pDataGridView, bool
pColumnNames = true

        

            DataTable dt =
new
DataTable(); 

            foreach
(DataGridViewColumn column in
pDataGridView.Columns) 

            

                if
(column.Visible) 

                

                    if
(pColumnNames) 

                    

                        dt.Columns.Add(new
DataColumn() { ColumnName = column.Name }); 

                    

                    else 

                    

                        dt.Columns.Add(); 

                    

                

            

            object[] cellValues =
new
object
[pDataGridView.Columns.Count]; 

            foreach
(DataGridViewRow row in
pDataGridView.Rows) 

            

                if
(!row.IsNewRow) 

                

                    for
(int
i = 0; i < row.Cells.Count; i++) 

                    

                        cellValues[i] = row.Cells[i].Value; 

                    

                    dt.Rows.Add(cellValues); 

                

            

            return
dt; 

        

        /// <summary> 

        /// Generates comma delimited rows into a string array. 

        /// </summary> 

        /// <param name="sender"></param> 

        /// <returns></returns> 

        /// <remarks></remarks> 

        public
static
string
[] CommaDelimitedRows(this
DataGridView sender) 

        

            return

                from row
in
sender.Rows.Cast<DataGridViewRow>() 

                where !((DataGridViewRow)row).IsNewRow 

                let RowItem =
string.Join(",", Array.ConvertAll(((DataGridViewRow)row).Cells.Cast<DataGridViewCell>().ToArray(), (DataGridViewCell
c) => ((c.Value ==
null) ?
"" : c.Value.ToString()))) 

                select RowItem).ToArray(); 

        

        public
static
void
ExportToCommandDelimitedFile(this
DataGridView pSender, string
pFileName) 

        

            File.WriteAllLines(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, pFileName), pSender.CommaDelimitedRows()); 

        

        public
static
void
ExpandColumns(this
DataGridView sender) 

        

            foreach
(DataGridViewColumn col in
sender.Columns) 

            

                col.AutoSizeMode = DataGridViewAutoSizeColumnMode.AllCells; 

            

        

    

}

With the DataTable returned from the extension above the following class provides exporting capabilities

using
System; 

using
System.Data; 

using
SpreadsheetLight; 

using
DocumentFormat.OpenXml.Spreadsheet; 

using
SysDraw = System.Drawing; 

using
System.IO; 

namespace
SpreadSheetLightLibrary 


    public
class
ExcelOperations 

    

        /// <summary> 

        /// True if last operation had a runtime exception 

        /// </summary> 

        /// <returns></returns> 

        public
bool
HasException {
get;
set; } 

        /// <summary> 

        /// Exception of last operation ran that throw an exception 

        /// </summary> 

        /// <returns></returns> 

        public
Exception Exception { get;
set; } 

        /// <summary> 

        /// Wraps Exception message 

        /// </summary> 

        /// <returns></returns> 

        public
string
ExceptionMessage 

        

            get 

            

                if
(HasException) 

                

                    return
Exception.Message; 

                

                else 

                

                    return
"None"

                

            

        

        /// <summary> 

        /// Export DataTable to a new Excel file 

        /// </summary> 

        /// <param name="pFileName">Path and Excel file name</param> 

        /// <param name="pSheetName">Sheet name to place data</param> 

        /// <param name="pDataTable">DataTable to import into pSheetName</param> 

        /// <param name="pColumnHeaders">Include or exclude columns</param> 

        /// <returns></returns> 

        public
bool
SimpleExport(
string
pFileName,
string
pSheetName, DataTable pDataTable,
bool
pColumnHeaders) 

        

            try 

            

                if
(File.Exists(pFileName)) 

                

                    File.Delete(pFileName); 

                

                using
(SLDocument doc = new
SLDocument()) 

                

                    doc.SelectWorksheet(pSheetName); 

                    if
(pSheetName != "Sheet1"

                    

                        doc.AddWorksheet(pSheetName); 

                        doc.DeleteWorksheet("Sheet1"); 

                    

                    // start at row 1, column A 

                    // SLConvert.ToColumnIndex("A") is kind of overkill but it exposes you to the convert class 

                    doc.ImportDataTable(1, SLConvert.ToColumnIndex("A"),
pDataTable, pColumnHeaders); 

                    if
(pColumnHeaders) 

                    

                        var headerSyle = doc.CreateStyle(); 

                        headerSyle.Font.FontColor = SysDraw.Color.White; 

                        headerSyle.Font.Strike =
false

                        headerSyle.Fill.SetPattern(PatternValues.Solid, SysDraw.Color.Green, SysDraw.Color.White); 

                        headerSyle.Font.Underline = UnderlineValues.None; 

                        headerSyle.Font.Bold =
true

                        headerSyle.Font.Italic =
false

                        doc.SetCellStyle(1, 1, 1, pDataTable.Columns.Count, headerSyle); 

                    

                    doc.AutoFitColumn(1, pDataTable.Columns.Count); 

                    doc.SaveAs(pFileName); 

                

                return
true

            

            catch
(Exception ex) 

            

                HasException =
true

                Exception = ex; 

                return
false

            

        

        /// <summary> 

        /// This represents the bare amount of code to import 

        /// a DataTable into a new Excel file. Please note if 

        /// there are dates they need to be format using a style. 

        ///  

        /// For an example of date styling/formatting 

        /// Class project ExcelBackend, Operations class method ImportDataTable2  

        /// where I setup a style for a date as mm-dd-yyyy 

        ///  

        /// Formatting is beyond this code sample as this code sample is meant 

        /// to be a generic method to take a DataGridView into a DataTable then 

        /// use the DataTable to import into a WorkSheet. I could had done dates 

        /// and other types but that is more specific to your data and as mentioned 

        /// the link above is my code sample that shows formatting/styling. 

        ///  

        /// </summary> 

        /// <param name="pFileName"></param> 

        /// <param name="pSheetName"></param> 

        /// <param name="pDataTable"></param> 

        /// <param name="pColumnHeaders"></param> 

        public
void
SimpleExportRaw(
string
pFileName, string
pSheetName, DataTable pDataTable, bool
pColumnHeaders) 

        

            using
(SLDocument doc = new
SLDocument()) 

            

                doc.SelectWorksheet(pSheetName); 

                if
(pSheetName != "Sheet1"

                

                    doc.AddWorksheet(pSheetName); 

                    doc.DeleteWorksheet("Sheet1"); 

                

                doc.ImportDataTable(1, SLConvert.ToColumnIndex("A"),
pDataTable, pColumnHeaders); 

                doc.SaveAs(pFileName); 

            

        

    

}

The key to exporting in this case, one sole method to perform the export. As mentioned earlier you could discard the idea of using a third party library and instead write this code yourself yet if not an expert with OpenXML there
really is no clear reason to go in this direction as we want easy to use methods without worrying about maintaining said code.

How to handle exceptions properly to ensure all objects are released? The key is persistence when designing and coding, in this case a method to export data to Excel using automation.

Start off by populating a DataGridView with perhaps 200 rows with a nominal column count. Run the operation, did all work properly, if so do the same operation with and without allowing users to add new rows to the DataGridView, 
in the sample above this would throw an exception if there is a new row in the DataGridView. A fix is to check for the new row while iterating the DataGridView.

If a runtime exception is thrown best to open Task Manager and kill any instances of Excel that are shown in the process window.

Tip:  Place a StatusStrip on the form, add a label on the status strip with images as shown in figure 2, use the image shown below from the sample project or whatever images work for you, images are easier to
see the text which is why images were used here. Add a Timer component and create the Tick event for the timer. In the Tick event add code as shown below (can be found in the source code for this article) and add the method IsExcelInMemory. Now when coding
and testing Excel operations when you see the square image Excel is not in memory while the Excel image indicates Excel is still in memory. Excel is notorious for staying in memory even if no runtime exception was thrown. The number one reason because a run
time exception being thrown for Excel to stay in memory is when you have a line of code where you have more than two dots to access a method or property e.g. Excel.WorkBook.WorkSheet.Range where there should be one object for Excel, one for the WorkBook then
one for the Sheet so when we access the Sheet there is only one dot in the code. When there are more than two dots the underlying COM objects will not release properly and tend to hang around in memory.

In each code sample for this article that demonstrates Excel automation, there are zero lines of code that have more than one dot in a method call to Excel for this very reason.

Another way to determine issues is to use an event in the class which does Excel operations. The following is an excerpt from the code provided with this article.

public
class
ExaminerEventArgs : EventArgs

{

    public
ExaminerEventArgs(string
message)

    {

        StatusMessage = message;

    }

    public
string
StatusMessage {
get;
set; }

}

public
EventHandler<ExaminerEventArgs> ProgressUpdated;

private
void
OnProgressUpdated(
string
message)

{

    ProgressUpdated?.Invoke(this,
new
ExaminerEventArgs(message));

}

Then at key areas in your code call OnProgressUpdate in the class which in turn (in this case) displays your text in a ListBox. We could instead write to a text file yet for development purposes immediate results are better.

xlWorkBooks = xlApp.Workbooks;

xlWorkBook = xlWorkBooks.Open(fileName);

OnProgressUpdated("Opened");

#region Timer logic

private
bool
IsExcelInMemory()

{

    return
Process.GetProcesses().Any(p => p.ProcessName.Contains("EXCEL"));

}

private
void
timer1_Tick(
object
sender, EventArgs e)

{

    if
(IsExcelInMemory())

    {

        toolStripStatusLabel1.Image = Properties.Resources.ExcelInMemory;

    }

    else

    {

        toolStripStatusLabel1.Image = Properties.Resources.ExcelNotInMemory;

    }

    toolStripStatusLabel1.Invalidate();

}

#endregion

Figure 2 (top image shows Excel is not in memory while the second indicates Excel is still in memory)

Coupling up the status image and event for displaying where we are at in code can greatly assist with debugging issues with Excel automation code in tangent with not using more than two dots in a call to Excel.

Another way to reduce the chances of objects staying in memory to avoid exceptions is to wrap the code which may have issues with a try-catch-finally.  The first step is to create a list of objects, each time a new object is
used to interact with Excel is created we add this object to the list. Then if an exception is thrown release the objects created prior to the exception. Chances are this will clutter your code to the point that it may become unmaintainable so the next option
is to discard the try-catch-finally and fully test your code but leave the list of objects part.

In the following example a list of objects is created, each time an Excel object is created it’s added to the list. Once the Excel operations are completed the objects are disposed. There is one exception to using the list of
objects in the code sample presented below which is locating the intended WorkSheet to read via a for-next if the current sheet in the iteration is not the sheet we want to read then dispose of this object immediately. This is done, in this case by using a
C# 7 feature, local function and if not using C# 7 the method ReleaseComObject would be moved outside the method with a private scope.

public
void
ReadCells(
string
pFileName,
string
pSheetName)

{

    void
ReleaseComObject(object
pComObject)

    {

        try

        {

            Marshal.ReleaseComObject(pComObject);

            pComObject =
null;

        }

        catch
(Exception)

        {

            pComObject =
null;

        }

    }

    var annihilationList =
new
List<
object>();

    var proceed =
false;

    Excel.Application xlApp =
null;

    Excel.Workbooks xlWorkBooks =
null;

    Excel.Workbook xlWorkBook =
null;

    Excel.Worksheet xlWorkSheet =
null;

    Excel.Sheets xlWorkSheets =
null;

    Excel.Range xlCells =
null;

    xlApp =
new
Excel.Application();

    annihilationList.Add(xlApp);

    xlApp.DisplayAlerts =
false;

    xlWorkBooks = xlApp.Workbooks;

    annihilationList.Add(xlWorkBooks);

    xlWorkBook = xlWorkBooks.Open(pFileName);

    annihilationList.Add(xlWorkBook);

    xlApp.Visible =
false;

    xlWorkSheets = xlWorkBook.Sheets;

    annihilationList.Add(xlWorkSheets);

    for
(var intSheet = 1; intSheet <= xlWorkSheets.Count; intSheet++)

    {

        try

        {

            xlWorkSheet = (Excel.Worksheet)xlWorkSheets[intSheet];

            if
(xlWorkSheet.Name == pSheetName)

            {

                proceed =
true;

                break;

            }

            else

            {

                ReleaseComObject(xlWorkSheet);

            }

        }

        catch
(Exception ex)

        {

            HasErrors =
true;

            ExceptionInfo.UnKnownException =
true;

            ExceptionInfo.Message = $"Error finding sheet: '{ex.Message}'";

            ExceptionInfo.FileNotFound =
false;

            ExceptionInfo.SheetNotFound =
false;

            proceed =
false;

            annihilationList.Add(xlWorkSheet);

        }

    }

    if
(!proceed)

    {

        var firstSheet = (Excel.Worksheet)xlWorkSheets[1];

        xlWorkSheet = xlWorkSheets.Add(firstSheet);

        xlWorkSheet.Name = pSheetName;

        annihilationList.Add(firstSheet);

        annihilationList.Add(xlWorkSheet);

        xlWorkSheet.Name = pSheetName;

        proceed =
true;

        ExceptionInfo.CreatedSheet =
true;

    }

    else

    {

        if
(!annihilationList.Contains(xlWorkSheet))

        {

            annihilationList.Add(xlWorkSheet);

        }

    }

    if
(proceed)

    {

        if
(!annihilationList.Contains(xlWorkSheet))

        {

            annihilationList.Add(xlWorkSheet);

        }

        foreach
(var key in
ReturnDictionary.Keys.ToArray())

        {

            try

            {

                xlCells = xlWorkSheet.Range[key];

                ReturnDictionary[key] = xlCells.Value;

                annihilationList.Add(xlCells);

            }

            catch
(Exception e)

            {

                HasErrors =
true;

                ExceptionInfo.Message = $"Error reading cell [{key}]: '{e.Message}'";

                ExceptionInfo.FileNotFound =
false;

                ExceptionInfo.SheetNotFound =
false;

                annihilationList.Add(xlCells);

                xlWorkBook.Close();

                xlApp.UserControl =
true;

                xlApp.Quit();

                annihilationList.Add(xlCells);

                return;

            }

        }

    }

    else

    {

        /*

            * Send information back to caller why we failed

            */

        HasErrors =
true;

        ExceptionInfo.SheetNotFound =
true;

        ExceptionInfo.FileNotFound =
false;

    }

    // this is debatable, should we save the file after adding a non-existing sheet?

    if
(ExceptionInfo.CreatedSheet)

    {

        xlWorkSheet?.SaveAs(pFileName);

    }

    xlWorkBook.Close();

    xlApp.UserControl =
true;

    xlApp.Quit();

    ReleaseObjects(annihilationList);

}
public
void
ReadCells(string
pFileName, string
pSheetName)

{

    void
ReleaseComObject(object
pComObject)

    {

        try

        {

            Marshal.ReleaseComObject(pComObject);

            pComObject =
null;

        }

        catch
(Exception)

        {

            pComObject =
null;

        }

    }

    var annihilationList =
new
List<
object>();

    var proceed =
false;

    Excel.Application xlApp =
null;

    Excel.Workbooks xlWorkBooks =
null;

    Excel.Workbook xlWorkBook =
null;

    Excel.Worksheet xlWorkSheet =
null;

    Excel.Sheets xlWorkSheets =
null;

    Excel.Range xlCells =
null;

    xlApp =
new
Excel.Application();

    annihilationList.Add(xlApp);

    xlApp.DisplayAlerts =
false;

    xlWorkBooks = xlApp.Workbooks;

    annihilationList.Add(xlWorkBooks);

    xlWorkBook = xlWorkBooks.Open(pFileName);

    annihilationList.Add(xlWorkBook);

    xlApp.Visible =
false;

    xlWorkSheets = xlWorkBook.Sheets;

    annihilationList.Add(xlWorkSheets);

    for
(var intSheet = 1; intSheet <= xlWorkSheets.Count; intSheet++)

    {

        try

        {

            xlWorkSheet = (Excel.Worksheet)xlWorkSheets[intSheet];

            if
(xlWorkSheet.Name == pSheetName)

            {

                proceed =
true;

                break;

            }

            else

            {

                ReleaseComObject(xlWorkSheet);

            }

        }

        catch
(Exception ex)

        {

            HasErrors =
true;

            ExceptionInfo.UnKnownException =
true;

            ExceptionInfo.Message = $"Error finding sheet: '{ex.Message}'";

            ExceptionInfo.FileNotFound =
false;

            ExceptionInfo.SheetNotFound =
false;

            proceed =
false;

            annihilationList.Add(xlWorkSheet);

        }

    }

    if
(!proceed)

    {

        var firstSheet = (Excel.Worksheet)xlWorkSheets[1];

        xlWorkSheet = xlWorkSheets.Add(firstSheet);

        xlWorkSheet.Name = pSheetName;

        annihilationList.Add(firstSheet);

        annihilationList.Add(xlWorkSheet);

        xlWorkSheet.Name = pSheetName;

        proceed =
true;

        ExceptionInfo.CreatedSheet =
true;

    }

    else

    {

        if
(!annihilationList.Contains(xlWorkSheet))

        {

            annihilationList.Add(xlWorkSheet);

        }

    }

    if
(proceed)

    {

        if
(!annihilationList.Contains(xlWorkSheet))

        {

            annihilationList.Add(xlWorkSheet);

        }

        foreach
(var key in
ReturnDictionary.Keys.ToArray())

        {

            try

            {

                xlCells = xlWorkSheet.Range[key];

                ReturnDictionary[key] = xlCells.Value;

                annihilationList.Add(xlCells);

            }

            catch
(Exception e)

            {

                HasErrors =
true;

                ExceptionInfo.Message = $"Error reading cell [{key}]: '{e.Message}'";

                ExceptionInfo.FileNotFound =
false;

                ExceptionInfo.SheetNotFound =
false;

                annihilationList.Add(xlCells);

                xlWorkBook.Close();

                xlApp.UserControl =
true;

                xlApp.Quit();

                annihilationList.Add(xlCells);

                return;

            }

        }

    }

    else

    {

        /*

            * Send information back to caller why we failed

            */

        HasErrors =
true;

        ExceptionInfo.SheetNotFound =
true;

        ExceptionInfo.FileNotFound =
false;

    }

    // this is debatable, should we save the file after adding a non-existing sheet?

    if
(ExceptionInfo.CreatedSheet)

    {

        xlWorkSheet?.SaveAs(pFileName);

    }

    xlWorkBook.Close();

    xlApp.UserControl =
true;

    xlApp.Quit();

    ReleaseObjects(annihilationList);

}

The last line, ReleaseObjects in this case resides in a base class which the method above implements. During the releasing of objects care is taken to ensure no runtime exception is thrown. If objects do not release as expected
you can set a break-point in ReleaseObjects method, inspect the list, see if something does not seem correct, by not seem correct, you may inspect these objects and one or more may be null which means a debug session is in order to examine how the objects
were created.

public
void
ReleaseObjects(List<
object> pAnnihilationList)

{

    for
(var indexer = 0; indexer < pAnnihilationList.Count; indexer++)

    {

        try

        {

            if
(pAnnihilationList[indexer] != null)

            {

                Marshal.ReleaseComObject(pAnnihilationList[indexer]);

                pAnnihilationList[indexer] =
null;

            }

        }

        catch
(Exception)

        {

            pAnnihilationList[indexer] =
null;

        }

    }

}

Keeping with disposal of objects, a quick search on the web for “kill Excel…” is all over the web. There will be suggestions such as

private
void
KillSpecificExcelFileProcess(
string
excelFileName)

{

        var processes = from p
in
Process.GetProcessesByName(
"EXCEL")

                        select p;

        foreach
(var process in
processes)

        {

            if
(process.MainWindowTitle == "Microsoft Excel - "
+ excelFileName)

                process.Kill();

        }

}

Which look great and most of the time work yet the problem lies in a poor understanding of how to put objects together and dispose of them. By having proper understanding of how each object is created, used and disposed of the
only time for code presented above is while in the process of writing your code and a mistake is made causing Excel to stay in memory.

Here is a brute force method to release objects.

GC.Collect();

GC.WaitForPendingFinalizers();

GC.Collect();

GC.WaitForPendingFinalizers();

Again this is only needed when the developer created objects incorrectly. The number one reason is the developer interacted with Excel, changed objects and properties together passing two dots in the call to get at a property
in very common.

The number one reason for not releasing objects correctly as mentioned is from not understanding how to create and use objects but where does this originate from? Old code samples on the web where developers perhaps wrote code
in a code module within Excel, the code worked so it was ported to a Visual Studio project. While in Excel modules Excel took care of releasing objects internally and the developers who ported code from Excel to a project did not understand this or that most
of the time (but not all of the time) objects were released upon closing the project as the GC (Garbage Collector) disposed of these objects. Yet some object will never release as expected when a) violating the two-dot rule or when the construction of a command
is done incorrectly.

The following example works as expected and releases objects but note there is very little that might cause objects not to release.

public
void
ExportToDelimited_1()

{

    Excel.Application xlApp =
new
Excel.Application();

    Excel.Workbook xlWorkBook =
null;

    string
exportFileName = null;

    xlApp.Visible =
false;

    xlWorkBook = xlApp.Workbooks.Open(Path.Combine(

        AppDomain.CurrentDomain.BaseDirectory,
"People.xlsx"));

    exportFileName = Path.Combine(AppDomain.CurrentDomain.BaseDirectory,
"People.csv");

    xlWorkBook.SaveAs(exportFileName, Excel.XlFileFormat.xlCSV);

    xlWorkBook =
null;

    xlApp.Quit();

}

We got away with releasing all object yet as mentioned earlier this can lead to a false sense of security that objects were released. Better to always release objects as shown below in a modified version of the last code sample.
Note more objects were used which is how we take full control of the executing code.

public
void
ExportToDelimited_2()

{

    void
ReleaseComObject(object
pComObject)

    {

        try

        {

            Marshal.ReleaseComObject(pComObject);

            pComObject =
null;

        }

        catch
(Exception)

        {

            pComObject =
null;

        }

    }

    Excel.Application xlApp =
null;

    Excel.Workbooks xlWorkBooks =
null;

    Excel.Workbook xlWorkBook =
null;

    Excel.Worksheet xlWorkSheet =
null;

    xlApp =
new
Excel.Application();

    xlApp.DisplayAlerts =
false;

    xlWorkBooks = xlApp.Workbooks;

    xlWorkBook = xlWorkBooks.Open(Path.Combine(

        AppDomain.CurrentDomain.BaseDirectory,
"People.xlsx"));

    xlWorkSheet = (Excel.Worksheet)xlWorkBook.ActiveSheet;

    xlWorkBook.SaveAs(Path.Combine(AppDomain.CurrentDomain.BaseDirectory,

            "People.csv"),

        FileFormat: Excel.XlFileFormat.xlCSVWindows);

    xlWorkBook.Close();

    xlApp.UserControl =
true;

    xlApp.Quit();

    ReleaseComObject(xlWorkSheet);

    ReleaseComObject(xlWorkBook);

    ReleaseComObject(xlWorkBooks);

    ReleaseComObject(xlApp);

}

When working with Excel automation be proactive, consider that whenever working with Excel automation that an object may not be released so take it upon yourself to fully test the code and release objects one by one.

OleDb option for exporting

Keeping with exporting a DataGridView bound or unbound with no formatting, a consideration should be with using OleDb. The main issue with interacting with Excel using OleDb is getting the connection string correct. There are
several pieces to a connection string.

The main parts we are interested are as follows.

Provider which is either Microsoft.Jet.OLEDB.4.0 for .xls file format or Microsoft.ACE.OLEDB.12.0 for .xlsx file format. Either is consider the OleDb data provider to interact with Microsoft Excel.

Extended properties

  •  HDR: It represents Header of the fields in the excel table. Default is YES. If you don’t have fieldnames in the header of your worksheet, you can specify HDR=NO which will take the columns of the tables that
    it finds as F1,F2 etc. where Fn represents a column e.g. F1 is column A, F2 is column B. If when displaying data in a DataGridView for example where the first row is data and we don’t specify column names then we will see Fn for column names. Since when reading
    data A SELECT statement of SELECT * with three columns of data the DataGridView would show F1 F2 and F3 but if we were to write SELCT F1 As FirstName, F2 As LastName, F3 As Country then as with any SQL SELECT statement the alias names for the column will display
    and when interacting with these columns the aliases would be used except for the ORDER BY or WHERE clauses. In a section below a read operation is performed which shows how to read data that has unexpected data types, not mixed but simply not what we where
    expecting.

  • IMEX: This attribute works in tangent with MaxScanRows attribute when dealing with mixed data. Mixed data refers to the following. In the first eight rows of a WorkSheet in a column, all data is of one type vs
    eight rows of data where there may be strings in three rows, numeric to four rows and one row with a date, this is mixed data. Excel attempts to figure out what type of data is in a column and rows which is known as a range. If there is mixed data in the first
    eight rows what you get back may not be what you expected. For instance, if there are 100 rows of data which is mixed data and you expected dates you might get all rows typed as strings or even a numeric. If you were to get back data as numbers, more specifically
    a double there is a method DateTime.FromOADate which returns a DateTime equivalent to the specified OLE Automation Date. IMEX can be set to allow read-only access or read-write access. The suggested setting for most operations is IMEX=1.

What is common for a connection string is as follows (note ,xls file format is shown yet the same holds true for .xlsx)

string
connectionstring = "Provider=Microsoft.Jet.OLEDB.4.0;   Data Source=c:\testexcel.xls; Extended Properties"Excel 8.0;HDR=YES"";

If you need to experiment with a connection string to get it right rather than using string concatenation directly in your code a class can be setup as follows.

namespace
ExcelOperations.OleDbWork

{

    public
enum
ExcelHeader

    {

        Yes,

        No

    }

    public
class
SmartConnection

    {

        public
string
ConnectionString(
string
pFileName, int
pImex = 1, ExcelHeader pHeader = ExcelHeader.No)

        {

            OleDbConnectionStringBuilder builder =
new
OleDbConnectionStringBuilder();

            if
(System.IO.Path.GetExtension(pFileName)?.ToUpper() ==
".XLS")

            {

                builder.Provider =
"Microsoft.Jet.OLEDB.4.0";

                builder.Add("Extended Properties", $"Excel
8.0;IMEX={pImex};HDR={pHeader.ToString()};"
);

            }

            else

            {

                builder.Provider =
"Microsoft.ACE.OLEDB.12.0";

                builder.Add("Extended Properties", $"Excel
12.0;IMEX={pImex};HDR={pHeader.ToString()};"
);

            }

            builder.DataSource = pFileName;

            return
builder.ConnectionString;

        }

    }

}

To create a new connection, create a new instance of SmartConnection class. Call ConnectionString with the file name to read, IMEX setting followed by Yes or No for setting up the connection for first row as data or first row
as column names. The file name is as setup the only required argument to be passed to the method unless IMEX or header arguments need to be overridden. This way there is string concatenation in the method which creates the connection.

Note in the method ConnectionString there is a class used, OleDbConnectionStringBuilder which makes life easy to setup the connection. Not only can a connection be created but also broken apart later for examination or to change
the connection.

Getting back to exporting data, there are no methods for creating an Excel file with OleDb data provider. The best course here is to have a blank Excel file ready to use. This file might be stored in a folder below the application
folder when needed copy the file to where you need the user of the application to access the file. Once the file has been copied (and renamed if needed) you can open the Excel file and import data from a DataTable or a List of a concrete class.

Another method to have a new file is to include a third-party library for the sole purpose of creating a new Excel file. There are several out there, one is called SpreadSheetLight
(there are code samples in the code samples provided with this article) which is a free library installable via NuGet Package manager. If you are the type that enjoys writing code rather than uses a library see

the following article which explains how to create an Excel file using OpenXml.

In the code samples provided here is part of the process to import a DataTable into an Excel file. The full code is setup and ready to run to test this method out.

public
void
ExportDataTableToExcel(
string
pFileName, DataTable pDataTable)

{

    mHasException =
false;

       /*

        * Create field names for the create table (worksheet)

        */

    var columnsPartsForCreateSheet = pDataTable.Columns.OfType<DataColumn>()

        .Select(col => $"{col.ColumnName.Replace("Column",
"
")} CHAR(255)")

        .ToArray();

       /*

        * Turn column name and type into a delimited string for the actual create statement

        * below.

        */

    var columnsForCreateSheet =
string.Join(",", columnsPartsForCreateSheet);

    /*

        * Full SQL INSERT statement

        */

    var createStatement = $"CREATE TABLE {pDataTable.TableName} ({columnsForCreateSheet})";

       /*

        * Column names for the INSERT SQL staetment.

        */

    var columnsPartsForInsert = pDataTable.Columns.OfType<DataColumn>()

        .Select(col => col.ColumnName.Replace("Column",""))

        .ToArray();

       /*

        * Create named parameters for the insert statement. Note that OleDb

        * does not 'use' named parameters so we could had used a question mark

        * for each parameter name as OleDb parameters are done in ordinal position

        * which the parameters are anyways. The idea is for developers to get

        * use to named parameters as when moving to SQL-Server named parameters

        * allow parameters to be set out of order

        */

    var paramsForInsert = pDataTable.Columns.OfType<DataColumn>()

        .Select(col =>
"@" + col.ColumnName.Replace("Column",""))

        .ToArray();

       /*

        * Insert statement for populating rows in the WorkSheet.

        */

    var insertStatement =

        $"INSERT INTO {pDataTable.TableName} ({string.Join(",",
columnsPartsForInsert)}) + "
+

        $"VALUES ({string.Join(",",
paramsForInsert)})"
;

    try

    {

        var con =
new
SmartConnection();

           /*

            * IMPORTANT: In the connection string, second parameter must be IMEX = 0 for this to work.

            */

        using
(var cn = new
OleDbConnection(con.ConnectionString(pFileName,0, ExcelHeader.Yes)))

        {

            using
(var cmd = new
OleDbCommand { Connection = cn })

            {

                cmd.CommandText = createStatement;

                cn.Open();

                   /*

                    * Create the WorkSheet

                    */

                cmd.ExecuteNonQuery();

                   /*

                    * Change our commandText for the INSERT

                    */

                cmd.CommandText = insertStatement;

                   /*

                    * Create parameters once rather than creating them for each

                    * iteration of an insert, clearing or re-creating the parameters.

                    */

                foreach
(var pName in
paramsForInsert)

                {

                    cmd.Parameters.Add(pName, OleDbType.VarWChar);

                }

                   /*

                    * Insert row into the WorkSheet.

                    */

                for
(int
rowIndex = 0; rowIndex < pDataTable.Rows.Count ; rowIndex++)

                {

                    for
(int
colIndex = 0; colIndex < pDataTable.Columns.Count ; colIndex++)

                    {

                           /*

                            * Set each parameter's value

                            */

                        cmd.Parameters[colIndex]

                            .Value = pDataTable.Rows[rowIndex]

                            .Field<string>(pDataTable.Columns.IndexOf(pDataTable.Columns[colIndex]));

                    }

                    cmd.ExecuteNonQuery();

                }

            }

        }

    }

    catch
(Exception e)

    {

        mHasException =
true;

        mLastException = e;

    }

}

/// <summary>

/// Get WorkSheet names for a specific Excel file

/// </summary>

/// <param name="pFileName"></param>

/// <returns></returns>

public
List<string> SheetNames(string
pFileName)

{

    mHasException =
false;

    var names =
new
List<
string>();

    var con =
new
SmartConnection();

    try

    {

        using
(var cn = new
OleDbConnection(con.ConnectionString(pFileName)))

        {

            cn.Open();

            var dt = cn.GetSchema("Tables",
new
string
[] { null,
null,
null,
"Table" });

            foreach
(DataRow row in
dt.Rows)

            {

                names.Add(row.Field<string>("Table_Name").Replace("$",
""));

            }

        }

    }

    catch
(Exception ex)

    {

        mHasException =
true;

        mLastException = ex;

    }

    return
names.Distinct().ToList(); 

}

The above shows one method to handle writing data to a WorkSheet, there are many other methods which can be used, this is one to help getting into writing data to a WorkSheet.

In the section above for setting up a connection for columns aliasing and unexpected data types there may be cases where the data types are different than expected. Suppose the operation calls for reading a WorkSheet into a list.
A connection is opened, a command object is setup, ExecuteReader is invoked off the command and while iterating the data a run time exception is thrown indicating the type you are asking for is incorrect and does not make sense so you try a different type
and this fails too. One way around this is to keep existing code, add code to read the data into a DataTable then iterate the columns and see what the data types are reported back as. For instance, you attempted to get column A as a Integer and failed. We
iterate the columns and see that what was returned as a Double, this now tells us to read column A as a Double. Column D was thought to be a Date but by iterating the columns what is reported is a string. This means a conversion is in order.

Here we have a reader to read by iterating results from the command object and also using the reader to populate a DataTable. The DataTable code can be removed once the data types are known. They have been left in for you the
reader to see this process in action by adding the DataTable and not disturbing the reader code for iterating data.

public
void
ReadPeopleNoHeaderRow(
string
pFileName, string
pSheetName) 

{

    mHasException =
false;

    List<Person> peopleList =
new
List<Person>();

    var con =
new
SmartConnection();

    var dt =
new
DataTable();

    try

    {

        using
(var cn = new
OleDbConnection { ConnectionString = con.ConnectionString(pFileName, 1, ExcelHeader.No) })

        {

            var selectStatement =
"SELECT F1 AS Identifer, F2 AS FirstName, F3 As LastName, "

                                            "F4 AS Gender, F5 As BirthDate "
+

                                    $"FROM [{pSheetName}$]";

            using
(var cmd = new
OleDbCommand { Connection = cn, CommandText = selectStatement })

            {

                cn.Open();

                var reader = cmd.ExecuteReader();

                dt.Load(reader);

                reader.Close();

                reader = cmd.ExecuteReader();

                while
(reader != null
&& reader.Read())

                {

                    peopleList.Add(new
Person()

                    {

                        Id = Convert.ToInt32(reader.GetDouble(0)),

                        FirstName = reader.GetString(1),

                        LastName = reader.GetString(2),

                        Gender = Convert.ToInt32(reader.GetDouble(3)),

                        BirthDay = Convert.ToDateTime(reader.GetString(4))

                    });

                }

                reader.Close();

            }

        }

    }

    catch
(Exception ex)

    {

        mHasException =
true;

        mLastException = ex;

    }

    foreach
(DataColumn col in
dt.Columns)

    {

        Console.WriteLine(col.DataType.ToString());

    }

}

The following shows a clean example of reading data from a WorkSheet into a List. The class is different from above but the same process. 

public
List<Customer> ReadCustomers(string
pFileName)

{

    mHasException =
false;

    var selectStatement =
"SELECT CompanyName, ContactName,ContactTitle FROM [Customers$]";

    List<Customer> customers =
new
List<Customer>();

    var con =
new
SmartConnection();

    try

    {

        using
(var cn = new
OleDbConnection { ConnectionString = con.ConnectionString(pFileName,1, ExcelHeader.Yes) })

        {

            using
(var cmd = new
OleDbCommand { Connection = cn, CommandText = selectStatement })

            {

                cn.Open();

                var reader = cmd.ExecuteReader();

                while
(reader != null
&& reader.Read())

                {

                    customers.Add(new
Customer()

                    {

                        CompanyName = reader.GetString(0),

                        ContactName = reader.GetString(1),

                        ContactTitle = reader.GetString(2)

                    });

                }

            }

        }

    }

    catch
(Exception ex)

    {

        mHasException =
true;

        mLastException = ex;

    }

    return
customers;

}

Other operations that OleDb excels at (although in some cases OpenXML is a better choice) is transferring data from a database directly to Excel. See
the following code sample for transferring MS-Access to Excel. Going in the opposite direction, this time switching it up to transfer from SQL-Server to Excel see
the following code sample. There is a bit more work involved for this process which is explained in the code sample and to get started is best to do the steps in SQL-Server Management
Studio.

Using OpenXML

So far Excel automation and OleDb have been touched on, another option for working with Excel is using OpenXml. The downside to working with OpenXML is there are many more moving parts and with that, without fully understanding
how pieces need to fall into place the end result is typically an unusable file. Unlike Excel automation, OpenXML does not leave objects in memory if there is an exception thrown at runtime.

The following code sample creates a new Excel file, a WorkSheet and populates the sheet with a list of a class.

public
void
CreateExcelDocPopulateWithPeople(
string
pFileName, string
pSheetName, List<Person> pList)

{

    mHasException =
false;

    try

    {

        using
(var document = SpreadsheetDocument.Create(pFileName, SpreadsheetDocumentType.Workbook))

        {

            WorkbookPart wbp = document.AddWorkbookPart();

            wbp.Workbook =
new
Workbook();

            var wsp = wbp.AddNewPart<WorksheetPart>();

            wsp.Worksheet =
new
Worksheet();

            var sheets = wbp.Workbook.AppendChild(new
Sheets());

            var sheet =
new
Sheet()

            {

                Id = wbp.GetIdOfPart(wsp),

                SheetId = 1,

                Name =
"People"

            };

            // ReSharper disable once PossiblyMistakenUseOfParamsMethod

            sheets?.Append(sheet);

            wbp.Workbook.Save();

            WorkbookStylesPart stylePart = wbp.AddNewPart<WorkbookStylesPart>();

            stylePart.Stylesheet = CreateStylesheet();

            stylePart.Stylesheet.Save();

            var sheetData = wsp.Worksheet.AppendChild(new
SheetData());

            var headerRow = sheetData.AppendChild(new
Row());

            headerRow.AppendChild(new
Cell()

            {

                CellValue =
new
CellValue(
"Id"),

                DataType = CellValues.String,

                StyleIndex = 2

            });

            headerRow.AppendChild(new
Cell()

            {

                CellValue =
new
CellValue(
"First Name"),

                DataType = CellValues.String,

                StyleIndex = 2

            });

            headerRow.AppendChild(new
Cell()

            {

                CellValue =
new
CellValue(
"Last Name"),

                DataType = CellValues.String,

                StyleIndex = 2

            });

            headerRow.AppendChild(new
Cell()

            {

                CellValue =
new
CellValue(
"Gender"),

                DataType = CellValues.String,

                StyleIndex = 2

            });

            headerRow.AppendChild(new
Cell()

            {

                CellValue =
new
CellValue(
"Birthday"),

                DataType = CellValues.String,

                StyleIndex = 2

            });    

            // insert people data

            foreach
(var person in
pList)

            {

                var row =
new
Row();

                row.Append(

                    ConstructCell(person.Id.ToString(), CellValues.Number),

                    ConstructCell(person.FirstName, CellValues.String),

                    ConstructCell(person.LastName, CellValues.String),

                    ConstructCell(person.Role, CellValues.String),

                    ConstructCell(person.BirthDay.ToString("MM/dd/yyyy"),
CellValues.String));

                sheetData.AppendChild(row);

            }

            wsp.Worksheet.Save();

        }

    }

    catch
(Exception ex)

    {

        mHasException =
true;

        mLastException = ex;

    }

}

private
Stylesheet CreateStylesheet()

{

    Stylesheet styleSheet =
null;

    // 0 is default

    // 1 is header

    var fonts =
new
Fonts(

        new
Font(new
FontSize() { Val = 10 }),

        new
Font(new
FontSize() { Val = 12 },new
Bold(),new
Color() { Rgb = "FFFFFF"
}));

    var fills =
new
Fills(

        new
Fill(new
PatternFill() { PatternType = PatternValues.None }),
// Index 0 - default

        new
Fill(new
PatternFill() { PatternType = PatternValues.Gray125 }),
// Index 1 - default

        new
Fill(new
PatternFill(new
ForegroundColor { Rgb = new
HexBinaryValue() { Value = "000000"
} })

            { PatternType = PatternValues.Solid })
// Index 2 - header

    );

    var borders =
new
Borders(

        new
Border(),

        new
Border(

            new
LeftBorder(new
Color()   { Auto = true
}) { Style = BorderStyleValues.None },

            new
RightBorder(new
Color()  { Auto = true
}) { Style = BorderStyleValues.None },

            new
TopBorder(new
Color()    { Auto = true
}) { Style = BorderStyleValues.Thin },

            new
BottomBorder(new
Color() { Auto = true
}) { Style = BorderStyleValues.Thin },

            new
DiagonalBorder())

    );

    var cellFormats =
new
CellFormats(

        new
CellFormat(), // default

        new
CellFormat { FontId = 0, FillId = 0, BorderId = 1, ApplyBorder =
true
},
// body

        new
CellFormat { FontId = 1, FillId = 2, BorderId = 1, ApplyFill =
true
}   
// header

    );

    styleSheet =
new
Stylesheet(fonts, fills, borders, cellFormats);

    return
styleSheet;

}

/// <summary>

/// Construct cell of specific type

/// </summary>

/// <param name="value"></param>

/// <param name="dataType"></param>

/// <returns></returns>

private
Cell ConstructCell(string
value, CellValues dataType)

{

    return
new
Cell()

    {

        CellValue =
new
CellValue(value),

        DataType =
new
EnumValue<CellValues>(dataType)

    };

}

When working with OpenXML it’s best to take your time and learn how things fit together rather than attempting to code this in a project on a tight timeline.

In the code that comes with this article, there are several examples of working with OpenXML which will allow you to get a handle on coding with OpenXML.

Comparing Excel Automation, OleDb and OpenXML performance.

When working with a fair amount of data e.g. thousands of rows and a fair amount of columns Excel automation is the slowest followed by OleDb. OpenXML is lighting fast yet as mention above the downside is OpenXML is more difficult
to grasp for someone with zero experience with OpenXML. This is one reason developers will work with Excel automation yet the pitfalls here is ensuring all objects are released. OleDb has pitfalls also, no method to create a new Excel file or to format cells.

Which one should you use? This is dependent on the task, is there a lot of data, first look at OpenXML. Is there a good deal of formatting, consider time which is needed to work out styles in OpenXML. With the samples provided
they give you a decent start. Is the task simply to export to Excel without formatting and is not a huge data set then consider OleDb in tangent (if needed) with the new file idea mentioned above.

Another road block with OleDb is learning where to append data too while with OpenXML and Excel automation there are methods which will provide the last used row and last used column in a WorkSheet. The library mentioned above
SpreadSheetLight also provides last used row and column (this is because at its core it OpenXML).

Some developers who frequently work with Excel will sometimes use OleDb while other times Excel automation and perhaps other times OpenXML.

There is another method which is to create a anML template for an Excel file/WorkSheet and use

embedded expressions coupled with
XML literals. The only issue is this is only possible with VB.NET. Since C# does not support this the way around this issue is to create a VB.NET class project and call the VB.NET methods from the C# project.

Steps to write this up.

Create a new Excel file as Excel 2003 XML. Create your column headers and style the headers then save the file which you can open in NotePad and then copy into a method in a class as a XDocument.

Next, locate the worksheet node e.g.

<Worksheet
ss:Name="People">

The next element is the table element

<Table
ss:ExpandedColumnCount=<%= columnCount %> ss:ExpandedRowCount=<%= customers.Count + 1 %> x:FullColumns="1"

Note columnCount and customers.Count, these refer to the count of columns and rows in the data source. The DataSource could be a DataTable or a List of a class.

This is followed by locating the row element directly below the Table element. Here we setup the column names followed by the data source.

The last step is to save the xml as a .xml file. When a user double clicks on the xml file in Windows Explorer Excel is the default program which will open the xml file. Once in Excel do a save as Excel 2007 format and now you
have a proper Excel file. This may not be for everyone but the benefit of this method is working with huge data sets is extremely fast.

Exception Handling

When working with Excel no matter which path is taken there should be proper exception handling rather than simply writing code and expecting it to function. For instance, a user creates an Excel file from your application then
opens the Excel file outside of the application. Then they forget and run the process again and fails because the file is open and can’t be created. Another failure point is when working with Excel automation the customer gets an update to Office with updates
the version of Excel Interop library and causes a runtime exception because the newer software is incompatible with methods used to perform operations you coded into the application.

For a repeatable pattern consider creating a base exception class which your class performing Excel operations implements.

A basic exception class

using System;

using System.Data.SqlClient;

namespace ExceptionsLibrary

{

    public class BaseExceptionsHandler

    {

        protected bool mHasException;

        /// <summary>

        /// Indicate the last operation thrown an 

        /// exception or not

        /// </summary>

        /// <returns></returns>

        public bool HasException => mHasException;

        protected Exception mLastException;

        /// <summary>

        /// Provides access to the last exception thrown

        /// </summary>

        /// <returns></returns>

        public Exception LastException => mLastException;

        /// <summary>

        /// Indicates if there was a sql related exception

        /// </summary>

        public bool HasSqlException => mLastException is SqlException;

        /// <summary>

        /// If you don't need the entire exception as in 

        /// LastException this provides just the text of the exception

        /// </summary>

        /// <returns></returns>

        public string LastExceptionMessage => mLastException.Message;

        /// <summary>

        /// Indicate for return of a function if there was an 

        /// exception thrown or not.

        /// </summary>

        /// <returns></returns>

        public bool IsSuccessFul => !mHasException;

    }

}

Setup for a class working with Excel

public
class
OpenXmlExamples : BaseExceptionsHandler

Using with Open XML

public
void
CreateNewFile(
string
pFileName,
string
pSheetName)

{

    mHasException =
false;

    try

    {

        using
(var doc = SpreadsheetDocument.Create(pFileName, SpreadsheetDocumentType.Workbook))

        {

            var wbp = doc.AddWorkbookPart();

            wbp.Workbook =
new
Workbook();

            var wsp = wbp.AddNewPart<WorksheetPart>();

            wsp.Worksheet =
new
Worksheet(
new
SheetData());

            var sheets = wbp.Workbook.AppendChild(new
Sheets());

            var sheet =
new
Sheet()

            {

                Id = wbp.GetIdOfPart(wsp),

                SheetId = 1,

                Name = pSheetName

            };

            sheets?.Append(sheet);

            wbp.Workbook.Save();

        }

    }

    catch
(Exception ex)

    {

        mHasException =
true;

        mLastException = ex;

    }

}

With OleDb

public
List<string> SheetNames(string
pFileName)

{

    mHasException =
false;

    var names =
new
List<
string>();

    var con =
new
SmartConnection();

    try

    {

        using
(var cn = new
OleDbConnection(con.ConnectionString(pFileName)))

        {

            cn.Open();

            var dt = cn.GetSchema("Tables",
new
string
[] { null,
null,
null,
"Table" });

            foreach
(DataRow row in
dt.Rows)

            {

                names.Add(row.Field<string>("Table_Name").Replace("$",
""));

            }

        }

    }

    catch
(Exception ex)

    {

        mHasException =
true;

        mLastException = ex;

    }

    return
names.Distinct().ToList(); 

}

Keeping with the last example, if there is no exception a list of strings is returned and if there is an exception a empty list of strings is returned. We can see if there was an exception or not as follows

private
void
cmdGetSheetNamesAutomation_Click(
object
sender, EventArgs e)

{

    var example =
new
ExcelBaseExample();

    var fileName = Path.Combine(AppDomain.CurrentDomain.BaseDirectory,
"Customers.xlsx");

    var sheetNames = example.GetWorkSheetNames(fileName);

    if
(!example.HasErrors)

    {

        sheetNames.Insert(0,
"Select sheet");

        cboSheetNamesAutomation.DataSource = sheetNames;

    }

    else

    {

        // notifiy user

    }

}

Location of excel operations

The best place for these operations is not within a form but instead within a class dedicated to working with Excel. This is no different than where should operations go for interacting with databases. We can decouple the Excel
operations into a class project so that if other projects need similar functionality all you need to do is add a reference to the class project with Excel operations.

Within the supplied code samples this is shown, Excel operations are in their own class project.  The following screen shot is from the code sample which goes with this article. Note that specific operations are separated into
various class projects and are all used in MainApplication. Several of the class projects were brought in from other solutions and simply work as they were intended to be plug and play.

Conclusions

There is more than one way to work with Excel, which method is best all depends on your requirements. In this article I, have briefly touched on several methods to interact with Excel. In part two of this series formatting of cells will be discussed along with
working with ranges, references tables. Part three in this series will cover reporting which includes working with charts. Part four in the series will deal with working with mixed data types along with suggestions for importing and exporting data from unknown
sources.

Special Note

When trying out the demonstrations in the sample code, when the main application executes Excel files in the bindebug folder are removed and or removed with fresh copies. This means for any write operations done on the last
execution will be lost on the new execution. All files which are expected to be in the application folder are in a folder at solution level. All operations are within the FolderLibrary project and are called in FormShown event of the main form.

NuGet packages used in the solution, there are a great deal of packages which in short are required by Open XML. So don’t be alarmed by the list of packages.

To list all packages in the NuGet console

Get-Package | ft -AutoSize

See also

Excel patterns for properly releasing memory once finished with Excel operations (C#).
Excel get last row and column for sheet, column or range (C#).
Export Excel from SQL-Server (VB.NET and C#).
DataGridView unbound to Excel or Text file (C#).
Alternate methods for with Microsoft Excel in VB.NET projects.
Export MS-Access table to MS-Excel Worksheet (VB.NET and C#).
Read ranges in Excel using OleDb data provider (VB.NET).
Set row height and column width for all cells in an Excel sheet using automation (VB.NET with C# helpers).

Open XML SDK.
Open XML SDK on GitHub.

Excel Object Model Overview.

, 2006-01-18 (first published: 2003-04-24)

This is a T-SQL script that uses OLE, ADO, Jet4 ISAM, and Linked Server to create and populate an Excel Workbook (XLS) file from T-SQL query. If the Excel Worksheet exists, the query will append to the «table».
The code is designed to be used by SQL Agent and to append to the step output with verbose and minimal detail.
Code is pretty well commented, including some hard won knowledge about Jet4 ISAM, OLE, ADO, and usage of the Excel table from T-SQL.

-- Create XLS script DAL - 04/24/2003
--
-- Designed for Agent scheduling, turn on "Append output for step history"
--
-- Search for %%% to find adjustable constants and other options
--
-- Uses OLE for ADO and OLE DB to create the XLS file if it does not exist
--   Linked server requires the XLS to exist before creation
-- Uses OLE ADO to Create the XLS Worksheet for use as a table by T-SQL
-- Uses Linked Server to allow T-SQL access to XLS table
-- Uses T-SQL to populate te XLS worksheet, very fast
--
PRINT 'Begin CreateXLS script at '+RTRIM(CONVERT(varchar(24),GETDATE(),121))+' '
PRINT ''
GO

SET NOCOUNT ON
DECLARE @Conn int -- ADO Connection object to create XLS
, @hr int -- OLE return value
, @src varchar(255) -- OLE Error Source
, @desc varchar(255) -- OLE Error Description
, @Path varchar(255) -- Drive or UNC path for XLS
, @Connect varchar(255) -- OLE DB Connection string for Jet 4 Excel ISAM
, @WKS_Created bit -- Whether the XLS Worksheet exists
, @WKS_Name varchar(128) -- Name of the XLS Worksheet (table)
, @ServerName nvarchar(128) -- Linked Server name for XLS
, @DDL varchar(8000) -- Jet4 DDL for the XLS WKS table creation
, @SQL varchar(8000) -- INSERT INTO XLS T-SQL
, @Recs int -- Number of records added to XLS
, @Log bit -- Whether to log process detail

-- Init variables
SELECT @Recs = 0
-- %%% 1 = Verbose output detail, helps find problems, 0 = minimal output detail
, @Log = 1 
-- %%% assign the UNC or path and name for the XLS file, requires Read/Write access
--   must be accessable from server via SQL Server service account
--   & SQL Server Agent service account, if scheduled
SET @Path = 'C:TEMPTest_'+CONVERT(varchar(10),GETDATE(),112)+'.xls'
-- assign the ADO connection string for the XLS creation
SET @Connect = 'Provider=Microsoft.Jet.OLEDB.4.0;Data Source='+@Path+';Extended Properties=Excel 8.0'
-- %%% assign the Linked Server name for the XLS population
SET @ServerName = 'EXCEL_TEST'
-- %%% Rename Table as required, this will also be the XLS Worksheet name
SET @WKS_Name = 'People'
-- %%% Table creation DDL, uses Jet4 syntax, 
--   Text data type = varchar(255) when accessed from T-SQL
SET @DDL = 'CREATE TABLE '+@WKS_Name+' (SSN Text, Name Text, Phone Text)'
-- %%% T-SQL for table population, note the 4 part naming required by Jet4 OLE DB
--   INSERT INTO SELECT, INSERT INTO VALUES, and EXEC sp types are supported
--   Linked Server does not support SELECT INTO types
SET @SQL = 'INSERT INTO '+@ServerName+'...'+@WKS_Name+' (SSN, Name, Phone) '
SET @SQL = @SQL+'SELECT au_id AS SSN'
SET @SQL = @SQL+', LTRIM(RTRIM(ISNULL(au_fname,'''')+'' ''+ISNULL(au_lname,''''))) AS Name'
SET @SQL = @SQL+', phone AS Phone '
SET @SQL = @SQL+'FROM pubs.dbo.authors'

IF @Log = 1 PRINT 'Created OLE ADODB.Connection object'
-- Create the Conn object
EXEC @hr = sp_OACreate 'ADODB.Connection', @Conn OUT
IF @hr <> 0 -- have to use <> as OLE / ADO can return negative error numbers
BEGIN
-- Return OLE error
EXEC sp_OAGetErrorInfo @Conn, @src OUT, @desc OUT 
SELECT Error=convert(varbinary(4),@hr), Source=@src, Description=@desc
RETURN
END

IF @Log = 1 PRINT char(9)+'Assigned ConnectionString property'
-- Set a the Conn object's ConnectionString property
--   Work-around for error using a variable parameter on the Open method
EXEC @hr = sp_OASetProperty @Conn, 'ConnectionString', @Connect
IF @hr <> 0
BEGIN
-- Return OLE error
EXEC sp_OAGetErrorInfo @Conn, @src OUT, @desc OUT 
SELECT Error=convert(varbinary(4),@hr), Source=@src, Description=@desc
RETURN
END

IF @Log = 1 PRINT char(9)+'Open Connection to XLS, for file Create or Append'
-- Call the Open method to create the XLS if it does not exist, can't use parameters
EXEC @hr = sp_OAMethod @Conn, 'Open'
IF @hr <> 0
BEGIN
-- Return OLE error
EXEC sp_OAGetErrorInfo @Conn, @src OUT, @desc OUT 
SELECT Error=convert(varbinary(4),@hr), Source=@src, Description=@desc
RETURN
END

-- %%% This section could be repeated for multiple Worksheets (Tables)
IF @Log = 1 PRINT char(9)+'Execute DDL to create '''+@WKS_Name+''' worksheet'
-- Call the Execute method to Create the work sheet with the @WKS_Name caption, 
--   which is also used as a Table reference in T-SQL
-- Neat way to define column data types in Excel worksheet
--   Sometimes converting to text is the only work-around for Excel's General 
--   Cell formatting, even though the Cell contains Text, Excel tries to format
--   it in a "Smart" way, I have even had to use the single quote appended as the
--   1st character in T-SQL to force Excel to leave it alone
EXEC @hr = sp_OAMethod @Conn, 'Execute', NULL, @DDL, NULL, 129 -- adCmdText + adExecuteNoRecords
-- 0x80040E14 for table exists in ADO
IF @hr = 0x80040E14 
-- kludge, skip 0x80042732 for ADO Optional parameters (NULL) in SQL7
OR @hr = 0x80042732
BEGIN
-- Trap these OLE Errors
IF @hr = 0x80040E14
BEGIN
PRINT char(9)+''''+@WKS_Name+''' Worksheet exists for append'
SET @WKS_Created = 0
END
SET @hr = 0 -- ignore these errors
END
IF @hr <> 0
BEGIN
-- Return OLE error
EXEC sp_OAGetErrorInfo @Conn, @src OUT, @desc OUT 
SELECT Error=convert(varbinary(4),@hr), Source=@src, Description=@desc
RETURN
END

IF @Log = 1 PRINT 'Destroyed OLE ADODB.Connection object'
-- Destroy the Conn object, +++ important to not leak memory +++
EXEC @hr = sp_OADestroy @Conn
IF @hr <> 0
BEGIN
-- Return OLE error
EXEC sp_OAGetErrorInfo @Conn, @src OUT, @desc OUT 
SELECT Error=convert(varbinary(4),@hr), Source=@src, Description=@desc
RETURN
END

-- Linked Server allows T-SQL to access the XLS worksheet (Table)
--   This must be performed after the ADO stuff as the XLS must exist
--   and contain the schema for the table, or worksheet
IF NOT EXISTS(SELECT srvname from master.dbo.sysservers where srvname = @ServerName)
BEGIN
IF @Log = 1 PRINT 'Created Linked Server '''+@ServerName+''' and Login'
EXEC sp_addlinkedserver @server = @ServerName
    , @srvproduct = 'Microsoft Excel Workbook'
    , @provider = 'Microsoft.Jet.OLEDB.4.0'
    , @datasrc = @Path
    , @provstr = 'Excel 8.0' 
-- no login name or password are required to connect to the Jet4 ISAM linked server
EXEC sp_addlinkedsrvlogin @ServerName, 'false' 
END

-- Have to EXEC the SQL, otherwise the SQL is evaluated 
--   for the linked server before it exists
EXEC (@SQL)
PRINT char(9)+'Populated '''+@WKS_Name+''' table with '+CONVERT(varchar,@@ROWCOUNT)+' Rows'

-- %%% Optional you may leave the Linked Server for other XLS operations
--   Remember that the Linked Server will not create the XLS, so remove it
--   When you are done with it, especially if you delete or move the file
IF EXISTS(SELECT srvname from master.dbo.sysservers where srvname = @ServerName)
BEGIN
IF @Log = 1 PRINT 'Deleted Linked Server '''+@ServerName+''' and Login'
EXEC sp_dropserver @ServerName, 'droplogins'
END
GO

SET NOCOUNT OFF
PRINT ''
PRINT 'Finished CreateXLS script at '+RTRIM(CONVERT(varchar(24),GETDATE(),121))+' '
GO

This example is useful for loading data from an OLEDB source into a dynamically created Excel file.

NOTE:
This is the core functionality. Things like logging, checkpointing, documentation, etc., are at the user’s discretion.

Steps:
1. Click on package properties. Set «DelayValidation» property to True.
The package will not validate tasks, connections, until they are executed.

2. Create a package level variable «XLFileRootDir» as string and set it to the root
directory where you want the excel file to be created.
Example: C:\ProjectData

3. Create an Excel connection in the connection manager. Browse to the target directory
and select the destination XL filename or type it in. It doesn’t matter if the file doesn’t exist.

4. Go to the Excel connection properties and expand the expressions ellipse (The button
with «…» on it).
Under the property drop down, select ‘ExcelFilePath‘ and click on the ellipse to
configure the expression:
@[User::XLFileRootDir] + (DT_WSTR, 2) DATEPART(«DD», GETDATE()) + (DT_WSTR, 2) DATEPART(«MM», GETDATE()) + (DT_WSTR, 4) DATEPART(«YYYY», GETDATE()) +».xls»
This should create an xl file like 01132007.xls.

5. Add a SQL task to package and double click to edit.
In the general tab, set ‘ConnectionType‘ to ‘Excel’.
For ‘SQLStatement‘, enter the create table SQL to create destination table.
For example:
CREATE TABLE `Employee List` (
    `EmployeeId` INTEGER,
    `EmployeeName` NVARCHAR(20)
)

Copy the create table command. It will come in handy later.

6. Add a Data Flow task. In the data flow editor, add an OLEDB source and an Excel destination.
Configure the source to select EmployeeId and EmployeeName from a table.

7. Connect this to Excel destination. In the destination editor, select the Excel connection in the
manager, choose ‘table or view’ for data access mode and for ‘name of the Excel sheet’ click on
new button and paste the create table command from Step 5.
Map the columns appropriately in the mappings tab and you are done.

Let me know if you have any questions.

Провайдеры данных

Для работы с Excel 2003 (.Xls) можно использовать провайдер Microsoft Jet OLE DB 4.0.

SELECT * FROM OPENROWSET(
	'Microsoft.Jet.OLEDB.4.0', 
	'Excel 12.0;Database=d:tmpTimeSheets.xlsx;HDR=YES;IMEX=1', 
	'SELECT * FROM [Sheet1$]');

Для работы с Excel 2007 (.Xlsx) — Microsoft ACE OLEDB 12.0.

SELECT * FROM OPENROWSET (
    'Microsoft.ACE.OLEDB.12.0',
    'Excel 12.0;Database=d:tmpTimeSheets.xlsx;HDR=YES;IMEX=1',
    'SELECT * FROM [Sheet1$]');

В Windows 10 открыть настройки источников данных ODBC можно написав «Источники данных ODBC» или через Панель управления Администрирование.

Extended Properties

  • HDR=YES|NO. HDR=YES означает, что первую строку листа, следует рассматривать как заголовки колонок. Т.о. значение из первой строки можно использовать как имена полей в sql запросах (любых: select, insert, update, delete).
  • IMEX=1|3. 1 — открыть соединение для чтения. 3 — для записи.

Создание Linked Server в Sql Server для доступа к Excel

EXEC sp_addLinkedServer
    @server= N'XLSX_2010',
    @srvproduct = N'Excel',
    @provider = N'Microsoft.ACE.OLEDB.12.0',
    @datasrc = N'd:tmpTimeSheets.xlsx',
    @provstr = N'Excel 12.0; HDR=Yes';
GO

После создания связанного сервера можно будет просмотреть имена доступных листов.

Затем, чтобы обратиться к сервису:

SELECT * FROM OPENQUERY (XLSX_2010, 'Select * from [Sheet1$]')
или
SELECT * FROM [XLSX_2010]...[Лист1$]

Обращение к лиcтам, диапазонам, полям

Для обращения к листу из SQL запроса нужно использовать имя листа, например: [Sheet1$] или [Лист1$]. Обращение к диапазону: [Sheet1$A16:F16].

Вставка данных в произвольное место

Примеры указания диапазона при вставке строк (insert)

  • [table1$B4:E20]
  • [table1$S4:U]
  • [table1$F:G]

При вставке должны выполняться следующие условия:

  • Первая строчка указанного диапазона дожна входить в диапазон ячеек с данными. Чтобы создать на листе диапазон с данными достаточно в углах некоторого прямоугольного диапазона (в левом верхнем и правом нижнем) вписать значение (C4:I7 на скриншоте). Т.е. сама первая строчка указанного в insert диапазона данные содержать не обязана, достаточно, чтобы она просто входила в такой диапазон. Иначе возникнет ошибка "This table contains cells that are outside the range of cells defined in this spreadsheet"
  • Хвост диапазона должен содержать пустые строки (хотя бы одну).

Пример: Дан лист, где заполнены только 2 ячейки: C4, I7. После выполнения команды INSERT INTO [table1$E6:G] VALUES(2, 'FF','2014-01-03') результат будет как на скриншоте. Поясним: строка E6:G6 является первой строкой диапазона для вставки. Она входит в заполненный диапазон C4:I7. Поэтому данные были вставлены на следующей пустой строке — 8. Из этого примера становится ясно, что через OleDb нельзя работать с несколькими независимыми диапазонами на одном листе, используя вставку (update будет работать).

Insert

Ошибки

  • System.Data.OleDb.OleDbException (0x80004005): Operation must use an updateable query. Соединение открыто для чтение, при этом происходит попытка внести изменения (выполнить insert, update или delete). Решение: открыть соединение для записи, установив свойство провайдера в строке соединения IMEX=3 (см. выше).
  • System.Data.OleDb.OleDbException (0x80004005): "This table contains cells that are outside the range of cells defined in this spreadsheet". Такая ошибка возникает при подпытке обновить (update) или вставить (insert) значения в диапазоне, в котором отсутствуют данные на листе.
    1. Если нужно произвести запись в определенные ячейки инструкцией update, то

Ссылки

  • https://www.codeproject.com/Tips/705470/Read-and-Write-Excel-Documents-Using-OLEDB
  • https://stackoverflow.com/questions/36987636/cannot-create-an-instance-of-ole-db-provider-microsoft-jet-oledb-4-0-for-linked
  • https://stackoverflow.com/questions/26267224/the-ole-db-provider-microsoft-ace-oledb-12-0-for-linked-server-null
  • http://www.ashishblog.com/importexport-excel-xlsx-or-xls-file-into-sql-server/
  • https://yoursandmyideas.com/2011/02/05/how-to-read-or-write-excel-file-using-ace-oledb-data-provider/
  • https://stackoverflow.com/questions/46373895/how-to-open-a-huge-excel-file-efficiently Несколько способов открыть большой Excel файл, в т.ч. с помощью OleDb.

Like this post? Please share to your friends:
  • Using narrator with word
  • Using names in excel
  • Using name in excel
  • Using multiple if in excel formula
  • Using module in excel