Update XRecord when entity is modified

Update XRecord when entity is modified

CJModis
Advocate Advocate
4,117 Views
12 Replies
Message 1 of 13

Update XRecord when entity is modified

CJModis
Advocate
Advocate

I want to discuss the issue in theory

Is it possible to update entity xrecord (or xdata), which depends on the geometry of the entity?
Take for example the line. I get the coordinates of the line center and put the value into the xrecord of the entity. If I create GripOverrule, then in the MoveGripPointsAt method, I can update the xrecord (recalculate center point and put in xrecord). But what if the primitive is modified by the normal functions of the autocad? For example, move, copy, etc.

0 Likes
Accepted solutions (1)
4,118 Views
12 Replies
Replies (12)
Message 2 of 13

ActivistInvestor
Mentor
Mentor

@CJModis wrote:

I want to discuss the issue in theory

Is it possible to update entity xrecord (or xdata), which depends on the geometry of the entity?
Take for example the line. I get the coordinates of the line center and put the value into the xrecord of the entity. If I create GripOverrule, then in the MoveGripPointsAt method, I can update the xrecord (recalculate center point and put in xrecord). But what if the primitive is modified by the normal functions of the autocad? For example, move, copy, etc.


You can use an ObjectOverrule to update your xdata/xrecord/etc. whenever the object is modified (by any means). Override the ObjectOverrule's Close() method, and update your data there. You should add the overrule to only those runtime classes that you want to manage, since it can be called often and can affect performance of most everything you do in the drawing editor.

0 Likes
Message 3 of 13

dgorsman
Consultant
Consultant

@Activist_Investor wrote:
... since it can be called often and can affect performance of most everything you do in the drawing editor.

I normally recommend creating your own commands/interface to specifically invoke changes so they are done in the intended way, along with instructions for users.  Being extra-extra cautious would involve veto'ing any change not originating from those commands.  Otherwise you'll go bonkers trying to cover every eventuality (API, Properties Palette, clipboard COPY-PASTE, WBLOCK, and so on).

 

If you *do* want to cover all sorts of direct manipulation it's time to get into ObjectARX and building your own custom objects.

----------------------------------
If you are going to fly by the seat of your pants, expect friction burns.
"I don't know" is the beginning of knowledge, not the end.


0 Likes
Message 4 of 13

ActivistInvestor
Mentor
Mentor

I think you don't understand the OP's problem at all.

 

He asked how he can automatically have application data updated whenever the entity it is attached to is modified, regardless of how that happens.

IOW, what the OP is describing is custom behavior for certain objects. That is exactly what Overrules were created for - to allow the programmer to implement custom behavior that doesn't require custom objects, or a bunch of dedicated commands, along with a book to instruct end users on how to use them.

 

In case you didn't know this, AutoCAD's associative arrays were implemented entirely with Overrules - with no custom objects involved at all.

 

If you have issues with the design underlying Overrules, I would hit that 'Submit an Idea' button and take it up with the folks at Autodesk.

 

 

0 Likes
Message 5 of 13

CJModis
Advocate
Advocate

Activist_Investor написано:

You can use an ObjectOverrule to update your xdata/xrecord/etc. whenever the object is modified (by any means). Override the ObjectOverrule's Close() method, and update your data there. You should add the overrule to only those runtime classes that you want to manage, since it can be called often and can affect performance of most everything you do in the drawing editor.



Interesting idea. I try it and...

I did so for the test:

 

public class DemoObjectOverrule : ObjectOverrule
{
    public static DemoObjectOverrule TheObjectOverrule;

    static DemoObjectOverrule()
    {
        TheObjectOverrule = new DemoObjectOverrule();
    }
    public DemoObjectOverrule() { }

    public override void Close(DBObject dbObject)
    {
        base.Close(dbObject);
        Helpers.Editor.WriteMessage("\nClose!");
    }
}

But when i run autocad and load dll, I see "Close" in the editor every time I move the mouse.

Is there anywhere examples or help on the ObjectOverrule?

0 Likes
Message 6 of 13

_gile
Consultant
Consultant

Hi,

 

What about using the Database.ObjectModified handler?

 

using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Runtime;
using System.Collections.Generic;

[assembly: CommandClass(typeof(ObjectModifiedSample.Commands))]

namespace ObjectModifiedSample
{
    public class Commands
    {
        Document doc;
        Database db;
        Editor ed;
        HashSet<ObjectId> ids;

