Files
pdm/BATCH_NOTES.md

7.0 KiB
Raw Blame History

Batch Workflow Transition — Implementation Notes

The Problem

IEdmFile5::ChangeState(toStateID, folderID, comment, flags) only accepts a destination state ID. When a vault has multiple transitions leading from State A → State B, the API picks one arbitrarily.

In this project, there were 3 transitions from "Under Editing" → "Approved" (IDs 8, 83, 268). The API consistently selected ID 8 or 83, both of which had folder-location conditions that failed silently — the call returned no error but the file never moved states.


The Solution: IEdmFile13::ChangeState3

The PDM API provides a newer method that accepts both a destination state ID and a specific transition ID:

void ChangeState3(
    ref object poStateIdOrName,       // destination state ID or name
    ref object poTransitionIdOrName,  // specific transition ID or name  ← key parameter
    int lFolderID,
    string bsComment,
    int lParentWnd,
    int lEdmStateFlags,
    string bsPasswd
)

By passing the specific transition ID (268, the admin "AA" transition with no conditions), the ambiguous transitions are bypassed entirely.


Why ChangeState3 Was Difficult to Call

ChangeState3 is marked [restricted] in the PDM type library. This means:

  • It has a dispatch ID (DISPID 48) in the type library.
  • The COM object's IDispatch::Invoke rejects it with "Member not found" regardless of how it is called through normal IDispatch.
  • It can only be called via the COM vtable directly — not through Python's win32com/IDispatch path.

Why [restricted] Was Used

The [restricted] flag in COM/IDL signals: "this method is for vtable callers (C++/.NET), not scripting clients (VBA, VBScript, Python via IDispatch)."

The primary reason is the parameter types. ChangeState3 takes VARIANT* (by-reference variants), which are awkward to marshal cleanly through IDispatch::Invoke. Rather than risk broken behavior from scripting clients passing wrong types, the method was restricted to vtable-only access.

Who the API Was Designed For

SolidWorks PDM's primary API consumers are C++ and .NET:

  • In C++, vtable methods are called directly with no additional effort.
  • In .NET, ref object parameters map naturally to VARIANT* and the interop layer handles everything transparently.
  • Python via win32com was never a first-class target. The [restricted] flag is invisible to C++/.NET developers and so was likely never considered a problem.

Why There Is No Simpler Alternative

ChangeState (original, no number) came first and was widely used, so it was kept for compatibility. ChangeState3 was added as the correct replacement for cases involving multiple transitions to the same state, but was never made scripting-accessible. This is a common rough edge with older Windows COM APIs designed primarily for C++.


How We Called It From Python

Step 1 — Find the vtable offset

Inspected the win32com gen_py stub file generated by EnsureDispatch("ConisioLib.EdmVault"):

C:\Users\youngwa\AppData\Local\Temp\gen_py\3.12\
    5FA2C692-8393-4F31-9BDB-05E6F807D0D3x0x5x27\IEdmFile13.py

That file contains IEdmFile13_vtables_ which lists:

IEdmFile13_vtables_ = [
    (('ChangeState3', ...), 48, (..., 432, ...), )),
]
  • DISPID: 48
  • oVft (vtable byte offset): 432 → slot 54
    • IUnknown: slots 02 (3 methods)
    • IDispatch: slots 36 (4 methods)
    • Base interface methods (IEdmObject5 … IEdmFile12): slots 753 (47 methods)
    • ChangeState3: slot 54

Step 2 — Find the IEdmFile13 interface IID

Also from IEdmFile13.py:

class IEdmFile13(DispatchBaseClass):
    CLSID = IID('{DB0646C9-9E3F-4EA2-93AA-EB6584D268E2}')

IEdmFile13 IID: {DB0646C9-9E3F-4EA2-93AA-EB6584D268E2}

Step 3 — Define the interface in comtypes

Built a comtypes class inheriting from IDispatch, with 47 placeholder methods to occupy slots 753, then ChangeState3 at slot 54:

class _IEdmFile13_CT(CT_IDispatch):
    _iid_     = GUID("{DB0646C9-9E3F-4EA2-93AA-EB6584D268E2}")
    _methods_ = [COMMETHOD([], HRESULT, f"_ph{i}") for i in range(47)] + [
        COMMETHOD(
            [], HRESULT, "ChangeState3",
            (["in"], POINTER(VARIANT), "poStateIdOrName"),
            (["in"], POINTER(VARIANT), "poTransitionIdOrName"),
            (["in"], c_long,           "lFolderID"),
            (["in"], c_wchar_p,        "bsComment"),
            (["in"], c_long,           "lParentWnd"),
            (["in"], c_long,           "lEdmStateFlags"),
            (["in"], c_wchar_p,        "bsPasswd"),
        ),
    ]

Step 4 — Extract the raw COM pointer

Used ctypes to read the raw IEdmFile13* out of the pythoncom wrapper's memory. CPython 64-bit stores the COM pointer at offset 16 in the Python object struct:

py_disp  = file_obj._oleobj_.QueryInterface(pythoncom.MakeIID(IID_IEdmFile13))
raw_ptr  = ctypes.c_uint64.from_address(id(py_disp) + 16).value
ct_unk   = ctypes.cast(raw_ptr, ctypes.POINTER(comtypes.IUnknown))
file13   = ct_unk.QueryInterface(_IEdmFile13_CT)   # properly AddRef'd

Step 5 — Call ChangeState3

Constructed VARIANT structs (VT_I4) for the state and transition IDs, then called through the vtable:

v_state = _make_i4_variant(to_state_id)
v_trans = _make_i4_variant(transition_id)

file13.ChangeState3(
    ctypes.byref(v_state),
    ctypes.byref(v_trans),
    ctypes.c_long(folder_id),
    comment,
    ctypes.c_long(0),
    ctypes.c_long(0),
    password,          # PDM login password — required by this transition
)

HRESULT 0x00000000 = success.


How Transition IDs Are Resolved in the Batch Script

No IDs are hardcoded. The script resolves the transition by name at runtime:

# In transition_file() — batch_workflows_paths.py
while not trans_pos.IsNull:
    transition = current_state.GetNextTransition(trans_pos)
    if transition.Name.lower() == transition_name.lower():
        target_transition = transition   # .ID and .ToState.ID are both available
        break

The user passes --transition "AA" (or whatever the transition name is), and the script passes target_transition.ID directly to ChangeState3. This means the same script works for any transition name, including ones where multiple transitions lead to the same destination state.


Key Files

File Purpose
helpers/batch_workflows_paths.py Production batch script — run this
helpers/test_batch_api.py Diagnostic/prototype used during development
documentation/API_GB.chm PDM API reference (local copy)

PDM Type Library Reference

Item Value
TypeLib GUID {5FA2C692-8393-4F31-9BDB-05E6F807D0D3}
TypeLib version 5.27
TypeLib name PDMWorks Enterprise 2024 Type Library
IEdmFile13 IID {DB0646C9-9E3F-4EA2-93AA-EB6584D268E2}
ChangeState3 DISPID 48 (restricted — not callable via IDispatch)
ChangeState3 vtable offset 432 bytes (slot 54)