Inserting a specific Excel sheet as an AutoCAD table using .NET

Last week I received the following question from Adam Schilling:

I have enjoyed your posts on .net programming for datalinks. I have searched high and low and haven't been able to find answers or any support to help me with a small issue. 

My code (much of which was based off of your posts from 2007) works fine, except I cannot get it to use a different sheet from the workbook that is select.  Since the option to select a specific sheet is available when adding a link manually, I would think that it would be possible to programmatically do it also.

If it would work for a post great, if not, I apologize for taking your time.

Seeing as Adam asked so nicely (and I found the question to be of broad interest, which is also important), I went ahead and dusted off the 6-year old posts that he'd referred to. [Has it really been that long since I wrote them? Sigh.]

Here's the series that I've refreshed for the purposes of this post:

  1. Creating an AutoCAD table linked to an Excel spreadsheet using .NET
  2. Updating an AutoCAD table linked to an Excel spreadsheet using .NET
  3. Updating an Excel spreadsheet from a linked AutoCAD table using .NET

It's really the code from the first that has been modified to allow selection of a specific sheet, but the code was all in the same file (and the compiler warned of some obsolete functions), so I decided to update the other commands, too, rather than stripping them out.

The quick answer to Adam's question is that you can specify a particular sheet to link to by passing its name (using the "!" character as a separator) into the DataLink's ConnectionString property. e.g.:

dl.ConnectionString = "spreadsheet.xlsx!Sheet2";

That's really all there is to it. But that would make for a pretty short post, so I went ahead and added some code that, for a selected spreadsheet, presents the list of contained sheets to the user and asks for one to be selected.

While not really being what the post is about – the main point is to show the AutoCAD side of things – there are a few different ways to access the contents of an Excel spreadsheet from .NET.

The one I'd personally have preferred to have used is the OleDb provider for Office. The problem with that approach relates to getting the right version of the component working for your app, even though 32- and 64-bit versions are both available (at least for Office 2010, for Office 2007 there's just the 32-bit version).

