[CONNECT C++] How to replace TagValue on an existing tag instance

I have installed:

MicroStation CONNECT Edition
10.15.0.74

MicroStation CONNECT Edition SDK
10.15.00.076

I am attempting to to replace the TagValue on existing tag instances.  From the mdlTag API I am only successful if I create a brand new tag with the desired value using the existing tag as a template then deleting the old one.

Since a tag item is an ECInstance of ECClass "TagElement" and ECSchema "DgnElementSchema", I thought it would be straightforward to simply make the change by modifying the ECProperty "TagValue".  However while debugging my code I discovered too late that all the ECProperties of the ECClass "TagElement" are marked as readOnly in the schema!

For what its worth I posted my function below.

Does anyone know of a work around that will let me change this ECProperty value?

Thanks,

John M.

Code Snippet

void TagHelper::SetTagValue (WString &tagName, WString &newTagValue)
    {
    DgnModelP dgnModelP = ISessionMgr::GetActiveDgnModelP();

    ECQueryPtr query = ECQuery::CreateQuery(L"DgnElementSchema", L"TagElement");
    if (query == nullptr)
        return;

    WhereCriterionPtr where = WhereCriterion::CreatePropertyComparison (L"TagName", WhereCriterion::EQ, ECValue(tagName.c_str()));
    
    query->SetWhereCriterion (*where);       
    query->SetSelectProperties(true);

    FindInstancesScopeOption opts(DgnECHostType::Element);
    opts.SetSearchPublicChildren (true);
    FindInstancesScopePtr scope = FindInstancesScope::CreateScope(*dgnModelP, opts);

    if (scope == nullptr)
        return;

    DgnECInstanceIterable instances = DgnECManager::GetManager ().FindInstances(*scope, *query);
    if (instances.empty())
        {
        wprintf (L"SetTagValue() No TagElements found where TagName equals %s\n", tagName.c_str());
        return;
        }

    for (DgnECInstancePtr dgnECInstance : instances)
        {
        if (dgnECInstance->IsPropertyReadOnly (L"TagValue"))
            wprintf(L"SetTagValue() ECProperty 'TagValue' is ReadOnly\n");

        ECObjectsStatus oStatus = dgnECInstance->SetValue (L"TagValue", ECValue (newTagValue.c_str()));

        if (ECObjectsStatus::ECOBJECTS_STATUS_Success == oStatus)
            {
            StatusInt status = dgnECInstance->WriteChanges (); 
            wprintf (L"SetTagValue() tagName %s newTagValue %s status = %d\n", tagName.c_str(), newTagValue.c_str(), status);             
            }
        else
            wprintf (L"SetTagValue() tagName %s newTagValue %s SetValueAsString ERROR, oStatus = %d\n", tagName.c_str(), newTagValue.c_str(), oStatus); 
        }
      }

