7.0 KiB
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::Invokerejects 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 objectparameters map naturally toVARIANT*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 0–2 (3 methods)
- IDispatch: slots 3–6 (4 methods)
- Base interface methods (IEdmObject5 … IEdmFile12): slots 7–53 (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 7–53, 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) |