3

I am new-ish to C# programming (and programming in general) but I'm getting my feet wet with AutoCAD development using the AutoDesk .NET API for projects at work.

There are certain repetitive tasks in AutoCAD dev that I've been creating helper methods for to simplify my code. In order to create an object(lines, polylines, annotation, etc...) in AutoCAD through the .API, the programmer has to write a fairly convoluted statement that accesses the AutoCAD environment, gets the current drawing, gets the database of the current drawing file, starts a transaction with the database, //do work, then append the created entities to the database before finally committing and closing the transaction.

So I wrote the following code to simplify this task:

public static void CreateObjectActionWithinTransaction(Action<Transaction, Database, BlockTable, BlockTableRecord> action)
{
    var document = Application.DocumentManager.MdiActiveDocument;
    var database = document.Database;
    using (var transaction = document.TransactionManager.StartTransaction())
    {
        BlockTable blocktable = transaction.GetObject(database.BlockTableId, OpenMode.ForRead) as BlockTable;
        BlockTableRecord blockTableRecord = transaction.GetObject(blocktable[BlockTableRecord.ModelSpace], OpenMode.ForWrite) as BlockTableRecord;
        action(transaction, database, blocktable, blockTableRecord);

        transaction.Commit();
    }
}

Then my Lambda expression that creates a generic MText and sets up some parameters for it:

public static void createMtext(Point3d location, AttachmentPoint attachmentpoint, string contents, double height, short color, bool usebackgroundmask, bool usebackgroundcolor, double backgroundscale)
{
    CreateObjectActionWithinTransaction((transaction, database, blocktable, blocktablerecord) =>
    {
        MText mt = new MText();
        mt.SetDatabaseDefaults();
        mt.Location = location;
        mt.Attachment = attachmentpoint;
        mt.Contents = contents;
        mt.Height = height;
        mt.Color = Color.FromColorIndex(ColorMethod.ByAci, color);
        mt.BackgroundFill = usebackgroundmask;
        mt.UseBackgroundColor = usebackgroundcolor;
        mt.BackgroundScaleFactor = backgroundscale;
        blocktablerecord.AppendEntity(mt);
        transaction.AddNewlyCreatedDBObject(mt, true);
    });
}

And then finally, when I'm actually creating MText somewhere, I can create it in one line, and pass in values for all the parameters without having to write out the huge transaction code for it:

Helpers.createMtext(insertpoint, AttachmentPoint.MiddleLeft, "hello world", .08, colors.AutoCAD_Red, true, true, 1.2);

So this is great and it works when I want to create an MText by itself and put it somewhere. However, there are certain other situations where instead of just creating an MText and placing it in the drawing, I want to create an MText using the same basic premise as above, but return it as a value to be used somewhere else.

AutoCAD has annotation objects called Multileaders which are essentially just an MText just like above, but attached to some lines and an arrow to point at something in the drawing. In the API you need to define an MText and attach it to the Multileader object. However my above code can't be used because it doesn't return anything.

So my question boils down to, how can I create a method like above to create an object, but instead of just creating that object, have it return that object to be used by another piece of code?

Also are there any good resources for beginners on Lambda expressions? Books, websites, YouTube?

Uwe Keim
  • 39,551
  • 56
  • 175
  • 291
Kefka
  • 89
  • 7

3 Answers3

2

For the AutoCAD part:

As Miiir stated in the comment, do not return an object, but rather ObjectId. An object instance belongs to a transaction, so if you open the object using some transaction, commit the transaction and try and use that object in another transaction, AutoCAD will basically just crash.

Working with AutoCAD API always follows this basic pattern:

  1. Start transaction
  2. Create new object or use transaction to get an existing object. This is done by either having an ObjectID or by looping over tables and looking for whatever attributes you are interested in (i.e. BlockTable, BlockTableRecord, LayerTable, etc.)
  3. Do stuff to the object.
  4. Commit or Abort transaction.

If you try and bypass steps 1 and 2, it will not work out so well. So, return ObjectID, and then use the id to get the object in another transaction.

As for the C# part:

If you are looking to return a value using a delegate, Action<T> is not your friend. Action does not return a value, it only "acts", thus the name. If you want to use a delegate to return a value you have 2 options:

  1. Define a custom delegate type.

  2. Use the generic delegate supplied by the .NET framework Func<T1,T2,T3,T4,TResult>.

Which one should you use? In your case, I'd probably go with option 1, for the simple reason that your code will just be much cleaner as easier to maintain. I will use that in this example. Using Func would work the exact same way, except your function signatures would look a bit ugly.

Custom delegate:

//somewhere in your code inside a namespace (rather than a class)
public delegate ObjectId MyCreateDelegate(Transaction transaction, Database db,
         BlockTable blockTable, BlockTableRecord blockTableRecord);

Then your general method

public static ObjectId CreateObjectActionWithinTransaction(MyCreateDelegate createDel)
{
    ObjectId ret;
    var document = Application.DocumentManager.MdiActiveDocument;
    var database = document.Database;
    using (var transaction = document.TransactionManager.StartTransaction())
    {
        BlockTable blocktable = transaction.GetObject(database.BlockTableId, OpenMode.ForRead) as BlockTable;
        BlockTableRecord blockTableRecord = transaction.GetObject(blocktable[BlockTableRecord.ModelSpace], OpenMode.ForWrite) as BlockTableRecord;
        //here createMtext will get called in this case, and return ObjectID
        ret = createDel(transaction, database, blocktable, blockTableRecord);
        transaction.Commit();
    }
    return ret;
}