        public Commands()
        {
            doc = Application.DocumentManager.MdiActiveDocument;
            db = doc.Database;
            ed = doc.Editor;
            ids = new HashSet<ObjectId>();
            db.ObjectModified += OnObjectModified;
        }

        [CommandMethod("TEST")]
        public void Test()
        {
            var options = new PromptEntityOptions("\nSelect line: ");
            options.SetRejectMessage("\nSelected object must be a line.");
            options.AddAllowedClass(typeof(Line), true);
            var result = ed.GetEntity(options);
            if (result.Status != PromptStatus.OK)
                return;

            if (ids.Add(result.ObjectId))
            {
                using (var tr = db.TransactionManager.StartTransaction())
                {
                    var line = (Line)tr.GetObject(result.ObjectId, OpenMode.ForRead);
                    var midPt = line.StartPoint + (line.EndPoint - line.StartPoint) / 2.0;
                    
                    if (line.ExtensionDictionary.IsNull)
                    {
                        line.UpgradeOpen();
                        line.CreateExtensionDictionary();
                    }
                    var xdict = (DBDictionary)tr.GetObject(line.ExtensionDictionary, OpenMode.ForRead);

                    Xrecord xrec;
                    if (xdict.Contains("test"))
                    {
                        xrec = (Xrecord)tr.GetObject(xdict.GetAt("test"), OpenMode.ForWrite);
                    }
                    else
                    {
                        xdict.UpgradeOpen();
                        xrec = new Xrecord();
                        xdict.SetAt("test", xrec);
                        tr.AddNewlyCreatedDBObject(xrec, true);
                    }
                    xrec.Data = new ResultBuffer(new TypedValue(10, midPt));
                    tr.Commit();

                    ed.WriteMessage($"\n{midPt}");
                }
            }
        }

        private void OnObjectModified(object sender, ObjectEventArgs e)
        {
            if (ids.Contains(e.DBObject.ObjectId))
            {
                var line = (Line)e.DBObject;
                var midPt = line.StartPoint + (line.EndPoint - line.StartPoint) / 2.0;

                using (var tr = db.TransactionManager.StartOpenCloseTransaction())
                {
                    var xdict = (DBDictionary)tr.GetObject(line.ExtensionDictionary, OpenMode.ForRead);
                    var xrec = (Xrecord)tr.GetObject(xdict.GetAt("test"), OpenMode.ForWrite);
                    xrec.Data = new ResultBuffer(new TypedValue(10, midPt));
                    tr.Commit();
                }

                ed.WriteMessage($"\n{midPt}");
            }
        }
    }
}


Gilles Chanteau
Programmation AutoCAD LISP/.NET
GileCAD
GitHub

0 Likes
Message 7 of 13

ActivistInvestor
Mentor
Mentor

See my mod of your test code, and the comments.

 

While @_gile's proposal will work with data stored in an XDictionary, it will not work for XData because the notifying object cannot be modified from the ObjectModified event handler. The other downside to that approach is that the ObjectModified event handler fires for every single object in the database without any constraints, and (in his example, at least) requires a HashSet lookup on every call. 

 

In contrast, a constrained overrule will only be called on the type(s) of object(s) you've added the overrule to, and only if they have your xdata/xrecord attached.

 

public class DemoObjectOverrule : ObjectOverrule
{
   public static DemoObjectOverrule TheObjectOverrule;

   // While this is ok for testing, it should be avoided
   // in real-world use, because it doesn't afford very
   // good control over when the overrule is created or 
   // disposed. 

   static DemoObjectOverrule()
   {
      TheObjectOverrule = new DemoObjectOverrule();
   }

   public DemoObjectOverrule()
   {
      // Add the overrule to the Line class.  To support
      // other types of objects, add the overrule to each
      // of their runtime classes similarly. Or, to support 
      // all types of Curves, add the overrule to the Curve 
      // runtime class only, but not to any derived clases:
      AddOverrule(RXClass.GetClass(typeof(Line)), this, true);

      // Important: 
      //
      // Constrain the overrule using either an XData filter
      // or a DictionaryEntryFilter(), as this is the key to
      // avoiding a performance hit if the overrule is being
      // used with many runtime classes. 
      
      // Set the XData filter to the xdata appname of the
      // xdata you store on the entities that you want be 
      // notified about:
      //
      // SetXDataFilter("MYAPP");

      // Or.... 

      // Set the Dictionary filter to the name of an entry in
      // the Extension dictionary of entities that you want to
      // be notified about:
      //
      // SetExtensionDictionaryEntryFilter("MYDICTIONARY");
   }

