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:
CSwAddinManager::loadAllAddins()insldappu.dllreads the registry atHKLM\SOFTWARE\SolidWorks\AddIns\and enumerates every CLSID subkey.For each enabled add-in, SolidWorks calls
CoCreateInstancewithCLSCTX_INPROC_SERVER.Windows looks up
HKCR\CLSID\{GUID}\InprocServer32and findsmscoree.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 tomscoree.dll.mscoree.dllreads theAssembly,Class,RuntimeVersion, andCodeBasevalues from the same registry key. It bootstraps the CLR (versionv4.0.30319) and instantiates your class.SolidWorks queries for
ISwAddinand callsConnectToSW.
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:
| Step | What the CLR checks | Result for COM add-in |
|---|---|---|
| 1 | Global Assembly Cache (GAC) | Not found (NuGet packages aren’t GAC-installed) |
| 2 | ApplicationBase directory | Probes C:\Program Files\SOLIDWORKS Corp\SOLIDWORKS\Syncfusion.SfGrid.WPF.dll — not found |
| 3 | ApplicationBase subdirectory | Probes ...\SOLIDWORKS\Syncfusion.SfGrid.WPF\Syncfusion.SfGrid.WPF.dll — not found |
| 4 | PrivateBinPath directories | None configured (no sldworks.exe.config) |
| 5 | AssemblyResolve event | No handler registered (default) |
| 6 | Failure | FileNotFoundException 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.WPFvia 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
When BAML calls
Assembly.Load("Syncfusion.SfGrid.WPF, ...")and the CLR fails to find it in the GAC or application base, theAssemblyResolveevent fires.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.
If found, it loads the assembly using
Assembly.LoadFrom()— notAssembly.Load(). This is critical:LoadFromaccepts a file path and doesn’t trigger theAssemblyResolveevent recursively. UsingAssembly.Load()inside the handler would cause aStackOverflowException.If not found, the handler returns
null, allowing other handlers or the default failure behavior to proceed.
Implementation Rules
| Rule | Why |
|---|---|
Register in the constructor, not ConnectToSW | The JIT compiler may trigger assembly loads when compiling ConnectToSW if it references WPF types |
Use Assembly.LoadFrom, never Assembly.Load | Assembly.Load inside the handler triggers infinite recursion |
Return null for unknown assemblies | Other add-ins or the host may have their own handlers |
Filter out .resources assemblies | Satellite assemblies for localization use a different resolution path |
Use Assembly.GetExecutingAssembly().Location | GetEntryAssembly() 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:
| Concern | NuGet / PackageReference | AssemblyResolve Handler |
|---|---|---|
| Compile-time type resolution | Yes | No |
| DLLs copied to output folder | Yes (Copy Local) | No |
| WiX installer harvesting | Yes | No |
| Runtime loading inside SolidWorks | No (wrong probing path) | Yes |
| Runtime loading in standalone app | Yes (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.EXEorEXCEL.EXE. The application base isC:\Program Files\Microsoft Office\root\Office16\. - AutoCAD .NET plugins: The host is
acad.exe. The application base isC:\Program Files\Autodesk\AutoCAD 2026\. A Syncfusion forum post documents the exact sameFileNotFoundExceptionpattern 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.dllhosts 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.Loadcall.Register
AppDomain.CurrentDomain.AssemblyResolvein your add-in constructor — before any WPF type is referenced. UseAssembly.LoadFrom(), neverAssembly.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
AssemblyResolvehandler 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.