You add a Syncfusion data grid to your SolidWorks add-in’s WPF panel. It compiles. The DLLs are in your output folder. You launch SolidWorks, and the add-in loads. Then you open the panel and get this:

System.Windows.Markup.XamlParseException: 
  Could not load file or assembly 'Syncfusion.SfGrid.WPF, Version=27.2.2.0, 
  Culture=neutral, PublicKeyToken=3d67ed1f87d44c89' or one of its dependencies.

Everything compiled. The DLLs are right there in bin\Debug. NuGet copied them. So why can’t WPF find them?

The answer involves a fundamental mismatch between how the .NET CLR probes for assemblies and where COM add-ins actually live on disk. We decompiled SolidWorks’ native add-in loader, examined the WPF BAML reader source code, and inspected the CLR’s assembly resolution pipeline to understand exactly why this happens — and how to fix it with five lines of code.

The Two Worlds of Assembly Resolution

To understand the bug, you need to understand that your SolidWorks add-in lives in two worlds simultaneously — and they resolve assemblies differently.

World 1: Your C# Code (Works Fine)

When your C# code creates a Syncfusion control:

var grid = new SfDataGrid();
grid.ItemsSource = myData;

The compiler has already resolved the type reference at build time. The JIT compiler knows where the assembly is because MSBuild placed it in your output directory and the CodeBase registry entry (written by regasm /codebase) tells the CLR exactly where your add-in DLL lives. Dependencies in the same directory are found through the standard probing sequence.

This works because the CLR can trace the dependency chain from your add-in DLL to its neighbors.

World 2: Your XAML (Breaks)

When XAML references the same control:

<syncfusion:SfDataGrid ItemsSource="{Binding Data}" />

The resolution path is completely different. XAML is compiled into BAML (Binary Application Markup Language) at build time, but types are resolved at runtime through Assembly.Load(assemblyName). This method doesn’t know about your add-in’s directory. It probes the host application’s base directory — and for a COM add-in, that’s C:\Program Files\SOLIDWORKS Corp\SOLIDWORKS\, not your add-in folder.

The Syncfusion DLL is sitting in C:\Users\You\AppData\...\MyAddin\bin\Debug\. The CLR is looking in C:\Program Files\SOLIDWORKS Corp\SOLIDWORKS\. It will never find it.

Why the Application Base Is Wrong

When SolidWorks loads your .NET add-in, the entire process starts with COM activation. Here’s what happens inside SLDWORKS.exe at startup:

  1. CSwAddinManager::loadAllAddins() in sldappu.dll reads the registry at HKLM\SOFTWARE\SolidWorks\AddIns\ and enumerates every CLSID subkey.

  2. For each enabled add-in, SolidWorks calls CoCreateInstance with CLSCTX_INPROC_SERVER.

  3. Windows looks up HKCR\CLSID\{GUID}\InprocServer32 and finds mscoree.dll — the .NET Framework CLR shim. For native C++ add-ins (like SolidWorks Composer), this would point to the actual DLL. For managed add-ins, it always points to mscoree.dll.

  4. mscoree.dll reads the Assembly, Class, RuntimeVersion, and CodeBase values from the same registry key. It bootstraps the CLR (version v4.0.30319) and instantiates your class.

  5. SolidWorks queries for ISwAddin and calls ConnectToSW.

The critical detail is step 4. The CLR is hosted inside SLDWORKS.exe. The AppDomain.CurrentDomain.BaseDirectory — the “application base” that the CLR uses as the root for all assembly probing — is the directory containing SLDWORKS.exe:

C:\Program Files\SOLIDWORKS Corp\SOLIDWORKS\

Not your add-in’s directory. Not the CodeBase path from the registry. The SolidWorks install folder.

The CodeBase registry entry only helps locate your add-in assembly itself. It does not change the probing root for your add-in’s dependencies. Once your DLL is loaded, any Assembly.Load() calls probe from the host’s application base.

We confirmed this by examining sldappu.dll exports. SolidWorks calls CLRCreateInstance (imported from mscoree.dll) to initialize the CLR with the ICLRMetaHost interface. The function startup_SetBindAsLegacyV2Runtime configures legacy binding mode. There is no sldworks.exe.config file — no <probing privatePath>, no <assemblyBinding> redirects, no configuration that would extend the probing path to include add-in directories.