   public override void Close(DBObject dbObject)
   {
      /// Act only if the IsModified property is true, and 
/// the IsUndoing property is false: if(dbObject.IsModified && ! dbObject.IsUndoing) OnModified(dbObject);
base.Close(dbObject); } private void OnModified(DBObject dbObject) { // This method will be called only if the argument was // actually modified and not the result of an UNDO/REDO: // // Here is where you would do your thing.
WriteMessage("\nOnModified(): {0}", dbObject.Handle.ToString()); } static void WriteMessage(string fmt, params object[] args) { Document doc = Application.DocumentManager.MdiActiveDocument; if(doc != null) doc.Editor.WriteMessage(fmt, args); } protected override void Dispose(bool disposing) { // Remove the overrule from each of the runtime // classes that you added it to in the constructor RemoveOverrule(RXClass.GetClass(typeof(Line)), this); base.Dispose(disposing); } }

 

 

Message 8 of 13

CJModis
Advocate
Advocate

Ask a little silly question))

You say:


 // While this is ok for testing, it should be avoided
// in real-world use, because it doesn't afford very
// good control over when the overrule is created or
// disposed.


So how will it be more correct?

 

Good. In general terms, I understood the idea.
Now I want to complicate it:
Let's say I create my own class that describes my specific line:

 

[Serializable]
public class MyLine
{
    public Point3d MiddlePoint { get; set; }
    // Link to associated Line
    public ObjectId LineObjectId { get; set; }
}

I want to serialize this class and store in the XRecord of the entity. But the xrecord of this class depends on the geometric characteristics of the primitive, so with each change of the primitive xrecord must be updated
The example is simple. More difficult if the xrecord does not depend on geometry, which I can get in standard ways.

 