Parents
  • Since a tag item is an ECInstance of ECClass "TagElement"

    Tags predate ECSchemas by decades.  Tag Elements are type 37 DGN elements.  Use the TagElementHandler or the mdlTag_api to work with tags.

    The DgnElementSchema provides an ECSchema view of DGN elements.  A tag value and an ECValue are different things.

    I am only successful if I create a brand new tag with the desired value using the existing tag as a template then deleting the old one.

    That's a usual idiom for modifying an element programmatically. Copy-and-replace sits well with MicroStation's transaction manager (Edit Undo/Redo).  You can take a shortcut with mdlTag_setAttrValue.

    With CONNECT you can use C++ to good effect: see DgnTagValue, TagElementHandler, which provides SetAttributeValue() and GetAttributeValue()

     
    Regards, Jon Summers
    LA Solutions

  • Hey Jon,

    Can TagElementHandler be used on an existing element or only on a memory copy of an existing element?

    I did try using TagElementHandler but ran into problems.  See code snippet, below.  I ended up creating a copy, set the desired value on the copy, persist the copy, and then delete the original.  This works but feels both kludgy and dangerous to me.  In a previous version I didn't make copy but instead changed the original followed by ReplaceInModel.  But that would always would always fail. 

    I understand that a type 37 (ATTRIBUTE_ELM) element is not the same thing as an EC TagElement.  However I was making an assumption that the actual tag data for a type 37 was stored as an ECInstance in the dgn file for ECClass TagElement.  My theory was that that ReplaceInModel wasn't working because under the hood the TagElement ECClass has all readOnly properties.  If not, do you have any idea where the tag data is stored?

    Thanks,

    John M.

    void TagHelper::SetTagValue (EditElementHandleR tagEeh, WString text)
        {
        StatusInt status = SUCCESS;
    
        ElementRefP elementRef = tagEeh.GetElementRef();
        DgnModelRefP modelRef = tagEeh.GetModelRef();
    
        DgnTagValue value;
        if (SUCCESS == TagElementHandler::GetAttributeValue (tagEeh, value))
            {
            switch(value.GetTagType())
                {
                case MS_TAGTYPE_CHAR:
                case MS_TAGTYPE_WCHAR:
                    {
                    DgnTagValue tagValue(text.c_str());
                    status = TagElementHandler::SetAttributeValue(tagEeh, tagValue);
                    break;
                    }
    
                case MS_TAGTYPE_SINT:
                case MS_TAGTYPE_LINT:
                    {
                    DgnTagValue tagValue(_wtol(text.c_str()));
                    status = TagElementHandler::SetAttributeValue(tagEeh, tagValue);
                    break;
                    }
    
                case MS_TAGTYPE_DOUBLE:
                    {
                    DgnTagValue tagValue(wcstod(text.c_str(), NULL));
                    status = TagElementHandler::SetAttributeValue(tagEeh, tagValue);
                    break;
                    }
                }
    
            // Add a copy of the original tag (with new text) to the model
            if (SUCCESS == tagEeh.AddToModel())
                {
                // Delete the original tag
                EditElementHandle eeh (elementRef, modelRef);                
                eeh.DeleteFromModel ();                
                }
            }
        }

  • Do you have any idea where the tag data is stored?

    The value of a tag element is stored in the element itself.  Integer and double values don't take up much space.  Binary and text values take more space — I don't know what the max stored value can be, but they presumably fit in the 768 65535 bytes of an MSElement unless stored in extended attribute data.

    I was making an assumption that the actual tag data for a type 37 was stored as an ECInstance in the dgn file

    That would break DGN file compatibility between MicroStation versions.

    Item Types supersede tags.  They have many advantages over tags and store data in EC instances.

     
    Regards, Jon Summers
    LA Solutions

  • In a previous version I didn't make copy but instead changed the original followed by ReplaceInModel. 

    You are always working on an in memory representation, otherwise you wouldn't need to call ReplaceInModel.

    The normal edit approach with the mdl api was always calling mdlThing_create with a full size MSElement and passing the original as the template, make changes, call mdlElement_rewrite. The full size MSElement was to ensure sufficient room for adding linkages, etc. when the input element came from and MSElementDescr as only enough space for the current element size has been allocated.

    The mdlTag functions should still work exactly the same with some small differences like requiring a DgnModel to be specified. That said the C++ api and using EditElementHandle is much nicer, especially since the max element size is so much larger now, you want to avoid declaring one on the stack.

    // Add a copy of the original tag (with new text) to the model
    if (SUCCESS == tagEeh.AddToModel())
    {
    // Delete the original tag
    EditElementHandle eeh (elementRef, modelRef);
    eeh.DeleteFromModel ();
    }

    ^ Should just be able to call tagEeh.ReplaceInModel(elementRef). Above code doesn't preserve the element id (it would if it was delete then add). You said ReplaceInModel didn't work for you, did you supply elementRef to replace? What error did you get? How was tagEeh created?

    I don't know what the max stored value can be, but they presumablyt fit in the 768 bytes of an MSElement unless stored in extended attribute data.

    V7 element size was 768 Int16, V8 size is 65535 Int16. Tag values are indeed stored as part of the element data.

    HTH

    -B



  • Hey Brien,

    Thanks for the explanation.  I guess coding tags builds character. :-)

    I'm in the process of porting a very large 25+ year old customer application written in native *.mc to CONNECT and don't have the luxury of replacing its use of tags with ItemTypes (or any other kind of ECInstance.)  There is a lot of code but the basic algorithm is:

    Scan for all shapes
    
    For-Each shape
    
        Get list of tags on shape
    
        For-Each tag on shape
    
            Replace tag text with new text based on an mdlCExpression result

    I encountered a variety of problems.  For example, if I delete a tag after creating a new one the memory would get hosed up in either the tag list (returned from mdlTag_getElementTags as a MSElementDescrP that contains a liked list of tag linked list of MSElementDescrP) or the scan iterator or both.  For example, the scanner would stop scanning after the first one.  And as mentioned I could add a new tag but not ReplaceInModel an existing tag.  (I also have to be careful because the tag has customer user data linkages that must be preserved after the text replacement)

    I'll try to write a simple function that does the essentials without all the detailed customer logic.  Perhaps there are bugs in the code I haven't worked out that are not obvious with the large numbers of lines to port.  If I learn more or find a solution I'll post it here.

    Thanks,

    John M.

  • Hey Brien,

    As promised I coded up a simplified test that demonstrates the overall algorithm and my problems.  See code sample below.  The function Experiment() scans for all shapes.  In the element callback function ModifyTagsScanCallback() I make call ModifyElementTags() up update all the tags associated with a shape.  ModifyElementTags() calls mdlTag_getElementTags in order to get an MSElementDescrP that contains a list of tags MSElementDescrP.    For each of these tags I call SetTagValue().  If the tag is of string type I change that string to L"Stella".

    I put a few notes in the comments.  ModifyElementTags() is throwing an exception at the end of the for-loop.  From experimentation I find that this is happening as a result of the call to TagElementHandler::SetAttributeValue() inside my SetTagValue() function.  Commenting out that line removes the exception.

    If I ignore the exceptions in the debugger I find that tagEeh.ReplaceInModel() is always returning a status of 32768 and that the tag text is not updated in the dgn file.

    Any advice would be greatly appreciated.

    Thanks,

    John M.

    Code Sample

                         

    void SetTagValue (EditElementHandleR tagEeh, WString &newTagValue)
        {
        DgnTagValue tagValue;
        if (SUCCESS == TagElementHandler::GetAttributeValue (tagEeh, tagValue))
            {
            LangCodePage codePage;
            BeStringUtilities::GetCurrentCodePage (codePage);
            WString oldTagValue = tagValue.GetStringValue(&codePage);
       
            TagType tagType = tagValue.GetTagType();
    
            if ((TagType::MS_TAGTYPE_CHAR == tagType) || (TagType::MS_TAGTYPE_WCHAR == tagType))
                {
                tagValue.SetTagValue(newTagValue.c_str());
    
                if (SUCCESS == TagElementHandler::SetAttributeValue (tagEeh, tagValue))  // Note: <-- Exception goes away if I comment out this line
                    {
                    StatusInt status = tagEeh.ReplaceInModel(tagEeh.GetElementRef ());   // Note: <-- status is always 32768      
                    wprintf (L"SetTagValue() oldTagValue >%s< newTagValue >%s< status = %d\n", oldTagValue.c_str(), newTagValue.c_str(), status);       
                    }
                }
            }
        }
    
    void ModifyElementTags (ElementHandleR eh)
        {
        MSElementP elP = const_cast<MSElementP> (eh.GetElementCP());
        DgnModelRefP modelRef = eh.GetModelRef();
    
        if (mdlElement_getID (elP) == 0)
            return;
    
        int numTags = 0;
        MSElementDescrP tagEdPList = NULL;
    
        if (SUCCESS != mdlTag_getElementTags (&tagEdPList, &numTags, elP, modelRef, 0))
            return;
    
        if (numTags < 1)
            return;
    
        for (MSElementDescrP edP = tagEdPList; NULL!= edP; edP = edP->h.next)
            {
            WString newTagValue (L"Stella"); // Set all string type Tag values to the name of my dog.
            EditElementHandle tagEeh (edP, true, true, modelRef);
            SetTagValue (tagEeh, newTagValue);
            }  // <-- Note: An exception is thrown in ElementHandle on m_descr->Release();
        }
    
    int ModifyTagsScanCallback (ElementRefP eleRef, void* userData, ScanCriteriaP scP)
        {
        // Shape Element
        ElementHandle eh (eleRef, scP->GetModelRef());
        ModifyElementTags (eh);
        return 0;
        }
    
    int Experiment(void)
        {
        int status = ERROR;
    
        ScanCriteriaP sc = mdlScanCriteria_create();
    
        mdlScanCriteria_setModel (sc, mdlModelRef_getActive());
        mdlScanCriteria_setReturnType (sc, MSSCANCRIT_ITERATE_ELMREF, false, true);
        mdlScanCriteria_addSingleElementTypeTest (sc, SHAPE_ELM); 
        mdlScanCriteria_setElemRefCallback (sc, ModifyTagsScanCallback, NULL);
        mdlScanCriteria_scan (sc, NULL, NULL, NULL);
        mdlScanCriteria_free (sc);
        
        return status;        
        }

  • You have a memory leak: mdlTag_getElementTags() must be balanced by a call to mdlElmDscr_freeAll().

    EditElementHandle tagEeh (edP, true, true, modelRef);
            ...
            }  // <-- Note: An exception is thrown in ElementHandle on m_descr->Release();

    You've constructed an ElementHandle that takes ownership of the descriptor.  It freed the descriptor on leaving the scope of the for loop.  There's no need to attempt to free the descriptor.

    TagType tagType = tagValue.GetTagType();
    
       if ((TagType::MS_TAGTYPE_CHAR == tagType) || (TagType::MS_TAGTYPE_WCHAR == tagType))
       {
         tagValue.SetTagValue(newTagValue.c_str());
    

    DgnTagValue.SetTagValue() is overridden for all tag types.  You don't need to perform that test, as you would do if writing mdlTag_xxx code.

        for (MSElementDescrP edP = tagEdPList; NULL!= edP; edP = edP->h.next)
        {
          ...
          EditElementHandle tagEeh (edP, true, true, modelRef);
          ...
        }  
    

    You're constructing an ElementHandle using one member of a linked list of descriptors.  Presumably the descr->h.next pointer is retained.  I imagine that what happens subsequently is that the descriptor is destroyed in the ElementHandle destructor.  The remainder of the list evaporates with it, so for the next tag in the loop there is no data.

    Try duplicating the descriptor of the single tag element in the list before passing to your modify function.

    MSElementDescrP singleTag = nullptr;
    mdlElmdscr_duplicateSingle (&singleTag, edP);
    const bool Owned {true};
    const bool IsModified  {true};
    EditElementHandle eehTag (singleTag, Owned, !IsModified, modelRef);

     
    Regards, Jon Summers
    LA Solutions

  • Howdy,

    Between the two of you I tracked down the needed corrections.  I pasted the corrected code sample below.

    First, Jon's suggestion did correct the exception I experienced.  However I found that an EditElementHandle created using an MSElementDescrP returns NULL for eehTag->GetElementRef().  I need this later on in my SetTagValue() funcrtion in order to successfully execute ReplaceInModel().

    Looking through the SDK samples I found a different method that generates an internal ElementRefP:

    EditElementHandle eehTag;
    eehTag.FindByID(edP->el.ehdr.uniqueId, modelRef);

    Unfortunately my call to ReplaceInModel() was still failing.  While debugging I discovered that the previous line wipes out the tagEeh internal ElementRefP:

    if (SUCCESS == TagElementHandler::SetAttributeValue (tagEeh, tagValue))

    My solution was to save this value off into a local variable before making the call and then using the local variable in ReplaceInModel().

    Glad to see this finally work but that last bit very much confuses me.  This feels like bug in TagElementHandler::SetAttributeValue() to me.  Perhaps Brien can explain it to us.

    Much thanks to Jon, Jan, and Brien for all the advice!

    --John M.

    Code Sample

    void SetTagValue (EditElementHandleR tagEeh, WString &newTagValue)
        {
        ElementRefP eleRefP = tagEeh.GetElementRef ();
        if (NULL == eleRefP)
            wprintf (L"SetTagValue() eleRefP is NULL\n");
    
        DgnTagValue tagValue;
        if (SUCCESS == TagElementHandler::GetAttributeValue (tagEeh, tagValue))
            {
            // Get the current tag value for tracing purposes
            LangCodePage codePage;
            BeStringUtilities::GetCurrentCodePage (codePage);
            WString oldTagValue = tagValue.GetStringValue(&codePage);
       
            tagValue.SetTagValue(newTagValue.c_str());
            if (SUCCESS == TagElementHandler::SetAttributeValue (tagEeh, tagValue))
                {
                ElementRefP eleRef02P = tagEeh.GetElementRef();
                if (NULL == eleRef02P)
                    wprintf(L"SetTagValue() eleRef02P is NULL\n");
    
                StatusInt status = tagEeh.ReplaceInModel(eleRefP);                      // Note: <-- status is always 0
                //StatusInt status = tagEeh.ReplaceInModel(tagEeh.GetElementRef ());    // Note: <-- status is always 32768      
                wprintf (L"SetTagValue() oldTagValue >%s< newTagValue >%s< status = %d\n", oldTagValue.c_str(), newTagValue.c_str(), status);       
                }
            }
        }    
    
    void ModifyElementTags (ElementHandleR eh)
        {
        MSElementP elP = const_cast<MSElementP> (eh.GetElementCP());
        DgnModelRefP modelRef = eh.GetModelRef();
    
        if (mdlElement_getID (elP) == 0)
            return;
    
        int numTags = 0;
        MSElementDescrP tagEdPList = NULL;
    
        // RetriEve a list of tags for this element
        if (SUCCESS != mdlTag_getElementTags (&tagEdPList, &numTags, elP, modelRef, 0))
            return;
    
        if (numTags < 1)
            return;
    
        // For each tag on the element
        for (MSElementDescrP edP = tagEdPList; NULL!= edP; edP = edP->h.next)
            {
            WString newTagValue (L"Stella"); // Set all string type Tag values to the name of my dog.
    
            EditElementHandle eehTag;
            eehTag.FindByID(edP->el.ehdr.uniqueId, modelRef);
    
            SetTagValue (eehTag, newTagValue);
            }
    
        mdlElmdscr_freeAll (&tagEdPList);        
        }
    
    int ModifyTagsScanCallback (ElementRefP eleRef, void* userData, ScanCriteriaP scP)
        {
        // Shape Element
        ElementHandle eh (eleRef, scP->GetModelRef());
        ModifyElementTags (eh);
        return 0;
        }
    
    int Experiment(void)
        {
        int status = ERROR;
    
        ScanCriteriaP sc = mdlScanCriteria_create();
    
        // Scan for all shapes in the active model
        mdlScanCriteria_setModel (sc, mdlModelRef_getActive());
        mdlScanCriteria_setReturnType (sc, MSSCANCRIT_ITERATE_ELMREF, false, true);
        mdlScanCriteria_addSingleElementTypeTest (sc, SHAPE_ELM); 
        mdlScanCriteria_setElemRefCallback (sc, ModifyTagsScanCallback, NULL);
        mdlScanCriteria_scan (sc, NULL, NULL, NULL);
        mdlScanCriteria_free (sc);
        
        return status;        
        }

Reply
  • Howdy,

    Between the two of you I tracked down the needed corrections.  I pasted the corrected code sample below.

    First, Jon's suggestion did correct the exception I experienced.  However I found that an EditElementHandle created using an MSElementDescrP returns NULL for eehTag->GetElementRef().  I need this later on in my SetTagValue() funcrtion in order to successfully execute ReplaceInModel().

    Looking through the SDK samples I found a different method that generates an internal ElementRefP:

    EditElementHandle eehTag;
    eehTag.FindByID(edP->el.ehdr.uniqueId, modelRef);

    Unfortunately my call to ReplaceInModel() was still failing.  While debugging I discovered that the previous line wipes out the tagEeh internal ElementRefP:

    if (SUCCESS == TagElementHandler::SetAttributeValue (tagEeh, tagValue))

    My solution was to save this value off into a local variable before making the call and then using the local variable in ReplaceInModel().

    Glad to see this finally work but that last bit very much confuses me.  This feels like bug in TagElementHandler::SetAttributeValue() to me.  Perhaps Brien can explain it to us.

    Much thanks to Jon, Jan, and Brien for all the advice!

    --John M.

    Code Sample

    void SetTagValue (EditElementHandleR tagEeh, WString &newTagValue)
        {
        ElementRefP eleRefP = tagEeh.GetElementRef ();
        if (NULL == eleRefP)
            wprintf (L"SetTagValue() eleRefP is NULL\n");
    
        DgnTagValue tagValue;
        if (SUCCESS == TagElementHandler::GetAttributeValue (tagEeh, tagValue))
            {
            // Get the current tag value for tracing purposes
            LangCodePage codePage;
            BeStringUtilities::GetCurrentCodePage (codePage);
            WString oldTagValue = tagValue.GetStringValue(&codePage);
       
            tagValue.SetTagValue(newTagValue.c_str());
            if (SUCCESS == TagElementHandler::SetAttributeValue (tagEeh, tagValue))
                {
                ElementRefP eleRef02P = tagEeh.GetElementRef();
                if (NULL == eleRef02P)
                    wprintf(L"SetTagValue() eleRef02P is NULL\n");
    
                StatusInt status = tagEeh.ReplaceInModel(eleRefP);                      // Note: <-- status is always 0
                //StatusInt status = tagEeh.ReplaceInModel(tagEeh.GetElementRef ());    // Note: <-- status is always 32768      
                wprintf (L"SetTagValue() oldTagValue >%s< newTagValue >%s< status = %d\n", oldTagValue.c_str(), newTagValue.c_str(), status);       
                }
            }
        }    
    
    void ModifyElementTags (ElementHandleR eh)
        {
        MSElementP elP = const_cast<MSElementP> (eh.GetElementCP());
        DgnModelRefP modelRef = eh.GetModelRef();
    
        if (mdlElement_getID (elP) == 0)
            return;
    
        int numTags = 0;
        MSElementDescrP tagEdPList = NULL;
    
        // RetriEve a list of tags for this element
        if (SUCCESS != mdlTag_getElementTags (&tagEdPList, &numTags, elP, modelRef, 0))
            return;
    
        if (numTags < 1)
            return;
    
        // For each tag on the element
        for (MSElementDescrP edP = tagEdPList; NULL!= edP; edP = edP->h.next)
            {
            WString newTagValue (L"Stella"); // Set all string type Tag values to the name of my dog.
    
            EditElementHandle eehTag;
            eehTag.FindByID(edP->el.ehdr.uniqueId, modelRef);
    
            SetTagValue (eehTag, newTagValue);
            }
    
        mdlElmdscr_freeAll (&tagEdPList);        
        }
    
    int ModifyTagsScanCallback (ElementRefP eleRef, void* userData, ScanCriteriaP scP)
        {
        // Shape Element
        ElementHandle eh (eleRef, scP->GetModelRef());
        ModifyElementTags (eh);
        return 0;
        }
    
    int Experiment(void)
        {
        int status = ERROR;
    
        ScanCriteriaP sc = mdlScanCriteria_create();
    
        // Scan for all shapes in the active model
        mdlScanCriteria_setModel (sc, mdlModelRef_getActive());
        mdlScanCriteria_setReturnType (sc, MSSCANCRIT_ITERATE_ELMREF, false, true);
        mdlScanCriteria_addSingleElementTypeTest (sc, SHAPE_ELM); 
        mdlScanCriteria_setElemRefCallback (sc, ModifyTagsScanCallback, NULL);
        mdlScanCriteria_scan (sc, NULL, NULL, NULL);
        mdlScanCriteria_free (sc);
        
        return status;        
        }

Children
  • Glad to see this finally work but that last bit very much confuses me.  This feels like bug in TagElementHandler::SetAttributeValue() to me.  Perhaps Brien can explain it to us.

    When you modify the element in memory, the EditElementHandle's ElementRef is cleared to make sure any access to the element data always sees the modified state and not the persistent state. The presence of the ElementRef signifies that an ElementHandle, that has a descr still represents an exact image of the persistent element.

    The standard way of using EditElementHandle (and what is used for DgnElementSetTool) is as follows:

    1) Get and save ElementRef.

    2) Create EditElementHandle from the ElementRef.

    3) Call ReplaceInModel using saved ElementRef from 1.

    Keep in mind that replace can be called with an entirely different element, ex. an insert vertex command could create a brand new LINE_STRING_ELM using a LINE_ELM as a template to preserve linkages, but the LINE_STRING_ELM was never persistent. Creating an EditElementHandle where the ElementRef refers to the LINE_ELM. but the descr refers to a LINE_STRING_ELM would be a recipe for chaos...

    HTH

    -B