How BAML Triggers the Failure

Now we get to the mechanism that makes WPF fail where regular C# code succeeds.

When MSBuild compiles your .xaml files, the PresentationBuildTask converts each one into BAML and embeds it as a resource. Inside the BAML binary, assembly references are stored as AssemblyInfoRecord entries — each containing the full assembly name (name, version, culture, public key token) and a numeric ID. Type references use TypeInfoRecord entries that pair the assembly ID with a CLR type name.

At runtime, when your WPF UserControl is instantiated, InitializeComponent() calls Application.LoadComponent(), which feeds the BAML stream to Baml2006Reader. The reader encounters each type reference and asks Baml2006SchemaContext to resolve it.

Here is the actual resolution method from the WPF source code (PresentationFramework.dll):

private Assembly ResolveAssembly(BamlAssembly bamlAssembly)
{
    // Step 1: Check assemblies already loaded in AppDomain
    bamlAssembly.Assembly = SafeSecurityHelper.GetLoadedAssembly(assemblyName);

    if (bamlAssembly.Assembly == null)
    {
        // Step 2: Assembly.Load with fully-qualified name
        bamlAssembly.Assembly = Assembly.Load(assemblyName.FullName);

        if (bamlAssembly.Assembly == null)
        {
            // Step 3: Retry with short name + public key token
            AssemblyName shortName = new AssemblyName(assemblyName.Name);
            shortName.SetPublicKeyToken(publicKeyToken);
            bamlAssembly.Assembly = Assembly.Load(shortName);
        }
    }
    return bamlAssembly.Assembly;
}

Every call to Assembly.Load(assemblyName) goes through the CLR’s standard probing sequence:

StepWhat the CLR checksResult for COM add-in
1Global Assembly Cache (GAC)Not found (NuGet packages aren’t GAC-installed)
2ApplicationBase directoryProbes C:\Program Files\SOLIDWORKS Corp\SOLIDWORKS\Syncfusion.SfGrid.WPF.dll — not found
3ApplicationBase subdirectoryProbes ...\SOLIDWORKS\Syncfusion.SfGrid.WPF\Syncfusion.SfGrid.WPF.dll — not found
4PrivateBinPath directoriesNone configured (no sldworks.exe.config)
5AssemblyResolve eventNo handler registered (default)
6FailureFileNotFoundException wrapped in XamlParseException

The DLLs are in your add-in’s output folder. The CLR never looks there.

Why C# References Don’t Have This Problem

When your C# code calls new SfDataGrid(), the JIT compiler resolves the assembly reference — but it does so through a different path. The metadata in your compiled assembly contains a direct reference to Syncfusion.SfGrid.WPF. The CLR’s loader sees that your assembly was loaded from C:\Users\...\MyAddin\bin\Debug\ (via the CodeBase registry entry), and it probes for dependencies relative to where the requesting assembly was loaded from. This is the “load context” mechanism — assemblies loaded via CodeBase or Assembly.LoadFrom carry their load path with them.

BAML doesn’t get this benefit. Baml2006SchemaContext.ResolveAssembly() calls Assembly.Load(string) with just an assembly name — no path context. This puts it in the “default load context,” which probes only the application base and GAC.

The Intermittent Failure Pattern

This bug has a maddening characteristic: it sometimes works.

If your solution has multiple projects, and one of them happens to reference and use the Syncfusion assembly in C# code before the WPF panel loads, the assembly gets loaded into the AppDomain. When Baml2006SchemaContext.ResolveAssembly() runs, step 1 — GetLoadedAssembly() — finds it already loaded and returns it immediately, skipping the Assembly.Load() call entirely.

This creates a dependency on load order:

  • Debug session with multiple projects: Project A loads Syncfusion.SfGrid.WPF via C# code. Your add-in’s WPF panel loads later. BAML finds the assembly already in the AppDomain. Everything works.

  • Clean build, single project, or different load order: No other code has loaded the assembly yet. BAML calls Assembly.Load(). The CLR probes the SolidWorks directory. Crash.

  • After a solution restructure: You moved code between projects or changed build dependencies. The load order shifted. The bug reappears “randomly.”

This is why developers report the bug as intermittent, build-dependent, or triggered by seemingly unrelated changes. It’s not random — it’s a race condition on assembly loading order within the AppDomain.

