ObjectOverrule for BlockReferences

ObjectOverrule for BlockReferences

AlexFielder
Advisor Advisor
552 Views
3 Replies
Message 1 of 4

ObjectOverrule for BlockReferences

AlexFielder
Advisor
Advisor

Hi all,

 

Building upon the excellent AttributeReferenceOverrule shared by @ActivistInvestor in this thread (my implementation of it shared here for information):

 

public class AttributeReferenceOverrule : ObjectOverrule
{
    static RXClass rxclass = RXObject.GetClass(typeof(AttributeReference));

    public AttributeReferenceOverrule()
    {
        AddOverrule(rxclass, this, true);
    }

    public override DBObject DeepClone(DBObject dbObject, DBObject ownerObject, IdMapping idMap, bool isPrimary)
    {
        var result = base.DeepClone(dbObject, ownerObject, idMap, isPrimary);
        AttributeReference copy = result as AttributeReference;
        if (copy != null)
        {
            /// TODO: Use additional conditions to decide if this
            /// AttributeReference's TextString should be cleared,
            /// which could involve any or all of the arguments to
            /// this method.

            /// In this example, the TextString is cleared unconditionally:

            if (!copy.IsWriteEnabled)
                copy.UpgradeOpen();

            if (copy.Tag == "NearestCornerBlockName" || copy.Tag == "BlockNumber")
            {
                copy.TextString = string.Empty;
            }
        }
        return result;
    }

    protected override void Dispose(bool disposing)
    {
        RemoveOverrule(rxclass, this);
        base.Dispose(disposing);
    }
}

 

I thought I would see whether I can achieve similar for BlockReferences. In this case I have two patterns for blockreference names I need to ObjectOverrule the Erase() event for: 

(.*)-(.*)-(\d{1,})

And:

(.*)-(.*)-(CR\d{1,})

 

So I have this:

public class BlockReferenceOverrule : ObjectOverrule
{
    static RXClass rxclass = RXObject.GetClass(typeof(BlockReference));

    public BlockReferenceOverrule()
    {
        AddOverrule(rxclass, this, true);
    }

    public override void Erase(DBObject dbObject, bool erasing)
    {
        Document doc = CadApp.DocumentManager.MdiActiveDocument;
        Editor ed = doc.Editor;
        Database db = doc.Database;
        base.Erase(dbObject, erasing);
        BlockHelpers blockHelpers = new BlockHelpers();
        CadHelper cadHelpers = new CadHelper();
        if(dbObject is BlockReference)
        {
            var blkRef = dbObject as BlockReference;
            if (blkRef != null)
            {
                string blockName = blockHelpers.GetEffectiveName(blkRef);
                string pattern = @"(.*)-(.*)-(CR\d{1,})";

                var match = Regex.Match(blockName, pattern);
                if (match.Success)
                {
                    var ownerId = db.GetLayoutBlockTableRecordId("Model");
                    if (ownerId.IsNull)
                    {
                        CadApp.ShowAlertDialog($"Layout 'Model' not found");
                        return;
                    }

                    using (var tr = db.TransactionManager.StartTransaction())
                    {
                        BlockTableRecord modelSpace = (BlockTableRecord)tr.GetObject(ownerId, OpenMode.ForWrite);
                        
                        if (modelSpace != null)
                        {
                            var bt = modelSpace.Database.BlockTableId.GetObject(OpenMode.ForRead) as BlockTable;
                            var blockRecord = bt[blockName].GetObject(OpenMode.ForWrite) as BlockTableRecord;
                            if (MessageBox.Show($"Are you sure you want to purge this block?\n{blockName}", "Purge Block",
                                MessageBoxButton.YesNo) == MessageBoxResult.Yes)
                            {
                                //purge the block
                                cadHelpers.PurgeItem(blockRecord.ObjectId);
                            }
                        }
                        tr.Commit();
                    }
                }
            }
        }
        
    }

    protected override void Dispose(bool disposing)
    {
        RemoveOverrule(rxclass, this);
        base.Dispose(disposing);
    }
}

 Here is the PurgeItem method:

internal void PurgeItem(ObjectId objectId)
{
    var doc = AcCoreAp.DocumentManager.MdiActiveDocument;
    var db = doc.Database;
    var ed = doc.Editor;

    bool itemsPurged = false;

    using (Transaction tr = db.TransactionManager.StartTransaction())
    {
        SymbolTableRecord record = (SymbolTableRecord)tr.GetObject(objectId, OpenMode.ForWrite);
        string recordName = record.Name;
        ed.WriteMessage("\nPurging " + record.GetType().Name + " " + recordName);
        log.Info("Purging " + record.GetType().Name + " " + recordName);
        
        record.Erase();

        tr.Commit();
    }
}

