It's indeed tricky getting all the details sorted but it is possible to make it work properly:

(Minimal) Example: https://gist.github.com/p2or/4635887cfe6ed9e652eadc8aa7585a87
Data Structure
I'd suggest create a nested collection for your objects as part of the main collection (users), register e.g. per scene to get your data structure working and then test using the Console in the first place:
class CUSTOM_ObjectProps(PropertyGroup):
obj_ref: PointerProperty(
name="Object",
type=bpy.types.Object)
class CUSTOM_NameProps(PropertyGroup):
object_collection: CollectionProperty(
name = "Object Collection",
type = CUSTOM_ObjectProps)
class CUSTOM_SceneProps(PropertyGroup):
user_collection: CollectionProperty(
name = "Name Collection",
type = CUSTOM_NameProps)
...
def register():
bpy.types.Scene.ctm = PointerProperty(type=CUSTOM_SceneProps)
...
Proof using the Console
# Add User
>>> user = C.scene.ctm.user_collection.add()
>>> user.name = "Bob"
>>> user
bpy.data.scenes['Scene'].ctm.user_collection[0]
>>> user_obj = C.scene.ctm.user_collection[0].object_collection.add()
>>> user_obj.obj_ref = C.object
>>> user_obj.name = "My Object"
>>> user_obj
bpy.data.scenes['Scene'].ctm.user_collection[0].object_collection[0]
>>> user_obj.obj_ref
bpy.data.objects['Mball']
To speed up testing, operators for adding or printing the values might become handy:
class CUSTOM_OT_PrintOperator(bpy.types.Operator):
"""Tooltip"""
bl_idname = "scene.ctm_print_operator"
bl_label = "Simple Print Operator"
def execute(self, context):
scn = context.scene
custom = scn.ctm
lookup_dict = {}
for uc, user in enumerate(custom.user_collection):
print ("User Index: {}, Name: {}, Ref: {}".format(uc, user.name, user))
for oc, obj in enumerate(user.object_collection):
print ("-> Object {}, Ref {}".format(oc, obj))
lookup_dict.setdefault(user, []).append(obj.obj_ref)
# In case, dicts are nice
print ("\nLookup:", lookup_dict)
return {'FINISHED'}
Proof using the Console
>>> bpy.ops.scene.ctm_print_operator()
User Index: 0, Name: Bob, Ref: <bpy_struct, CUSTOM_NameProps("Bob")>
-> Object 0, Ref <bpy_struct, CUSTOM_ObjectProps("Bob_Mball")>
-> Object 1, Ref <bpy_struct, CUSTOM_ObjectProps("Bob_Icosphere")>
Lookup: {bpy.data.scenes['Scene'].ctm.user_collection[0]: [bpy.data.objects['Mball'], bpy.data.objects['Icosphere']]}
{'FINISHED'}
Drawing
Hardest part is getting both lists to draw
properly. Since the actual names (from the first list) don't necessarily have to be unique, we obviously can not rely on any name value. One approach is introducing a custom index property to get track of the changes and keep it in sync (required for template_list
anyway).

class CUSTOM_NameProps(PropertyGroup):
active_obj_index: IntProperty()
object_collection: CollectionProperty(
name = "Object Collection",
type = CUSTOM_ObjectProps)
user = custom.user_collection[custom.active_user_index]
...
layout.row().template_list(
listtype_name = "CUSTOM_UL_Sub",
list_id = "",
dataptr = user,
propname = "object_collection",
active_dataptr = user,
active_propname = "active_obj_index",
rows = min_rows
)
Notice, when using the wrong index value pointing to any non existing item will raise exceptions.