The Fix: AppDomain.AssemblyResolve

The solution is to register an AssemblyResolve event handler that tells the CLR where to find assemblies when standard probing fails. This handler fires as the last step in the probing sequence — after GAC, application base, and private paths have all been checked.

Register it in your add-in’s constructor or at the very beginning of ConnectToSW, before any WPF type is referenced:

[Guid("AA151FF9-531B-46F3-B961-4BCBC9BC56F6")]
[ComVisible(true)]
public class MyAddin : ISwAddin
{
    public MyAddin()
    {
        AppDomain.CurrentDomain.AssemblyResolve += OnAssemblyResolve;
    }

    private static Assembly OnAssemblyResolve(object sender, ResolveEventArgs args)
    {
        // Skip resource assemblies
        if (args.Name.Contains(".resources"))
            return null;

        string addinDir = Path.GetDirectoryName(
            Assembly.GetExecutingAssembly().Location);
        string dllPath = Path.Combine(addinDir,
            new AssemblyName(args.Name).Name + ".dll");

        return File.Exists(dllPath)
            ? Assembly.LoadFrom(dllPath)
            : null;
    }

    public bool ConnectToSW(object ThisSW, int Cookie)
    {
        // Safe to create WPF controls now — handler is registered
        // ...
    }
}

Why This Works

  1. When BAML calls Assembly.Load("Syncfusion.SfGrid.WPF, ...") and the CLR fails to find it in the GAC or application base, the AssemblyResolve event fires.

  2. The handler extracts the assembly name from the event args, builds a path to the add-in’s own directory, and checks if the DLL exists there.

  3. If found, it loads the assembly using Assembly.LoadFrom() — not Assembly.Load(). This is critical: LoadFrom accepts a file path and doesn’t trigger the AssemblyResolve event recursively. Using Assembly.Load() inside the handler would cause a StackOverflowException.

  4. If not found, the handler returns null, allowing other handlers or the default failure behavior to proceed.

Implementation Rules

RuleWhy
Register in the constructor, not ConnectToSWThe JIT compiler may trigger assembly loads when compiling ConnectToSW if it references WPF types
Use Assembly.LoadFrom, never Assembly.LoadAssembly.Load inside the handler triggers infinite recursion
Return null for unknown assembliesOther add-ins or the host may have their own handlers
Filter out .resources assembliesSatellite assemblies for localization use a different resolution path
Use Assembly.GetExecutingAssembly().LocationGetEntryAssembly() returns SLDWORKS.exe — useless for finding your add-in’s directory

Diagnosing Assembly Binding Failures

If you’re not sure which assemblies are failing to load, the Fusion Log Viewer (fuslogvw.exe) shows exactly what the CLR tried and where it looked.

Run it as administrator from the Developer Command Prompt and enable “Log bind failures to disk.” Then reproduce the crash in SolidWorks. The log will show:

LOG: Appbase = file:///C:/Program Files/SOLIDWORKS Corp/SOLIDWORKS/
LOG: Initial PrivatePath = NULL
...
LOG: Attempting download of new URL 
     file:///C:/Program Files/SOLIDWORKS Corp/SOLIDWORKS/Syncfusion.SfGrid.WPF.DLL.
LOG: Attempting download of new URL 
     file:///C:/Program Files/SOLIDWORKS Corp/SOLIDWORKS/Syncfusion.SfGrid.WPF/
     Syncfusion.SfGrid.WPF.DLL.
LOG: All probing URLs attempted and failed.

The Appbase line confirms the problem immediately — it shows the SolidWorks directory, not your add-in’s directory.

You Still Need the NuGet References

The AssemblyResolve handler fixes the runtime loading problem. But you still need proper NuGet PackageReference entries (or direct assembly references) in your .csproj for two reasons:

Build-time correctness: MSBuild needs the references to compile your XAML and C# code. Without them, the BAML compiler can’t resolve the clr-namespace mappings and the C# compiler can’t resolve type references.

Deployment: If you’re building an installer with WiX, the heat.exe harvester walks your output directory to find DLLs to include. Copy Local = True ensures the third-party DLLs land in your output folder where heat.exe can find them. Without them, your installer will ship without the dependencies and the AssemblyResolve handler will have nothing to resolve to.

