Detail Blog
Nhu.Truong
How to debug without restarting AutoCAD?
The approach is applicable not only to AutoCAD but also to the majority of software that supports .NET, like Revit or Navisworks, aiming to simplify the add-in development process.
Overview
The application executes commands developed by users through System.Reflection in .NET.
During software startup, it loads .dll files (referred to as assemblies, compiled by users using an integrated development environment) as configured in the manifest file, following the specific format of each software.
This loading process is accomplished through Assembly.LoadFile, and the assembly references that users have included are also brought into the software's domain.
What will happen next?
Issue
If, during the debugging process, we attempt to apply new code after compiling while the software is still open, it will lead to the following error:
Could not copy "..." to "...". Exceeded retry count of 10. Failed. The file is locked by: "AutoCAD Application (6824)".
Unable to copy file "..." to "...". The process cannot access the file '...' because it is being used by another process.
- We cannot modify the application's assembly loading method.
- We do not want to restart the application after applying new code.
So, is there any solution to solve this problem?
Solution
Load the BeyCons.Debug assembly directly into the AutoCAD domain. In BeyCons.Debug, the BeyCons.App assembly will be loaded into the AutoCAD domain using Assembly.Load(byte[] rawAssembly).
Now, BeyCons.App can edit and compile code easily without being blocked.
Invoke the commands of BeyCons.App within the commands of BeyCons.Debug using Reflection as follows:
#region Using using Autodesk.AutoCAD.Runtime; using System; using System.IO; using System.Reflection; #endregion #nullable enable namespace BeyCons.Debug.Commands { public class DebugCommand { #region Fields private const string _assemblyName = "BeyCons.App"; #endregion #region Commands [CommandMethod("LC", CommandFlags.Session | CommandFlags.UsePickSet)] public static void LoadCommand() { var arrayByte = File.ReadAllBytes(@$"<The folder path of BeyCons.App>\{_assemblyName}.dll"); var assembly = Assembly.Load(arrayByte); if (assembly.GetType($"{_assemblyName}.Commands.TestingCommand") is not { } type) return; if (type.GetMethod("ShowDialog") is not { } methodInfo) return; var instance = Activator.CreateInstance(type); methodInfo.Invoke(instance, null); } #endregion } }
#region Using using Autodesk.AutoCAD.ApplicationServices; using Autodesk.AutoCAD.Runtime; #endregion namespace BeyCons.App.Commands { internal class TestingCommand { [CommandMethod("ShowDialog", CommandFlags.Session)] public static void ShowDialog() { Application.ShowAlertDialog("Hello World!"); } } }
Another arising issue is that Assembly.Load(byte[] rawAssembly) will not load the assembly references used in BeyCons.App.
How do we resolve it?
You have to implement the AssemblyResolve event of the AppDomain to load the assembly references when BeyCons.App requests them and modify the code as follows:
#region Using using Autodesk.AutoCAD.Runtime; using System; using System.IO; using System.Reflection; #endregion #nullable enable namespace BeyCons.Debug.Commands { public class DebugCommand { #region Fields private const string _assemblyName = "BeyCons.App"; #endregion #region Commands [CommandMethod("LC", CommandFlags.Session | CommandFlags.UsePickSet)] public static void LoadCommand() { var folderPath = @"<The folder path of BeyCons.App>";
var arrayByte = File.ReadAllBytes(Path.Combine(folderPath, $"{_assemblyName}.dll")); var assembly = Assembly.Load(arrayByte); if (assembly.GetType($"{_assemblyName}.Commands.TestingCommand") is not { } type) return; if (type.GetMethod("ShowDialog") is not { } methodInfo) return; var instance = Activator.CreateInstance(type, folderPath); methodInfo.Invoke(instance, null); } #endregion } }
#region Using using Autodesk.AutoCAD.Runtime; using System; using System.IO; using System.Reflection; #endregion #nullable enable namespace BeyCons.App.Commands { internal class TestingCommand { #region Fields private readonly string _folderPath; #endregion public TestingCommand(string folderPath) { _folderPath = folderPath; AppDomain.CurrentDomain.AssemblyResolve += AssemblyResolve; } #region Events private Assembly? AssemblyResolve(object sender, ResolveEventArgs args) { if (args.RequestingAssembly is null) return null; var assemblyDependentPath = string.Empty; foreach (var referencedAssembly in args.RequestingAssembly.GetReferencedAssemblies()) { var assemblyName = args.Name.Substring(0, args.Name.IndexOf(",")); if (referencedAssembly.FullName.Substring(0, referencedAssembly.FullName.IndexOf(",")) != assemblyName) continue; assemblyDependentPath = Path.Combine(_folderPath, $"{assemblyName}.dll"); break; } if (string.IsNullOrEmpty(assemblyDependentPath)) return null; return Assembly.Load(File.ReadAllBytes(assemblyDependentPath)); } #endregion #region Commands [CommandMethod("ShowDialog", CommandFlags.Session)] public static void ShowDialog() { BeyCons.UI.Notification.ShowDialog("Hello BeyCons!"); } #endregion } }
As you can see, now BeyCons.App successfully executes the function located in the BeyCons.UI assembly that BeyCons.App references.
Conclusion
Utilizing .NET Reflection allows you to modify the loading process of assemblies into the application's domain, preventing potential blocks during source code recompilation.
Additionally, it empowers the execution of commands from a separate assembly at runtime, simplifying add-in development and speeding up the debugging process.