Inserting a cell range from an Excel sheet as an AutoCAD table using .NET

I wasn't planning on covering this particular topic today, but then this comment came in overnight and I ended up taking a look into it. Paul has been trying to append a specific cell range to his connection string when creating a data-link for an Excel spreadsheet, adding to the code from this previous post.

I gave it a try myself with a hardcoded cell range and it seemed to work fine, and so went ahead and modified the implementation of the TFS command to ask the user to enter their own cell range. What's probably most interesting about the code in this post is its use of a regular expression โ€“ something I don't use often enough, in truth โ€“ which is a great way of validating that strings conform to a particular pattern.

It's also possible to apply group labels within a regex to tag groups of characters for later extraction and validation. We use this technique to check that the row numbers are both greater than 0, for instance ("A0:B3" and "A10:B0" are both invalid cell ranges that otherwise meet the pattern we define).

There may well be a better way to do this within a regex โ€“ as I've said, I don't use them as often as I should โ€“ but hopefully the overall technique of using them will be a helpful reminder for people.

On a more general note, this kind of input validation is extremely important if you're asking for user input and then using it to build database queries: if you don't protect against arbitrary strings being entered then your application will be wide open to SQL Injection attacks. This isn't something that's as important with desktop applications accessing local databases, perhaps, but as applications move more to the web this becomes absolutely critical (as the unfortunate autocomplete snafu on healthcare.gov highlighted a couple of months ago).

Here's the updated C# code... the TFS command is the only section that's had significant changes from the previous version, in case:

using System;

using System.Collections.Generic;

using System.Text.RegularExpressions;

using Autodesk.AutoCAD.ApplicationServices;

using Autodesk.AutoCAD.DatabaseServices;

using Autodesk.AutoCAD.EditorInput;

using Autodesk.AutoCAD.Runtime;

using Autodesk.AutoCAD.Windows;

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 (Autodesk.AutoCAD.Runtime.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;

 

      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.Upg
radeOpen();

 

            // 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 (Autodesk.AutoCAD.Runtime.Exception ex)

        {

          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

        );

 

      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)

      {

     
60;  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 the user to select a range of cells in the spreadsheet

 

      // We'll use a Regular Expression that matches a column (with

      // one or more letters) followed by a numeric row (which we're

      // naming "row1" so we can validate it's > 0 later),

      // followed by a colon and then the same (but with "row2")

 

      const string rangeExp =

        "^[A-Z]+(?<row1>[0-9]+):[A-Z]+(?<row2>[0-9]+)$";

      bool done = false;

      string range = "";

 

      do

      {

        var psr = ed.GetString("\nEnter cell range <entire sheet>");

        if (psr.Status != PromptStatus.OK)

          return;

 

        if (String.IsNullOrEmpty(psr.StringResult))

        {

          // Default is to select entire sheet

 

          done = true;

        }

        else

        {

          // If a string was entered, make sure it's a

          // valid cell range, which means it matches the

          // Regular Expression and has positive (non-zero)

          // row numbers

 

          var m =

            Regex.Match(

              psr.StringResult, rangeExp, RegexOptions.IgnoreCase

            );

          if (

            m.Success &&

            Int32.Parse(m.Groups["row1"].Value) > 0 &&

            Int32.Parse(m.Groups["row2"].Value) > 0

          )

          {

            done = true;

            range = psr.StringResult.ToUpper();           

          }

          else

          {

            ed.WriteMessage("\nInvalid range, please try again.");

          }

        }

      } while (!done);

 

      // Ask for the insertion point of the table

 

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

      if (ppr.Status != PromptStatus.OK)

        return;

 

      try

      {

        // Remove any Data Link, if one exists already

 

        var dlm = db.DataLink
Manager;

        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] +

          (String.IsNullOrEmpty(range) ? "" : "!" + range);

        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 =

            (BlockTableRecord)tr.GetObject(

              db.CurrentSpaceId,

              OpenMode.ForWrite

            );

 

          btr.AppendEntity(tb);

          tr.AddNewlyCreatedDBObject(tb, true);

          tr.Commit();

        }

      }

      catch (Autodesk.AutoCAD.Runtime.Exception ex)

      {

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

      }

    }

  }

}

17 responses to “Inserting a cell range from an Excel sheet as an AutoCAD table using .NET”

  1. Hi Kean,

    nice post, it's always interesting to see out what AutoCAD is capable of!

    You could simplify your code a bit by using this Regex-pattern: "^[A-Z]+([1-9][0-9]*):[A-Z]+([1-9][0-9]*)$"
    It will make sure that no invalid ranges are entered, so you don't have to check afterwards ๐Ÿ™‚

    Best Regards

    Christian

  2. Hi Christian,

    Perfect - thanks. It's obvious in hindsight. ๐Ÿ™‚

    I'll leave the post as is - I think the ability to extract grouped characters will also be of use to people, even if it's a little more longwinded for this particular scenario.

    Best regards,

    Kean

  3. Kean,

    Thanks for this (especially the regular expression, which I had not used in practice since using them in school). It turns out there's something funky with my Excel sheet where I can't use the "A1" cell for this. I don't know why yet, but other than that this works great.

    Thanks,

    Paul

  4. Hi Kean,

    Wonder how you are keep posting different categories of posts at the same time when someone is really looking for that.

    This post and removing format from pasted excel sheet today helped/saved a lot of time for me.

    Also you were right on spot when we were struggling with DGNPurge.

    Thanks !!

  5. My pleasure! ๐Ÿ™‚

    Kean

  6. I know I'm a bit late to the party, but just a comment: I've taken to using the OpenXML SDK rather than the Excel Object Model. Don't ask me why, just appeals as it seems more direct and versatile going forward. works for me. Dale

  7. I am curious as to how the code could be altered to allow a selection set to update multiple tables in a drawing versus just a single entity. The multiple ways i have attempted results in memory errors

    1. I'm not sure what's not working: if you just want the user to be able to select multiple tables - and update them all - then it should be a (simple?) matter of iterating the selection set and updating each one.

      You might try refactoring the code to create a function that updates a single table and then call that from your loop.

      Kean

  8. Is there an easy to increment a new datalink. With every next selected sheet?

    1. I don't see anything complicated (although I don't fully understand what you're hoping to achieve). If you can't modify the code here to do what you want, I suggest posting to the AutoCAD .NET Discussion Group: forums.autodesk.com/

      Kean

  9. Hey Kean, do I just copy paste this code after importing the libraries to a windos form application? I don't undesrtand how to implement this to vsC#

    1. Hi Victor,

      Build it into a Class Library, adding the appropriate project references (most easily done via NuGet). Then you can NETLOAD the resultant DLL into AutoCAD and call the commands.

      I hope this helps,

      Kean

      1. Oh, thank you very much Mr. Kean, this blog is of great help and such great programs and ideas, nice job mate.
        Regars Victor

        1. Thanks! ๐Ÿ™‚

          Kean

      2. Oh, just one more question Mr. Kean, I need to download the objectArx to get the autocad libraries, right?

  10. This is totally awesome!

Leave a Reply to AJILAL VIJAYAN Cancel reply

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