and the specific method with the lambda:

public ObjectId createMtext(Point3d location, AttachmentPoint attachmentpoint, string contents, double height, short color, bool usebackgroundmask, bool usebackgroundcolor, double backgroundscale)
{
    //here you can return the result the general function
    return CreateObjectActionWithinTransaction((transaction, database, blocktable, blocktablerecord) =>
    {
        MText mt = new MText();
        mt.SetDatabaseDefaults();
        mt.Location = location;
        mt.Attachment = attachmentpoint;
        mt.Contents = contents;
        mt.Height = height;
        mt.Color = Color.FromColorIndex(ColorMethod.ByAci, color);
        mt.BackgroundFill = usebackgroundmask;
        mt.UseBackgroundColor = usebackgroundcolor;
        mt.BackgroundScaleFactor = backgroundscale;
        blocktablerecord.AppendEntity(mt);
        transaction.AddNewlyCreatedDBObject(mt, true);
        //make sure to get ObjectId only AFTER adding to db.
        return mt.ObjectId;
    });
}

And lastly, use it like this

ObjectId mtId = Helpers.createMtext(insertpoint, AttachmentPoint.MiddleLeft, "hello world", .08, colors.AutoCAD_Red, true, true, 1.2);
//now use another transaction to open the object and do stuff to it.

Learning Resources:

And lastly, to understand lambda expressions, you need to start with understanding delegates, if you don't already. All lambdas are is syntactic sugar for instantiating a delegate object that either points to a method or an anonymous method as you've done in your example. This tutorial looks pretty good. And remember, delegates such as Action, Func and Predicate, or no different. So whether you define your own delegate or use the out-of-the box solution, lambda expressions do not care.

For a lambda overview, check out this tutorial.

Do not limit yourself to the two source I provided. Just Google it, and the top 10 hits will all be fairly good information. You can also check out Pluralsight. I do a lot of my learning there.

Nik
  • 1,780
  • 1
  • 14
  • 23
2

Instead of using delegates, I'd rather use extension methods called from within a transaction in the calling method.

static class ExtensionMethods
{
    public static BlockTableRecord GetModelSpace(this Database db, OpenMode mode = OpenMode.ForRead)
    {
        var tr = db.TransactionManager.TopTransaction;
        if (tr == null)
            throw new Autodesk.AutoCAD.Runtime.Exception(ErrorStatus.NoActiveTransactions);
        return (BlockTableRecord)tr.GetObject(SymbolUtilityServices.GetBlockModelSpaceId(db), mode);
    }

    public static void Add(this BlockTableRecord btr, Entity entity)
    {
        var tr = btr.Database.TransactionManager.TopTransaction;
        if (tr == null)
            throw new Autodesk.AutoCAD.Runtime.Exception(ErrorStatus.NoActiveTransactions);
        btr.AppendEntity(entity);
        tr.AddNewlyCreatedDBObject(entity, true);
    }

    public static MText AddMtext(this BlockTableRecord btr, 
        Point3d location, 
        AttachmentPoint attachmentpoint, 
        string contents, 
        double height, 
        short color = 256, 
        bool usebackgroundmask = false, 
        bool usebackgroundcolor = false, 
        double backgroundscale = 1.5)
    {
        MText mt = new MText();
        mt.SetDatabaseDefaults();
        mt.Location = location;
        mt.Attachment = attachmentpoint;
        mt.Contents = contents;
        mt.Height = height;
        mt.ColorIndex = color;
        mt.BackgroundFill = usebackgroundmask;
        mt.UseBackgroundColor = usebackgroundcolor;
        mt.BackgroundScaleFactor = backgroundscale;
        btr.Add(mt);
        return mt;
    }
}

Using example:

        public static void Test()
    {
        var doc = AcAp.DocumentManager.MdiActiveDocument;
        var db = doc.Database;
        using (var tr = db.TransactionManager.StartTransaction())
        {
            var ms = db.GetModelSpace(OpenMode.ForWrite);
            var mt = ms.AddMtext(Point3d.Origin, AttachmentPoint.TopLeft, "foobar", 2.5);
            // do what you want with 'mt'
            tr.Commit();
        }
    }
gileCAD
  • 2,295
  • 1
  • 10
  • 10
0

I am not familiar with AutoCad API, but it appears that "transaction.Commit()" is the line that actually performs the action of placing the MText on your model.

if this is the case; i would do something like the following:

public MText CreateMTextObject({parameters})
{
//code
  Return textObject
}

public PlaceTextObject({parameters})
{
  CreateTextObject(parameters).Commit()
}

This way, you can choose to keep the textobject for further manipulation, while still allowing the option to apply it in one go. It will also have only one codeblock for making the object, making sure that there are no differences in implementation between the two methods

ThisIsMe
  • 274
  • 1
  • 5
  • You should always return the ObjectId of the entity, not the entity itself. – Miiir Dec 12 '18 at 15:24
  • Thanks I will try this out in a little while, with MiiIr's suggestion to return the objectid and let you all know if it works. – Kefka Dec 12 '18 at 15:48