The relationship between NuGet references and AssemblyResolve is complementary:

ConcernNuGet / PackageReferenceAssemblyResolve Handler
Compile-time type resolutionYesNo
DLLs copied to output folderYes (Copy Local)No
WiX installer harvestingYesNo
Runtime loading inside SolidWorksNo (wrong probing path)Yes
Runtime loading in standalone appYes (app is the host)Not needed

This Isn’t Just a SolidWorks Problem

Every COM-hosted .NET add-in that uses WPF with third-party controls hits this same issue. The pattern is identical:

  • Office VSTO add-ins: The host is WINWORD.EXE or EXCEL.EXE. The application base is C:\Program Files\Microsoft Office\root\Office16\.
  • AutoCAD .NET plugins: The host is acad.exe. The application base is C:\Program Files\Autodesk\AutoCAD 2026\. A Syncfusion forum post documents the exact same FileNotFoundException pattern with AutoCAD plugins.
  • Revit add-ins: The host is Revit.exe. Same issue.
  • Any COM-activated .NET assembly: If the host process is not your application, the application base is wrong for your dependencies.

The AssemblyResolve handler pattern works in all of these scenarios. The only thing that changes is the host process name in the Fusion Log.

Interestingly, WinForms controls do not have this problem in the same way. WinForms uses Type.GetType() and direct assembly references rather than BAML’s Assembly.Load() path. If you swap a Syncfusion WPF grid for a Syncfusion WinForms grid in the same add-in, the WinForms version works without AssemblyResolve. This confirms the issue is specifically in the BAML/XAML type resolution path.

The SolidWorks Add-In Loading Pipeline

For reference, here is the complete add-in loading sequence as we reconstructed it from sldappu.dll exports and the SolidWorks API documentation:

SLDWORKS.exe startup
  └─ CAmApp::startup_SetBindAsLegacyV2Runtime()     ← configures CLR legacy mode
  └─ CAmApp::startup_loadAddIns()
       └─ CSwAddinManager::loadAllAddins()
            └─ [for each CLSID in HKLM\SOFTWARE\SolidWorks\AddIns\]
                 └─ CSwAddinManager::loadAddIn(GUID, int)
                      └─ CoCreateInstance(CLSID, CLSCTX_INPROC_SERVER)
                           └─ mscoree.dll (CLR shim)
                                ├─ Reads Assembly, Class, CodeBase from registry
                                ├─ CLRCreateInstance → ICLRMetaHost
                                ├─ Bootstraps CLR v4.0.30319
                                ├─ Loads your DLL from CodeBase path
                                └─ Creates your ISwAddin class instance
                      └─ QueryInterface(IID_ISwAddin)
                      └─ ISwAddin::ConnectToSW(ISldWorks, cookie)
                           └─ [Your code runs here — WPF panels created]
                                └─ InitializeComponent()
                                     └─ Application.LoadComponent(uri)
                                          └─ Baml2006Reader
                                               └─ Baml2006SchemaContext.ResolveAssembly()
                                                    └─ Assembly.Load("ThirdParty.WPF, ...")
                                                         ├─ GAC? No
                                                         ├─ C:\...\SOLIDWORKS\ThirdParty.WPF.dll? No
                                                         ├─ PrivatePath? None configured
                                                         └─ AssemblyResolve event
                                                              └─ YOUR HANDLER → LoadFrom add-in dir ✓

The clrAssemblyLoader_c class and clrAssemblyEntry_c class in sldappu.dll mediate between the native COM activation layer and the CLR’s assembly loading infrastructure. SolidWorks also has CSwAddinManager::loadAddinFromfile() for direct file-based loading and forceLoadAddIn() for bypassing the normal enable/disable check.

Troubleshooting Checklist

When a SolidWorks add-in throws XamlParseException wrapping FileNotFoundException on a WPF panel, run through this checklist in order:

1. Confirm the application base is wrong

Open Fusion Log Viewer (fuslogvw.exe) as administrator. Enable “Log bind failures to disk.” Reproduce the crash. Find the log entry and confirm the Appbase line shows C:\Program Files\SOLIDWORKS Corp\SOLIDWORKS\ — not your add-in directory. If it shows a different path, the failure has a different root cause.

2. Verify Copy Local = True for the failing assembly