(having just copied that code there, yes I am aware I can refactor the code in the PurgeItem method into the calling code- I wrote it several months ago but haven't needed it until now so sue me 🤣)

 

The reasons for the blockreference Erase() objectoverrule are as follows:

  1. when the user deletes a block that matches the "(.*)-(.*)-(CR\d{1,})" pattern, there is only ever one of them so to save the user a step (and prevent a gap in the numbering since CR blocks increment through system-level-CR# with each new placement) I prompt the user to purge the block so we can reuse that name.
  2. Find any remaining blockreferences whose names match the "(.*)-(.*)-(\d{1,})" pattern and whose attributereferences contain the name of the CR blockreference and reset them to "".
  3. Any block that matches the "(.*)-(.*)-(\d{1,})" pattern should erase as normal, and if that blockreference was the last remaining reference, it should again ask if the user wishes to purge that block.

Number 1 is covered by the code I already shared (unless I have missed something blindingly obvious?), but I wondered if anyone has any suggestions for items 2 & 3?

 

(Having written out item 3 - it seems as simple as checking whether there remain blockreferences in ModelSpace whose name matches the erased blockreference, and if not, then pop the prompt about purging?)

 

Thanks,

 

Alex.

0 Likes
Accepted solutions (1)
553 Views
3 Replies
Replies (3)
Message 2 of 4

ActivistInvestor
Mentor
Mentor

I got as far as

 

 

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

 

 

And that's where I stopped and starting this reply.

 

Overrules operate at a very low-level. You can't use a regular transaction inside of an Overrule, under no circumstances.

 

After looking further at the code, your idea is not going to work. You can't purge objects or initiate any other type of high-level operation from an overrule method to start with. You definitely should not display a UI or try to interact with the user from an Overrule method. Again, overrules are very low-level operations that cannot initiate or control higher-level operations without all sorts of things going wrong. Overrules are good at operations where there is little or no dependence on other objects (e.g., opening other objects and proceeding as if code was running in a registered command). But they are not good at solving problems that require many other objects (beside the one passed into an Overrule method) to be opened/operated on as part of a higher-level operation.

 

The only thing I can suggest is to collect the ObjectIds of objects that you are trying to operate on directly from the Overrule's Erase() method, and once the 'higher-level' operation or command that is underway has ended, then act on all of the collected ObjectIds.

 

Also keep in mind that whatever operation triggers calls to an Overrule can be UNDOne by the user.

 

0 Likes
Message 3 of 4

AlexFielder
Advisor
Advisor

Hi @ActivistInvestor,

 

Thanks for this feedback; I wasn't aware of that limitation with the ObjectOverrule approach.

 

From my own testing I have witnessed the "undo" command causing this code to fire again, but haven't seen anything that I would call "deal breaking" that would cause errors in the drawing and/or a hard crash. 😬

 

Here is my refactored code for clarity:

 

 

 

 

public class BlockReferenceOverrule : ObjectOverrule
{
    private static readonly RXClass rxclass = RXObject.GetClass(typeof(BlockReference));
    private readonly BlockHelpers blockHelpers = new BlockHelpers();
    private readonly CadHelper cadHelpers = new CadHelper();

    public BlockReferenceOverrule()
    {
        AddOverrule(rxclass, this, true);
    }

    public override void Erase(DBObject dbObject, bool erasing)
    {
        if (dbObject is BlockReference blkRef)
        {
            Document doc = CadApp.DocumentManager.MdiActiveDocument;
            if (doc != null) // Ensure document is active
            {
                Database db = doc.Database;
                base.Erase(dbObject, erasing);

                ProcessBlockReference(doc, blkRef, db);
            }
        }
    }

    private void ProcessBlockReference(Document doc, BlockReference blkRef, Database db)
    {
        string blockName = blockHelpers.GetEffectiveName(blkRef);
        string cornerPattern = @"(.*)-(.*)-(CR\d{1,})";
        string blockPattern = @"(.*)-(.*)-(\d{1,})";

        if (Regex.IsMatch(blockName, cornerPattern))
        {
            ProcessCornerBlock(blkRef, db, blockName);
        }
        else if (Regex.IsMatch(blockName, blockPattern))
        {
            ProcessRemainingBlock(doc, blkRef, db, blockName);
        }
    }

    private void ProcessRemainingBlock(Document doc, BlockReference blkRef, Database db, string blockName)
    {
        var possibleRemainingBlocks = blockHelpers.FindBlockReferences(null, null, @"(.*)-(.*)-(\d{1,})", false, false);
        if (possibleRemainingBlocks.Count() == 0) // i.e. no remaining blockreferences
        {
            var ownerId = db.GetLayoutBlockTableRecordId("Model");
            if (ownerId.IsNull)
            {
                CadApp.ShowAlertDialog($"Layout 'Model' not found");
                return;
            }

            using (var doclock = doc.LockDocument())
            {
                using (var tr = db.TransactionManager.StartTransaction())
                {
                    BlockTableRecord modelSpace = (BlockTableRecord)tr.GetObject(ownerId, OpenMode.ForWrite);

                    if (modelSpace != null)
                    {
                        var bt = modelSpace.Database.BlockTableId.GetObject(OpenMode.ForRead) as BlockTable;
                        var blockRecord = bt[blockName].GetObject(OpenMode.ForWrite) as BlockTableRecord;
                        if (MessageBox.Show($"Are you sure you want to purge this block?\n{blockName}", "Purge Block",
                            MessageBoxButton.YesNo) == MessageBoxResult.Yes)
                        {
                            //purge the block
                            cadHelpers.PurgeItem(blockRecord.ObjectId);
                        }
                    }
                    tr.Commit();
                }
            }
        }
    }

    private void ProcessCornerBlock(BlockReference blkRef, Database db, string blockName)
    {
        ObjectId modelSpaceId = db.GetLayoutBlockTableRecordId("Model");
        if (modelSpaceId.IsNull)
        {
            CadApp.ShowAlertDialog($"Layout 'Model' not found");
            return;
        }

        using (var tr = db.TransactionManager.StartTransaction())
        {
            ProcessCornerBlockTransaction(tr, blockName, modelSpaceId);
            tr.Commit();
        }
    }

    private void ProcessCornerBlockTransaction(Transaction tr,
                                               string blockName, ObjectId modelSpaceId)
    {
        BlockTableRecord modelSpace = tr.GetObject(modelSpaceId, OpenMode.ForWrite) as BlockTableRecord;
        if (modelSpace != null)
        {
            if (TryPurgeBlock(modelSpace, blockName))
            {
                ResetAdjacentBlockAttributes(blockName);
            }
        }
    }

    private void ResetAdjacentBlockAttributes(string blockName)
    {
        //reset to blank the NearestCornerBlockName and BlockNumber attributes

        var possibleCornerAdjacentBlocks = blockHelpers.FindBlockReferences(null, null, @"(.*)-(.*)-(\d{1,})", false, false);
        foreach (var block in possibleCornerAdjacentBlocks)
        {
            if (block != null)
            {
                if (block.AttributeCollection.Contains("NearestCornerBlockName"))
                {
                    if (block.AttributeCollection.GetValue("NearestCornerBlockName").Equals(blockName, StringComparison.CurrentCultureIgnoreCase))
                    {
                        block.AttributeCollection.SetValue("NearestCornerBlockName", "");
                        if ((block.AttributeCollection.Contains("BlockNumber")))
                        {
                            block.AttributeCollection.SetValue("BlockNumber", "");
                        }
                    }
                }

            }
        }
    }

    private bool TryPurgeBlock(BlockTableRecord modelSpace, string blockName)
    {
        BlockTable bt = modelSpace.Database.BlockTableId.GetObject(OpenMode.ForRead) as BlockTable;
        if (bt != null)
        {
            BlockTableRecord blockRecord = bt[blockName].GetObject(OpenMode.ForWrite) as BlockTableRecord;
            if (blockRecord != null)
            {
                if (MessageBox.Show($"Are you sure you want to purge this block?\n{blockName}",
                                    "Purge Block", MessageBoxButton.YesNo) == MessageBoxResult.Yes)
                {
                    cadHelpers.PurgeItem(blockRecord.ObjectId);
                    return true;
                }
            }
        }
        return false;
    }

    protected override void Dispose(bool disposing)
    {
        RemoveOverrule(rxclass, this);
        base.Dispose(disposing);
    }
}

 

 

 

Were I to move away from doing any of this in the ObjectOverrule itself, what would you recommend my approach should look like?

 

EDIT: I suppose the methods themselves as described above would work regardless of whether they are called from inside the ObjectOverrule- so my question really should have been: what is the best method to get the list of ObjectIds out of the ObjectOverrule?

0 Likes
Message 4 of 4

ActivistInvestor
Mentor
Mentor
Accepted solution

You don't really need an overrule for this problem. A quick search of my codebase revealed a generic solution for dealing with this type of problem, that I was able to quickly adapt to the specifics of your case.

 

You can find the example code file here: