Python

Named Groups

See this article for an introduction to Named Groups.

MicroStation: Named Groups dialog

Programming Named Groups

When developing code to deal with Named Groups, I found that NamedGroup class instances do not copy successfully. What you think should be a copy of a NamedGroup is something that crashes MicroStation. I developed class NamedGroupProxy to act as a proxy for a NamedGroup: it is a Python class that follows Python conventions and enables the examples to work as expected.

Named Group Proxy

My NamedGroupProxy class provides a way to preserve Named Group properties as you use them in your code. It steps past a number of problems raised by the implementation of the MicroStation Python NamedGroup class. It helps when you want to serialize Named Group data, perhaps to a JSON file, including handling the opaque NamedGroupFlags class.

Named Group Instance Problem

The NamedGroup has problems. It is supposed to be a wrapper around the C++ NamedGroup, but it misbehaves. By 'misbehave' I mean that, when you copy a NamedGroup, it may cause a MicroStation crash. In my examples, I convert a NamedGroupCollection to a Python list of NamedGroupProxy (list:[NamedGroupProxy]) as soon as possible. Subsequently, we work with those NamedGroupProxys.

Translating Named Group Flags

In my examples I write NamedGroup data to a JSON file and read from a JSON file. JSON understands only simple data types. One member of a NamedGroup is NamedGroupFlags, which is not a type that JSON understands. We must translate to something that JSON does understand, such as an int. Two functions are used to translate between NamedGroupFlags and an int …

Those functions were kindly provided by YongAn Fu, a Bentley Systems staff member who supports MicroStation developers. You'll find his posts on the MicroStation Programming Forum.

They are implemented as members of the NamedGroupProxy class.

Magic Methods

I wanted the NamedGroupProxy class to be sortable and to able to participate in a Python set. I added the following comparison and hash methods, sometimes called magic methods or dunder methods, to enable that requirement …

The entire class is decorated with @functools.total_ordering. That decorator supplies the remaining comparator classes automatically.

discuss_status function

Most NamedGroup methods return a NamedGroupStatus code. Method discuss_status takes that code and explains it using human-comprehensible words.


Named Group Proxy class code

You can obtain this source code from the download page.

from MSPyDgnPlatform import *
from MSPyMstnPlatform import *

from collections import namedtuple
import functools