Here's my own situation, as an example: I'm running 32-bit Office on a 64-bit system (the main reason being that's the way our IT department supports it, as far as I can tell), which means the installer doesn't let me install the 64-bit component (probably because it needs to match the underlying Office version, which I suppose is fair enough). But while I can install the 32-bit version, my .NET application inside AutoCAD needs to be either x64 or "Any CPU", and so leads to the "The 'Microsoft.ACE.OLEDB.12.0' provider is not registered on the local machine" exception being thrown when the connection attempt is made.

Which led me back to using the COM library for Excel, instead (for which I added a COM project reference to the "Microsoft Excel 14.0 Object Library"). Using this component actually fires up an instance of Excel in the background – which certainly seems like overkill just to access the list of sheets contained in a spreadsheet – but it's reliable and easy to get working. And the code is certainly more readable.

Speaking of the code, here's the updated C# code letting a user select a specific Excel sheet for insertion (in the TFS command):

using Autodesk.AutoCAD.ApplicationServices;

using Autodesk.AutoCAD.DatabaseServices;

using Autodesk.AutoCAD.EditorInput;

using Autodesk.AutoCAD.Runtime;

using Autodesk.AutoCAD.Windows;

using System.Collections.Generic;

using Excel = Microsoft.Office.Interop.Excel;

 

namespace LinkToExcel

{

  public class Commands

  {

    [CommandMethod("S2T")]

    static public void UpdateTableFromSpreadsheet()

    {

      var doc =

        Application.DocumentManager.MdiActiveDocument;

      var db = doc.Database;

      var ed = doc.Editor;

 

      var opt = new PromptEntityOptions("\nSelect table to update");

      opt.SetRejectMessage("\nEntity is not a table.");

      opt.AddAllowedClass(typeof(Table), false);     

 

      var per = ed.GetEntity(opt);

      if (per.Status != PromptStatus.OK)

        return;

 

      using (var tr = db.TransactionManager.StartTransaction())

      {

        try

        {

          var obj = tr.GetObject(per.ObjectId, OpenMode.ForRead);

          var tb = obj as Table;

 

          // It should always be a table

          // but we'll check, just in case

 

          if (tb != null)

          {

            // The table must be open for write

 

            tb.UpgradeOpen();

 

            // Update data link from the spreadsheet

 

            var dlIds = tb.Cells.GetDataLink();

 

            foreach (ObjectId dlId in dlIds)

            {

              var dl =

                (DataLink)tr.GetObject(dlId, OpenMode.ForWrite);

              dl.Update(

                UpdateDirection.SourceToData,

                UpdateOption.None

              );

 

              // And the table from the data link

 

              tb.UpdateDataLink(

                UpdateDirection.SourceToData,

                UpdateOption.None

              );

            }

          }

          tr.Commit();

          ed.WriteMessage(

            "\nUpdated the table from the spreadsheet."

          );

        }

        catch (Exception ex)

        {

          ed.WriteMessage(

            "\nException: {0}",

            ex.Message

          );

        }

      }

    }

 

    [CommandMethod("T2S")]

    static public void UpdateSpreadsheetFromTable()

    {

      var doc =

        Application.DocumentManager.MdiActiveDocument;

      var db = doc.Database;

      var ed = doc.Editor;

 

      var opt =

        new PromptEntityOptions(

          "\nSelect table with spreadsheet to update"

        );

      opt.SetRejectMessage(

        "\nEntity is not a table."

      );

      opt.AddAllowedClass(typeof(Table), false);

 

      var per = ed.GetEntity(opt);

      if (per.Status != PromptStatus.OK)

        return;

 

      Transaction tr =

        db.TransactionManager.StartTransaction();

      using (tr)

      {

        try

        {

          DBObject obj =

            tr.GetObject(per.ObjectId, OpenMode.ForRead);

          Table tb = obj as Table;

 

          // It should always be a table

          // but we'll check, just in case

 

          if (tb != null)

          {

            // The table must be open for write

 

            tb.UpgradeOpen();

 

            // Update the data link from the table

 

            tb.UpdateDataLink(

              UpdateDirection.DataToSource,

              UpdateOption.ForceFullSourceUpdate

            );

 

            // And the spreadsheet from the data link

 

            var dlIds = tb.Cells.GetDataLink();

            foreach (ObjectId dlId in dlIds)

            {

              var dl =

                (DataLink)tr.GetObject(dlId, OpenMode.ForWrite);

              dl.Update(

                UpdateDirection.DataToSource,

                UpdateOption.ForceFullSourceUpdate

              );

            }

          }

          tr.Commit();

 

          ed.WriteMessage(

            "\nUpdated the spreadsheet from the table."

          );

        }

        catch (Exception ex)

        {

     &
#160;    ed.WriteMessage(
"\nException: {0}", ex.Message);

        }

      }

    }

 

    static public List<string> GetSheetNames(string excelFileName)

    {

      var listSheets = new List<string>();

 

      var excel = new Excel.Application();

      var wbs = excel.Workbooks.Open(excelFileName);

      foreach (Excel.Worksheet sheet in wbs.Worksheets)

      {

        listSheets.Add(sheet.Name);

      }

      excel.Quit();

 

      return listSheets;

    }

 

    [CommandMethod("TFS")]

    static public void TableFromSpreadsheet()

    {

      const string dlName = "Import table from Excel demo";

 

      var doc =

        Application.DocumentManager.MdiActiveDocument;

      var db = doc.Database;

      var ed = doc.Editor;

 

      // Ask the user to select an XLS(X) file

 

      var ofd =

        new OpenFileDialog(

          "Select Excel spreadsheet to link",

          null,

          "xls; xlsx",

          "ExcelFileToLink",

          OpenFileDialog.OpenFileDialogFlags.

            DoNotTransferRemoteFiles

  &#
160;     );

 

      var dr = ofd.ShowDialog();

 

      if (dr != System.Windows.Forms.DialogResult.OK)

        return;

 

      // Display the name of the file and the contained sheets

 

      ed.WriteMessage(

        "\nFile selected was \"{0}\". Contains these sheets:",

        ofd.Filename

      );

 

      // First we get the sheet names

 

      var sheetNames = GetSheetNames(ofd.Filename);

 

      if (sheetNames.Count == 0)

      {

        ed.WriteMessage(

          "\nWorkbook doesn't contain any sheets."

        );

        return;

      }

 

      // And loop through, printing their names

 

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

      {

        var name = sheetNames[i];

 

        ed.WriteMessage("\n{0} - {1}", i + 1, name);

      }

 

      // Ask the user to select one

 

      var pio = new PromptIntegerOptions("\nSelect a sheet");

      pio.AllowNegative = false;

      pio.AllowZero = false;

      pio.DefaultValue = 1;

      pio.UseDefaultValue = true;

      pio.LowerLimit = 1;

      pio.UpperLimit = sheetNames.Count;

 

      var pir = ed.GetInteger(pio);

      if (pir.Status != PromptStatus.OK)

        return;

 

      // Ask for the insertion point of the table

 

      var ppr = ed.GetPoint("\nEnter table insertion point");

      if (ppr.Status != PromptStatus.OK)

        return;

 

      // Remove any Data Link, if one exists already

 

      var dlm = db.DataLinkManager;

      var dlId = dlm.GetDataLink(dlName);

      if (dlId != ObjectId.Null)

      {

        dlm.RemoveDataLink(dlId);

      }

 

      // Create and add the new Data Link, this time with

      // a direction connection to the selected sheet

 

      var dl = new DataLink();

      dl.DataAdapterId = "AcExcel";

      dl.Name = dlName;

      dl.Description = "Excel fun with Through the Interface";

      dl.ConnectionString =

        ofd.Filename + "!" + sheetNames[pir.Value - 1];

      dl.DataLinkOption =

        DataLinkOption.PersistCache;

      dl.UpdateOption |=

        (int)UpdateOption.AllowSourceUpdate;

 

      dlId = dlm.AddDataLink(dl);

 

      using (var tr = doc.TransactionManager.StartTransaction())

      {

        tr.AddNewlyCreatedDBObject(dl, true);

 

        var bt =

          (BlockTable)tr.GetObject(

            db.BlockTableId,

            OpenMode.ForRead

          );

 

        // Create our table

 

        var tb = new Table();

        tb.TableStyle = db.Tablestyle;

        tb.Position = ppr.Value;

        tb.Cells.SetDataLink(dlId, true);

        tb.GenerateLayout();

 

        // Add it to the drawing

 

        var btr =

      
0;   (
BlockTableRecord)tr.GetObject(

            db.CurrentSpaceId,

            OpenMode.ForWrite

          );

 

        btr.AppendEntity(tb);

        tr.AddNewlyCreatedDBObject(tb, true);

        tr.Commit();

      }

    }

  }

}

