Skip to content

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.