@functools.total_ordering
class NamedGroupProxy():
    '''
    Python class acts as a proxy for a NamedGroup instance.

    Remarks: A NamedGroup instance doesn't copy and crashes MicroStation.  The proxy contains the essential information and is a normal Python class.

    Created: 03-Feb-2026
    Updated: 19-Feb-2026

    The primary purpose of this class is to interpret NamedGroup string (WString) values as Python strings,
    and preserve those values when the proxy is copied.

    That avoids the undesired behaviour of a NamedGroup, which misbehaves when copied.
    The cause of that behaviour is to do with translation of the underlying C++ code.

    When serializing NamedGroupFlags there is an explicit conversion function for NamedGroupFlags.
    NamedGroupFlags.__str()__ doesn't behave as it should.
    We store the flags as a Python int and convert to/from NamedGroupFlags as required.

    Decorator @total_ordering automatically creates all comparison operators that are not defined here (==, <).

    '''
    def __init__(self, name: str, descr: str, type: str, flags: int, dgn_model: DgnModel = ISessionMgr.ActiveDgnModelRef):
        self._dgn_model = dgn_model
        self._name = name
        self._flags = flags
        self._description = descr
        self._type = type
        self._present = False # Used when reading from JSON, informs us when this group is already defined in a DGN file

    # Class methods
    @classmethod
    def from_named_group(cls, ng: NamedGroup, dgn_model: DgnModel = ISessionMgr.ActiveDgnModelRef):
        assert ng is not None, "NamedGroupProxy.from_named_group - ng is None"
        return cls(str(ng.GetName()), str(ng.GetDescription()), str(ng.GetType()), NamedGroupProxy.NamedGroupFlags_to_int (ng.GetFlags()), dgn_model)

    @classmethod
    def from_name_and_model(cls, name: str, dgn_model: DgnModel = ISessionMgr.ActiveDgnModelRef):
        '''
        Attempt to find the NamedGroup with given name in the specified DGN model.

        Returns: freshly-minted NamedGroupProxy.
        '''
        groups = NamedGroupCollection(dgn_model)
        assert groups is not None, "NamedGroupProxy.from_name_and_model - groups is None"
        ng = groups.FindByName(name)
        assert ng is not None, f"NamedGroupProxy.from_name_and_model - NamedGroup '{name}' not found"
        return NamedGroupProxy.from_named_group (ng, dgn_model)

    def discuss_status(self, status: NamedGroupStatus)->NamedGroupStatus:
        '''
        Many NamedGroup functions return a eNG_Success value.  This method renders that status into human-readable text.

        Returns: The same NamedGroupStatus received from the caller.
        '''
        if eNG_Success == status:
            #print("Named Group operation succeeded")
            pass
        elif eNG_ClosedGroup == status:
            print(f"Can't add new Named Group '{self._name}' to existing closed group")
        elif eNG_FarReferenceDisallowed == status:
            print("Named Group far reference not allowed")
        elif eNG_DuplicateMember == status:
            print(f"Operation would create a duplicate group member '{self._name}'")
        elif eNG_BadMember == status:
            print(f"Bad Named Group member '{self._name}'")
        elif eNG_NameTooLong == status:
            print(f"Named Group name '{self._name}' is too long ")
        elif eNG_NameTooShort == status:
            print(f"Named Group name '{self._name}' is too short ")
        elif eNG_DescriptionTooLong == status:
            print(f"Named Group description '{self._name}' is too long ")
        elif eNG_TypeTooLong == status:
            print(f"Named Group type '{self._name}' is too long ")
        elif eNG_CircularDependency == status:
            print(f"Named Group '{name}' has circular dependency")
        elif eNG_CantCreateSubgroup == status:
            print(f"Can't create sub-group of Named Group '{self._name}' has circular dependency")
        elif  eNG_BadArg == status:
            print("Named Group bad argument")
        elif  eNG_NotNamedGroupElement == status:
            print(f"'{self._name}' is not a Named Group element")
        elif  eNG_FileReadOnly == status:
            print("File is read-only")
        elif   eNG_ExistsNotOverwriting == status:
            print(f"'{self._name}' exists: not overwriting")
        elif   eNG_NameNotUnique == status:
            print(f"'{self._name}' is not unique")
        elif   eNG_NotFound == status:
            print(f"'{self._name}' not found")
        elif    eNG_NotPersistent == status:
            print(f"'{name}' not persistent")
        elif     eNG_OperationInProgress == status:
            print("Named Group operation in progress")
        elif     eNG_IsFarReference == status:
            print("is far reference")
        return status

    def create_named_group(self, dgn_model: DgnModel = ISessionMgr.ActiveDgnModelRef)->(int, NamedGroup):
        '''
        Create a Named Group.

        Returns: (NamedGroupStatus, NamedGroup)
        '''
        (status, new_ng) = NamedGroup.Create(self._name, self._description, NamedGroupProxy.int_to_NamedGroupFlags(self._flags), dgn_model)
        self.discuss_status(status, self._name)
        return (status, new_ng)

    def add_to_file(self, dgn_model: DgnModel = ISessionMgr.ActiveDgnModelRef)->NamedGroupStatus:
        '''
        Create a Named Group and persist it in the specified DGN file.

        Returns: NamedGroupStatus
        '''
        status, ng = self.create_named_group(dgn_model)
        self.discuss_status(status, self._name)
        if eNG_Success == status:
            _OVERWRITE_EXISTING = True
            _DONT_OVERWRITE_EXISTING = not _OVERWRITE_EXISTING
            status = ng.WriteToFile(_DONT_OVERWRITE_EXISTING)
            self.discuss_status(status, self._name)
        return status

    # Equality tests enable you to sort a list of NamedGroupProxy or compare two NamedGroupProxy instances.
    def __eq__(self, other):
        '''
        Equality test implements case-insensitive text comparison of _name.
        '''
        return self._name.casefold() == other._name.casefold()

    def __lt__(self, other):
        '''
        Less-than test implements case-insensitive text comparison of _name.
        '''
        return self._name.casefold() < other._name.casefold()

    # Make instances hashable, so that they can participate in some Python operations e.g. set
    def __hash__(self):
        '''
        Each Named Group muat have a unique name, meaning it is suitable for hashing.
        '''
        return hash(self._name)

    # Properties
    @property
    def present(self)->bool:
        '''
        Returns the state of the 'present' flag.
        '''
        return self._present

    def set_present(self, state: bool = True):
        '''
        Set the state of the 'present' flag.
        '''
        self._present = state

    @property
    def name(self)->str:
        '''
        Get the name of a NamedGroup.

        Remarks: The text attributes of a NamedGroup are returns as Bentley.WString, which we convert to a Pything string.
        '''
        return self._name

    @property
    def description(self)->str:
        '''
        Get the description of a NamedGroup.

        Remarks: The text attributes of a NamedGroup are returns as Bentley.WString, which we convert to a Pything string.
        '''
        return str(self._description)

    @property
    def flags(self)->int:
        '''
        Get the flags of a NamedGroup.

        Remarks: The flag attributes of a NamedGroup.
        '''
        return self._flags

    @property
    def named_group_type(self)->str:
        '''
        Get the user-assigned type of a NamedGroup.

        Remarks: The text attributes of a NamedGroup are returns as Bentley.WString, which we convert to a Pything string.
        '''
        return self._type

	# NamedGroupFlag translation
    @classmethod
    def NamedGroupFlags_to_int(cls, flags: NamedGroupFlags) -> int:
        '''
        Class method interprets NamedGroupFlags as an integer.
        '''
        value = 0
        if flags.AllowDuplicates:
            value |= 1 << 0
        if flags.ExclusiveMembers:
            value |= 1 << 1
        if flags.AllowFarReferences:
            value |= 1 << 2
        if flags.Closed:
            value |= 1 << 3
        if flags.SelectMembers:
            value |= 1 << 4
        if flags.Anonymous:
            value |= 1 << 5
        return value

    @classmethod
    def int_to_NamedGroupFlags(cls, value: int) -> NamedGroupFlags:
        '''
        Class method interprets an integer as NamedGroupFlags.
        '''
        flags = NamedGroupFlags()
        flags.AllowDuplicates = bool(value & (1 << 0))
        flags.ExclusiveMembers = bool(value & (1 << 1))
        flags.AllowFarReferences = bool(value & (1 << 2))
        flags.Closed = bool(value & (1 << 3))
        flags.SelectMembers = bool(value & (1 << 4))
        flags.Anonymous = bool(value & (1 << 5))
        return flags

    def __str__(self)->str:
        summary = str(f"Name: '{self.name}' Descr: '{self.description}' Type: '{self.named_group_type}' Flags: {self._flags} Present: {self._present} " )
        return summary

Usage

For links to Python examples that show how NamedGroupProxy is used, see LA Solutions' NamedGroup Examples.

Questions

Post questions about MicroStation programming to the MicroStation Programming Forum.