It's a little difficult for me to explain, because I'm writing through an online translator ((

For example, I created a class that describes a break line (in video). There are two points in the class and all geometry is constructed from these two points. The block is taken as the basis for the entity. So when i creating there no problems - I write data to the expanded data. When editing the grips, i can also update the data in xrecord. But with the standard autocad's modification of the entity, I can not think of a solution.
Is my idea generally feasible?

0 Likes
Message 9 of 13

dgorsman
Consultant
Consultant

I do understand, quite well.  Just because it doesn't implement an MLP doesn't mean it's not a solution.

 

I've looked at similar problems, and looked at similar questions here and elsewhere, and it ends up boiling down to one or two important methods that cannot be handled and a whole mess of complicated code besides.  I've also looked at solution from other programs, and how they handle it - by providing a specific interface to manipulate objects in a specific way.

----------------------------------
If you are going to fly by the seat of your pants, expect friction burns.
"I don't know" is the beginning of knowledge, not the end.


0 Likes
Message 10 of 13

ActivistInvestor
Mentor
Mentor

 

 


@CJModis wrote:

   You say:


    // While this is ok for testing, it should be avoided
   // in real-world use, because it doesn't afford very
   // good control over when the overrule is created or
   // disposed.


   So how will it be more correct?


Using the Singleton pattern doesn't afford you the means to call Dispose() on the instance at whatever point you may need to, or at least, not without adding dedicated methods to the overrule to do that. I've come across cases where AutoCAD crashed at shutdown if an overrule was not disposed before that point, and required the use of the Application.BeginQuit event to dispose the overrule just before shutdown.

 

Good. In general terms, I understood the idea.
Now I want to complicate it:
Let's say I create my own class that describes my specific line:

 

[Serializable]
public class MyLine
{
    public Point3d MiddlePoint { get; set; }
    // Link to associated Line
    public ObjectId LineObjectId { get; set; }
}

I want to serialize this class and store in the XRecord of the entity. But the xrecord of this class depends on the geometric characteristics of the primitive, so with each change of the primitive xrecord must be updated
The example is simple. More difficult if the xrecord does not depend on geometry, which I can get in standard ways.

 

It's a little difficult for me to explain, because I'm writing through an online translator ((

For example, I created a class that describes a break line (in video). There are two points in the class and all geometry is constructed from these two points. The block is taken as the basis for the entity. So when i creating there no problems - I write data to the expanded data. When editing the grips, i can also update the data in xrecord. But with the standard autocad's modification of the entity, I can not think of a solution.
Is my idea generally feasible?

 

Either an ObjectOverrule or an ObjectModified event handler (as @_gile shows), can be used to keep your application data in an xrecord synchronized with the data of the entity the application data is attached to. If you use @_gile's suggested approach with the ObjectModified event and a HashSet acting as a filter, you will also have to contend with deep-cloning (copying), and that would make things a bit more complicated than it may seem, as the ObjectIds of the newly-created clones will not be in the HashSet.

 

It's difficult to recommend if keeping your app data synchronized with that data its attached to is feasible, or the best solution, without knowing more about the specifics of your app, rather than the relatively-simple examples you are giving (the break line). For example you are not revealing what you are using the stored data for (a DrawableOverrule, perhaps?). I generally find it difficult to make recommendations about how to go about solving a problem without knowing all the details.

 

 

Message 11 of 13

CJModis
Advocate
Advocate

Activist_Investor написано: 

It's difficult to recommend if keeping your app data synchronized with that data its attached to is feasible, or the best solution, without knowing more about the specifics of your app, rather than the relatively-simple examples you are giving (the break line). For example you are not revealing what you are using the stored data for (a DrawableOverrule, perhaps?). I generally find it difficult to make recommendations about how to go about solving a problem without knowing all the details.

 

 


Ok, i'm trying to explain what i'm trying to do.

1. I create class thats discribe my base entity

2. I create class thats discribe my entity Inherited from base entity

This class contains points:

private Point3d _insertionPoint = Point3d.Origin;
/// <summary>
/// Точка вставки СПДС примитива. Должна быть у всех СПДС примитивов
/// </summary>
public Point3d InsertionPoint { get { return _insertionPoint; } set { _insertionPoint = value; UpdateEntities(); } }

private Point3d _middlePoint = Point3d.Origin;
/// <summary>
/// Средняя точка. Нужна для перемещения СПДС примитива
/// </summary>
public Point3d MiddlePoint { get { return _middlePoint; } set { _middlePoint = value; } }

private Point3d _endPoint = Point3d.Origin;
/// <summary>
/// Вторая (конечная) точка СПДС примитива
/// </summary>
public Point3d EndPoint { get { return _endPoint; } set { _endPoint = value; UpdateEntities(); } }

Sub entities:

private readonly Lazy<Polyline> _mainPolyline = new Lazy<Polyline>(() => new Polyline());
public Polyline MainPolyline => _mainPolyline.Value;

public override IEnumerable<Autodesk.AutoCAD.DatabaseServices.Entity> Entities
{
    get
    {
        yield return MainPolyline;
        //yield return other entities
    }
}

And conatains void to update sub entities (or create its):

public void UpdateEntities()
{
    var length = EndPoint.DistanceTo(InsertionPoint);
    var scale = GetScale();
    
    if (EndPoint.Equals(Point3d.Origin))
    {
        // Задание точки вставки (т.е. второй точки еще нет)
        MakeSimplyEntity("SetInsertionPoint");
    }
    else if (length / scale < BreakLineMinLength)
    {
        // Задание второй точки - случай когда расстояние между точками меньше минимального
        MakeSimplyEntity("SetEndPointMinLenght");
    }
    else 
    {
        // Задание второй точки
        Plane plane = new Plane();
        var pts = PointsToCreatePolyline(scale, plane, InsertionPoint, EndPoint);
        for (var i = 0; i < pts.Count; i++)
            MainPolyline.SetPointAt(i, pts[i]);
    }
}

3. I create Jig with Update method:

protected override bool Update()
{
    try
    {
        using (AcadHelpers.Document.LockDocument(DocumentLockMode.ProtectedAutoWrite, null, null, true))
        {
            using (var tr = AcadHelpers.Document.TransactionManager.StartTransaction())
            {
                var obj = (BlockReference)tr.GetObject(Entity.Id, OpenMode.ForWrite, true);
                obj.Erase(false);
                obj.Position = BreakLine.InsertionPoint;
                obj.BlockUnit = AcadHelpers.Database.Insunits;
                tr.Commit();
            }
            BreakLine.UpdateEntities();
            BreakLine.BlockRecord.UpdateAnonymousBlocks();
        }
        return true;
    }
    catch
    {
        // ignored
    }
    return false;
}

4. On starting function creating my entity, based on anonimus block:

[CommandMethod("ModPlus", "mpBreakLine", CommandFlags.Redraw)]
public void CreateBreakLine()
{
    try
    {
        /* Регистрация СПДС приложения должна запускаться при запуске
         * функции, т.к. регистрация происходит в текущем документе
         * При инициализации плагина регистрации нет!
         */
         ModPlus.SPDSHelpers.ExtendedDataHelpers.AddRegAppTableRecord();
        //
        var objectIdCollection = new ObjectIdCollection();
        var contextCollection =
            AcadHelpers.Database.ObjectContextManager.GetContextCollection("ACDB_ANNOTATIONSCALES");
        var breakLine = new Entity.BreakLine(BreakLineFunction.currentStyle);
        var blockReference = CreateBreakLineBlock(ref breakLine, contextCollection);
        bool breakLoop = false;
        while (!breakLoop)
        {
            var breakLineJig = new Jig.BreakLineJig(breakLine, blockReference);
            do
            {
                label0:
                var status = AcadHelpers.Editor.Drag(breakLineJig).Status;
                if (status == PromptStatus.OK)
                {
                    if (breakLineJig.JigState != BreakLineJigState.PromptInsertPoint)
                    {
                        breakLoop = true;
                        status = PromptStatus.Other;
                    }
                    else
                    {
                        breakLineJig.JigState = BreakLineJigState.PromptEndPoint;
                        goto label0;
                    }
                }
                else if (status != PromptStatus.Other)
                {
                    using (AcadHelpers.Document.LockDocument())
                    {
                        using (var tr = AcadHelpers.Document.TransactionManager.StartTransaction())
                        {
                            var obj = (BlockReference)tr.GetObject(blockReference.Id, OpenMode.ForWrite);
                            obj.Erase(true);
                            tr.Commit();
                        }
                    }
                    breakLoop = true;
                }
                else
                {
                    breakLine.UpdateEntities();
                    //var blockId = breakLine.BlockId;
                    breakLine.BlockRecord.UpdateAnonymousBlocks();
                    objectIdCollection.Add(breakLine.BlockId);
                }
            } while (!breakLoop);
        }
    }
    catch (Autodesk.AutoCAD.Runtime.Exception exception)
    {
        MpExWin.Show(exception);
    }
}
private static BlockReference CreateBreakLineBlock(ref Entity.BreakLine breakLine, ObjectContextCollection occ)
{
    ObjectId objectId;
    BlockReference blockReference;
    using (AcadHelpers.Document.LockDocument())
    {
        using (var transaction = AcadHelpers.Document.TransactionManager.StartTransaction())
        {
            using (var blockTable = AcadHelpers.Database.BlockTableId.Write<BlockTable>())
            {
                var objectId1 = blockTable.Add(breakLine.BlockRecord);
                blockReference = new BlockReference(breakLine.InsertionPoint, objectId1);
                using (var blockTableRecord = AcadHelpers.Database.CurrentSpaceId.Write<BlockTableRecord>())
                {
                    objectId = blockTableRecord.AppendEntity(blockReference);
                    // Добавление "метки" СПДС примитива
                    ModPlus.SPDSHelpers.ExtendedDataHelpers.SetSPDSentityNameToXData(BreakLineFunction.SPDSEntName, blockReference);
                }
                transaction.AddNewlyCreatedDBObject(blockReference, true);
                transaction.AddNewlyCreatedDBObject(breakLine.BlockRecord, true);
            }
            transaction.Commit();
        }
        breakLine.BlockId = objectId;
        //breakLine.UpdateParameters(objectId);
    }
    return blockReference;
}

The result is what I showed on the video.

As I see the problem - the class that describes my entity, put in the xrecord block. If change the block, change the xrecord. On GripsOvverlue, i can do this. But when editing the block by autocad's functions, I do not know how to do it.
This has already been implemented, but there are no source codes and I can not contact the author

0 Likes
Message 12 of 13

ActivistInvestor
Mentor
Mentor
Accepted solution

@CJModis wrote:

 

As I see the problem - the class that describes my entity, put in the xrecord block. If change the block, change the xrecord. On GripsOvverlue, i can do this. But when editing the block by autocad's functions, I do not know how to do it.


I didn't really look too closely at your code, but I'm not sure I understand what the issue is with using either the ObjectModified event, or an ObjectOverrule.Close() override?

 

They both can notify you when your entity changes. In the case of the ObjectModified event, you can't modify the entity from the event handler, because it is in a notifying state, but you can modify an xrecord attached to it. With the overrule approach, you can modify the entity and any xrecords attached to it.

 

The only possible issue I see with your code, is that you are using a regular Transaction when you update your entity, but if you want to be able to do that at any time, including when AutoCAD is in the middle of a command that modifies the entity, you will need to use the OpenCloseTransaction instead.

Message 13 of 13

CJModis
Advocate
Advocate
Activist_Investor, Thank you very much. I think that all your recommendations, which you gave in this topic, should fully suit my needs. I will try and ask the emerging questions in new topics
 
0 Likes