When we run the TFS command, we see the user gets presented with the usual file selection dialog, but then a command-line interface for choosing a specific sheet from the selected spreadsheet:

Command: TFS

File selected was "C:\Data\Spreadsheet.xlsx". Contains these sheets:

1 - Sheet1

2 - Sheet2

3 - Sheet3

Select a sheet <1>: 2

Enter table insertion point:

Which results in the specified sheet getting inserted as a table into the current AutoCAD drawing.

10 responses to “Inserting a specific Excel sheet as an AutoCAD table using .NET”

  1. Keen,
    We've stopped using the Excel objects to read Excel files by using the Open XML SDK 2.0 for Microsoft Office.

    This way you don't even need Excel installed on a machine to get to Excel data (in xlsx format).

    Takes a little more work setting up but sure is nice not having to worry about versions anymore.

    Thank,
    Mike

  2. Kean Walmsley Avatar

    Mike,

    Great - glad to know there's a better way. 🙂

    Cheers,

    Kean

  3. Kean,
    I have used this method to success except for one thing, I can't specify a "range" of cells for the datalink. Trying to do the same ! separator followed by a range (e.g., A1:D10) it always throws a "eDataLinkBadConnectionString" error.

    I have made a little function that tells me the connection strings of the all of the present data links. Interestingly, if I manually create a datalink from Excel with a specified range instead of "Entire Sheet" it looks exactly as I was making my own connection string (e.g., C:\file.xls!Sheet1!A1:D10).

    This leaves me questioning why the heck I can't make it do this behavior programmatically.

  4. Paul,

    I just tried modifying the sample to add a hardcoded range to the DataLink and it worked for me. Here's the modified code from the TFS command:

    dl.ConnectionString =
    ofd.Filename + "!" + sheetNames[pir.Value - 1] + "!A1:B8";

    I'm working with AutoCAD 2014, in case.

    Regards,

    Kean

  5. Hi Kean,

    Nice post, as MikeR said about using XML in order to obtain data from Excel files, this is a link to a good MSDN article on that: msdn.microsoft.com/en-us/library/hh370976%28v=office.14%29.aspx

    Regards,

    Gaston Nunez

  6. Hi Gaston,

    Thanks!

    (And sorry for the delayed reply - this ended up in my spam folder for some reason.).

    Regards,

    Kean

  7. Dear Mr. Kean,

    i am aware that this post was created too long ago but i am gettin same problem as Mr.Paul had below.

    dl.ConnectionString = ofd.Filename & "!" & sheetNames(a) & "!B11:T25"

    also gives the badconnectionstring exception when i try to get the range strings with below code:

    Public Shared Function GetRanges(excelFileName As String)
    Dim listRanges As List(Of String) = New List(Of String)
    Dim excel As Excel.Application = New Excel.Application()
    Dim wbs As Excel.Workbook = excel.Workbooks.Open(excelFileName)
    For Each sheet As Excel.Worksheet In wbs.Worksheets
    If Right(sheet.Name, 6) = "tekhat" Then
    Dim wbilk As String = sheet.Range("b11", sheet.Range("b11").End(Microsoft.Office.Interop.Excel.XlDirection.xlDown).End(Microsoft.Office.Interop.Excel.XlDirection.xlToRight)).Address.ToString()
    Dim wb2 As String = wbilk.Replace("$", "")
    listRanges.Add(wb2)
    End If
    Next
    excel.Quit()
    Return listRanges
    End Function

    i saw your other post which you ask to user to input the range strings. when i compile your code it works.

    1. Dear Kenan,

      I'm afraid I don't have time to provide support. I see that you've posted your code to the AutoCAD .NET discussion group - which is absolutely the right thing to do - hopefully someone there will be able to help.

      Regards,

      Kean

      1. Dear Kean,
        Thank you for replying and the good work you are doing.

  8. Man u r great .....

Leave a Reply to Kean Walmsley Cancel reply

Your email address will not be published. Required fields are marked *