[MSCE U14 C++, C#] DgnEC Instance and Relationship Change Events

Introduction

With Microstation CE U14, we are introducing a way to listen to DgnEC instance and relationship change events at both Native DgnPlatform and Managed DgnPlatformNet layers w.r.t. the following interfaces: Bentley::DgnPlatform::IDgnECChangeListener & Bentley.DgnPlatformNet.DgnEC.IDgnECNetChangeListener

SDK Sample

In this blog, I am going to explain how can we use IDgnECChangeListener and IDgnECNetChangeListener interfaces w.r.t. following SDK example

  • MstnExamples\DgnEC\DgnECChangeEvents\NativeExample
  • MstnExamples\DgnEC\DgnECChangeEvents\ManagedExample

Native EC change events

Step:1: Implement IDgnECChangeListener

This interface is available in DgnPlatform/DgnECChangeManager.h. You need to register an implementation of IDgnECChangeListener with DgnECManager. The constructor takes *Priority* as a input. This variable defines order for your listener in ascending order. 

struct BaseChangeProcessor : Bentley::DgnPlatform::IDgnECChangeListener
{
public:
    BaseChangeProcessor (int priority) : IDgnECChangeListener (priority)
        {
        DgnECManager::GetManager ().RegisterDgnECChangeListener (this);
        }
    virtual ~BaseChangeProcessor ()
        {
        this->Clear ();
        DgnECManager::GetManager ().UnregisterDgnECChangeListener (this);
        }

Step :2: Implement AcceptChangeFor. 

Once something changed in EC system, you will get callback in this method. Here you get an ECClass which is undergoing changes and transaction type. TransactionType is an enum that shows whether transaction is regular transaction or UnDo/ReDo transaction. If you are interested to respond to changes for this particular class, you need to return true.

    virtual bool AcceptChangeFor (ECN::ECClassCR ecClass, TransactionType transactionType) override
        {
        return ecClass.GetSchema ().GetFullSchemaName ().Equals (SCHEMA);
        }
        
    //Or
    
    virtual bool AcceptChangeFor (ECN::ECClassCR ecClass, TransactionType transactionType) override
        {
        return ItemTypeLibrary::IsItemTypeSchema (ecClass.GetSchema ());
        }

Step 3: Implement DgnECInstancesChanged

If you have responded +ve in AcceptChangeFor, you will get this callback for any changes in DgnEC instances. It gives you collection of change DgnEC instance records.

    virtual void DgnECInstancesChanged (DgnInstanceChangeRecords& changedRecords, DgnFileR file, TransactionType transactionType) override
    {
        for (auto const& nRecord : changedRecords)
        {
            switch (nRecord->GetChangeState ())
            {
            case DgnECChangeType::Added:
                m_addedInstances.push_back (nRecord);
                break;
            case DgnECChangeType::Modified:
                m_modifiedInstances.push_back (nRecord);
                break;
            case DgnECChangeType::Deleted:
                m_deletedInstances.push_back (nRecord);
                break;
            default:
                m_existingInstances.push_back (nRecord);
                break;
            }
        }
    }

Understanding DgnInstanceChangeRecords

DgnInstanceChangeRecords are collection of non copy-able class, DgnInstanceChangeRecord. It has following important members.

  • DgnECChangeType GetChangeState (): Indicates whether available instance in record is added, modified or deleted.
  • ECN::ECClassCR GetClassOfChange (): ECClass under change
  • DgnECInstanceP GetAfterChangeInstance (): Modified or added instance after change. This will be null for deleted change state.
  • WCharCP GetDeletedInstanceId (): If change state is deleted, you can get deleted instanceId.

For now, we cannot provide an instance before change because of performance and change tracking overhead.

Step 4: Implement RelationshipsChanged

If you have responded +ve in AcceptChangeFor, you will get this callback for any changes in DgnEC relationships. It gives you collection of changed relationship records.

    virtual void RelationshipsChanged (DgnRelationChangeRecords& changedRecords, DgnFiles& files, TransactionType transactionType) override
    {
        for (auto const& nRecord : changedRecords)
        {
            switch (nRecord->GetChangeState ())
            {
            case DgnECChangeType::Added:
                m_addedRelations.push_back (nRecord);
                break;
            case DgnECChangeType::Modified:
                m_modifiedRelations.push_back (nRecord);
                break;
            case DgnECChangeType::Deleted:
                m_deletedRelations.push_back (nRecord);
                break;
            default:
                m_existingRelations.push_back (nRecord);
                break;
            }
        }
    }

Understanding DgnRelationChangeRecords

DgnRelationChangeRecordsare collection of non copy-able class, DgnRelationChangeRecord. It has following important members.

  • DgnECChangeType GetChangeState (): Indicates whether available instance in record is added, modified or deleted.
  • ECN::ECClassCR GetClassOfChange (): ECClass under change
  • IDgnECRelationshipInstanceP GetAfterChangeInstance (): Relationship instance after change.
  • IDgnECRelationshipInstanceP GetBeforeChangeInstance (): Relationship instance before change.

Step 5: Understanding SetIsHandled/GetIsHandled

You can have multiple listeners. During instance or relationship changed callback, if you want other listeners should not receive further callbacks for a particular changed block, set <your listener>->SetIsHandled (true); For e.g. you have two listeners A and B. Now, change property from ElementInfo dialog. Based on priority, say you get callback in A. Set *SetIsHandled*, you will not get callback in B.

Managed EC change events

Step:1: Implement IDgnECNetChangeListener

This interface is available in Bentley.DgnPlatformNet.dll. You need to register implementation of your listener with DgnECChangeAdapter.  For Priority, you need to implement GetPriority.

internal abstract class BaseChangeProcessor: IDgnECNetChangeListener
{
public BaseChangeProcessor()
    {
    DgnECManager.Manager.GetDgnECChangeAdapter().RegisterListener(this);
    }
public override int GetPriority()
    {
    return 5;
    }

Step :2: Implement AcceptChangeFor. 

Same as native API. Here we get ChangeClassification instead of transaction type enum. Purpose of this enum is exactly same as it's native counter part.

public override bool AcceptChangeFor(IECClass ecClass, ChangeClassification transactionType)
    {
    if (ecClass.Schema == null)
        return false;
    return ecClass.Schema.Name == SCHEMA;
    }

//or

public override bool AcceptChangeFor(IECClass ecClass, ChangeClassification transactionType)
    {
    return ItemTypeLibrary.IsItemTypeSchema(ecClass.Schema.FullName);
    }

Step 3: Implement DgnECInstancesChanged

On any changes in DgnEC instances, you will get this callback.

public void DgnECInstancesChanged(IList<DgnInstanceChangeRecord> changeRecords, DgnFile file, ChangeClassification transactionType)
    {
    AddInstanceChanges(changeRecords);
    }

protected void AddInstanceChanges(IList<DgnInstanceChangeRecord> changeRecords)
    {
    foreach(var record in changeRecords)
        {
        switch (record.ChangeState)
            {
            case ChangeSetElementState.New:
                m_addedInstances.Add(record);
                break;
            case ChangeSetElementState.Modified:
                m_modifiedInstances.Add(record);
                break;
            case ChangeSetElementState.Deleted:
                m_deletedInstances.Add(record);
                break;
            default:
                m_existingInstances.Add(record);
                break;
            };
        }
    }

Understanding DgnInstanceChangeRecord

DgnInstanceChangeRecords are collection of non copy-able class, DgnInstanceChangeRecord. It has following important members.

ChangeSetElementState ChangeState: Indicates whether available instance in record is added, modified or deleted.
IECClass ClassOfChange: ECClass under change
IDgnECInstance AfterChange: Modified or added instance after change. This will be null for deleted change state.
string DeletedInstanceId: If change state is deleted, you can get deleted instanceId.

For now, we cannot provide instance before change because of performance and change tracking overhead.

Step 4: Implement RelationshipsChanged

On any change in DgnEC relationships, you will get this callback.

public void RelationshipsChanged(IList<DgnRelationChangeRecord> changeRecords, IList<DgnFile> files, ChangeClassification transactionType)
    {
    AddRelationChanges(changeRecords);
    }

protected void AddRelationChanges(IList<DgnRelationChangeRecord> changeRecords)
    {
    foreach(var record in changeRecords)
        {
        switch (record.ChangeState)
            {
            case ChangeSetElementState.New:
                m_addedRelations.Add(record);
                break;
            case ChangeSetElementState.Modified:
                m_modifiedRelations.Add(record);
                break;
            case ChangeSetElementState.Deleted:
                m_deletedRelations.Add(record);
                break;
            default:
                m_existingRelations.Add(record);
                break;
            };
        }
    }

Understanding DgnRelationChangeRecord

DgnRelationChangeRecordsare collection of non copyable class, DgnRelationChangeRecord. It has following important members.

  • ChangeSetElementState ChangeState: Indicates whether available instance in record is added, modified or deleted.
  • IECClass ClassOfChange: ECClass under change
  • IDgnECRelationshipInstance AfterChange: Relationship instance after change.
  • IDgnECRelationshipInstance BeforeChange: Relationship instance before change.
  • RelationshipInfo AfterChangeRelationInfo: This provides information like relationship class, source/target ECClasses and Ids for changed relationships.
  • RelationshipInfo BeforeChangeRelationInfo: This provides information like relationship class, source/target ECClasses and Ids for relationships before changed.

Ideally, you should use RelationshipInfo. It will save performance overhead of serializing native source/target instances to managed.

Step 5: Understanding IsHandled

It works same as native Set/Get IsHandled method.

Some useful tips

  • Prefer native listener over managed one. This will save time in native to managed serialization.
  • Listeners are registered in following order:

    1. Listeners of Microstation native controls like Explorer, Element Info etc. This listener will not be exposed.
    2. All registered native listeners
    3. All registered managed listeners.
    4. If you set, IsHandled for any of listeners during point#2/3, all other in queue will be skipped.

That's all. Please try this in the MicroStation U14 SDK and give us feedback. Thanks a lot.