PDM Integration Guide¶
This guide shows how to build a PDM (Product Data Management) connector. The example uses a fictional "Acme Vault" PDM system, but the pattern applies to any PDM: Windchill, EPDM/PDM Standard, Meridian, SharePoint with locking, etc.
Which capabilities to implement¶
A full PDM integration uses these capabilities:
| Capability | Method | Purpose |
|---|---|---|
getFileStatus |
GetFileStatus |
Show lock status when a file is opened |
checkOut |
CheckOut |
Lock the file for editing |
checkIn |
CheckIn |
Release the lock and create a new version |
saveFile |
SaveFile |
Upload the saved file to the vault |
A minimal PDM connector only needs getFileStatus and checkIn/checkOut. saveFile is only needed if HydroSym's local file also needs to be synced to the vault on save.
Declare capabilities and menus¶
public override GetInfoResponse GetInfo()
{
return new GetInfoResponse
{
Name = "Acme Vault Connector",
Vendor = "Acme Engineering B.V.",
Version = "1.2.0",
ApiVersion = 1,
Capabilities = ["getFileStatus", "checkOut", "checkIn", "saveFile"],
HasSettings = true,
Menus =
[
new MenuItem
{
Id = "vault-checkout",
Label = "Check &Out from Vault",
Location = "file",
Icon = "checkout",
Shortcut = "Ctrl+Shift+O",
RequiresOpenProject = true,
},
new MenuItem
{
Id = "vault-checkin",
Label = "Check &In to Vault",
Location = "file",
Icon = "checkin",
Shortcut = "Ctrl+Shift+I",
RequiresOpenProject = true,
},
new MenuItem
{
Id = "vault-status",
Label = "Show Vault Status",
Location = "file",
Icon = "info",
RequiresOpenProject = true,
},
],
};
}
Authentication¶
Use Initialize to authenticate. If your PDM uses Windows authentication (common in enterprise environments), use windowsUser and windowsDomain:
private AcmeVaultClient _vault = null!;
public override void Initialize(InitializeRequest request)
{
var config = IniFile.Load(request.ConfigPath);
string serverUrl = config["Vault", "ServerUrl"];
// Windows-integrated auth — no password needed
_vault = new AcmeVaultClient(serverUrl,
windowsUser: request.WindowsUser,
domain: request.WindowsDomain);
// Test the connection
_vault.Ping(); // throws on failure
}
For form-based login, show a login dialog in Initialize using mainWindowHandle as the parent. Store the session token for the lifetime of the session.
GetFileStatus¶
public override GetFileStatusResponse GetFileStatus(GetFileStatusRequest request)
{
var vaultInfo = _vault.QueryFile(request.FilePath);
if (vaultInfo is null)
return new GetFileStatusResponse { Status = "unknown", IsManaged = false };
return new GetFileStatusResponse
{
Status = vaultInfo.IsCheckedOut ? "checkedOut" : "checkedIn",
LockedBy = vaultInfo.CheckedOutBy,
Version = vaultInfo.Version,
IsManaged = true,
};
}
Valid status values: unknown, checkedIn, checkedOut, released.
CheckOut¶
public override CheckOutResponse CheckOut(CheckOutRequest request)
{
try
{
var result = _vault.CheckOut(request.FilePath);
return new CheckOutResponse
{
LockedBy = result.CheckedOutBy,
Version = result.Version,
Message = $"Checked out version {result.Version}.",
};
}
catch (VaultConflictException ex)
{
throw new PluginException("CONFLICT",
$"File is already checked out by {ex.LockedBy}.");
}
}
CheckIn¶
public override CheckInResponse CheckIn(CheckInRequest request)
{
var result = _vault.CheckIn(request.FilePath, comment: request.Comment ?? "");
return new CheckInResponse
{
NewVersion = result.NewVersion,
Message = $"Checked in as version {result.NewVersion}.",
};
}
Dynamic menu state¶
Disable "Check Out" when the file is already checked out by the current user, and disable "Check In" when it is not checked out:
private readonly HashSet<string> _checkedOutByMe = new(StringComparer.OrdinalIgnoreCase);
public override GetMenuItemStateResponse GetMenuItemState(GetMenuItemStateRequest request)
{
bool hasProject = request.Context.ProjectPath is not null;
bool isCheckedOutByMe = hasProject &&
_checkedOutByMe.Contains(request.Context.ProjectPath!);
return new GetMenuItemStateResponse
{
States = new()
{
["vault-checkout"] = new() { Enabled = hasProject && !isCheckedOutByMe },
["vault-checkin"] = new() { Enabled = isCheckedOutByMe },
["vault-status"] = new() { Enabled = hasProject },
}
};
}
Update _checkedOutByMe in CheckOut and CheckIn.
Manual check-out/check-in via menu¶
public override OnMenuActionResponse OnMenuAction(OnMenuActionRequest request)
{
return request.MenuItemId switch
{
"vault-checkout" => HandleCheckOut(request),
"vault-checkin" => HandleCheckIn(request),
"vault-status" => HandleShowStatus(request),
_ => throw new PluginException("NOT_FOUND",
$"Unknown menu item: {request.MenuItemId}")
};
}
private OnMenuActionResponse HandleCheckOut(OnMenuActionRequest request)
{
if (request.Context.ProjectPath is null)
throw new PluginException("VALIDATION_ERROR", "No project is open.");
_vault.CheckOut(request.Context.ProjectPath);
_checkedOutByMe.Add(request.Context.ProjectPath);
return new OnMenuActionResponse(); // resultType=none
}
private OnMenuActionResponse HandleShowStatus(OnMenuActionRequest request)
{
var status = _vault.QueryFile(request.Context.ProjectPath!);
var dialog = new VaultStatusDialog(status)
{
Owner = NativeWindow.FromHandle(new IntPtr(request.ParentWindowHandle))
};
dialog.ShowDialog();
return new OnMenuActionResponse();
}
Complete example¶
A minimal but working PDM connector is included in the SDK repository at samples/AcmeVaultConnector/. It uses an in-memory mock vault backend and demonstrates all the patterns above with xUnit tests.