In Visual Studio, expand the NuGet reference in Solution Explorer. Select the failing DLL. In Properties, confirm Copy Local is True. If it was False, the DLL is missing from your output folder and AssemblyResolve cannot find it even with a handler registered.

3. Check that AssemblyResolve is registered in the constructor, not ConnectToSW

// WRONG — JIT may resolve WPF types before ConnectToSW runs
public bool ConnectToSW(object ThisSW, int Cookie)
{
    AppDomain.CurrentDomain.AssemblyResolve += OnAssemblyResolve; // too late
    var panel = new MyWpfPanel(); // crashes here
}

// CORRECT — constructor runs before any type resolution
public MyAddin()
{
    AppDomain.CurrentDomain.AssemblyResolve += OnAssemblyResolve;
}

4. Confirm your handler uses Assembly.LoadFrom, not Assembly.Load

Assembly.Load inside the handler triggers the resolve event recursively, causing a StackOverflowException. Assembly.LoadFrom accepts a file path and bypasses the normal probing sequence — that’s the behavior you need.

5. Check for satellite assembly interference

If your third-party control ships localization DLLs (Syncfusion.SfGrid.WPF.resources.dll in a en-US\ subfolder), the resolve event fires for those too. The .resources filter in the handler — if (args.Name.Contains(".resources")) return null; — lets the CLR’s normal satellite assembly resolution handle those. Remove the filter and satellite resolution breaks.

6. Rule out version mismatches

The resolve event fires with the fully-qualified assembly name including version. If your handler finds a DLL on disk but the version doesn’t match what BAML expects, you’ll get a BadImageFormatException instead of FileNotFoundException. Check the version token in the Fusion Log against the DLL version in your output folder.

7. Check for duplicate handlers from multiple add-ins

AppDomain.CurrentDomain.AssemblyResolve is a global event. If two add-ins register handlers, both fire for every resolution failure. If the first handler returns a loaded assembly of the wrong version (because it searched its own directory and found a different version of the same DLL), the second add-in’s types may behave unexpectedly. Use the AssemblyName.Name to restrict each handler to only its own assemblies:

private static readonly HashSet<string> _ownedAssemblies = new(
    Directory.GetFiles(AddinDir, "*.dll")
             .Select(f => Path.GetFileNameWithoutExtension(f)));

private static Assembly OnAssemblyResolve(object sender, ResolveEventArgs args)
{
    if (args.Name.Contains(".resources")) return null;
    string shortName = new AssemblyName(args.Name).Name;
    if (!_ownedAssemblies.Contains(shortName)) return null; // not ours
    string path = Path.Combine(AddinDir, shortName + ".dll");
    return File.Exists(path) ? Assembly.LoadFrom(path) : null;
}

8. If none of the above applies: check whether the assembly is in the GAC

Some third-party controls offer a GAC installer. If an older version was GAC-installed and a newer version is in your output folder, Assembly.Load finds the GAC version first (step 1 of probing, before the resolve event fires). The version mismatch then fails at type resolution. Uninstall the GAC version or use Assembly.LoadFile with the exact path to force the correct version.

Takeaways

  • Your add-in runs inside SLDWORKS.exe. The CLR’s application base is the SolidWorks install directory, not your add-in’s folder. This is a consequence of the COM activation model — mscoree.dll hosts the CLR in the host process.

  • C# code references work because the JIT compiler resolves them in the load-from context. XAML/BAML uses Assembly.Load(assemblyName) which probes only the application base and GAC — a fundamentally different resolution path.

  • The bug is intermittent because it depends on load order. If another assembly happens to load the dependency into the AppDomain first, BAML finds it already loaded and skips the failing Assembly.Load call.

  • Register AppDomain.CurrentDomain.AssemblyResolve in your add-in constructor — before any WPF type is referenced. Use Assembly.LoadFrom(), never Assembly.Load(), inside the handler.

  • You still need NuGet references so MSBuild can compile the XAML, Copy Local can populate the output folder, and WiX can harvest the DLLs for your installer. The AssemblyResolve handler and NuGet references solve different halves of the same problem.

  • This affects every COM-hosted .NET add-in with WPF — not just SolidWorks. Office VSTO, AutoCAD, Revit, and any other COM host will exhibit the same behavior with the same fix.