diff --git a/CHANGELOG.md b/CHANGELOG.md index dbc7df7..3a70768 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,10 +7,14 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## [1.0.0] / 2025-01-20 ### Features - Initial release. +### DesignApplication +- Add `DesignApplicationLoader` to fix [Design Automation for Revit ignores PackageContents.xml configuration.](https://github.com/ricaun-io/RevitAddin.DA.Tester/issues/7) +- Add `ExternalServer` to fix [Design Automation for Revit ActiveAddInId is null](https://github.com/ricaun-io/RevitAddin.DA.Tester/issues/9) ### Application - Sample to execute `ricaun.Revit.DA` library. ### Tests - Test `ricaun.Revit.DA` library. +- Update `TestCase` with expected results. (`Reference`, `TargetFramework` and `ActiveAddInId`) [vNext]: ../../compare/1.0.0...HEAD [1.0.0]: ../../compare/1.0.0 \ No newline at end of file diff --git a/Directory.Build.props b/Directory.Build.props index ea76a19..a75d114 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,5 +1,5 @@ - 1.0.0-alpha + 1.0.0-beta \ No newline at end of file diff --git a/ricaun.Revit.DA.Example/Revit/App.cs b/ricaun.Revit.DA.Example/Revit/App.cs index a7fbd9b..213e5a2 100644 --- a/ricaun.Revit.DA.Example/Revit/App.cs +++ b/ricaun.Revit.DA.Example/Revit/App.cs @@ -8,18 +8,14 @@ public class App : DesignApplication { public override bool Execute(Application application, string filePath, Document document) { - Console.WriteLine($"Execute: \t{application}"); - Console.WriteLine($"ActiveAddInId: \t{application.ActiveAddInId?.GetAddInName()}"); - var output = RevitOutputModel.Create(application); output.Save(); - return true; } public override void OnStartup() { - Console.WriteLine($"ActiveAddInId: \t{ControlledApplication.ActiveAddInId?.GetAddInName()}"); + } public override void OnShutdown() diff --git a/ricaun.Revit.DA.Tests/Tests.cs b/ricaun.Revit.DA.Tests/Tests.cs index efe4630..b90fca6 100644 --- a/ricaun.Revit.DA.Tests/Tests.cs +++ b/ricaun.Revit.DA.Tests/Tests.cs @@ -9,8 +9,10 @@ namespace ricaun.Revit.DA.Tests { public class Tests : BundleFileTests { - [TestCase("2024", "4.7", "19", null)] - [TestCase("2025", "4.7", "19", null)] + //[TestCase("2024", "4.7", "19", null)] + //[TestCase("2025", "4.7", "19", null)] + [TestCase("2024", "4.8", "21", "Example")] + [TestCase("2025", "8.0", "25", "Example")] public async Task ExecuteDesignAutomation(string engine, string frameworkNameContain, string referenceContain, string addInNameContain) { var bundlePaths = GetBundles(); @@ -22,12 +24,12 @@ public async Task ExecuteDesignAutomation(string engine, string frameworkNameCon Console.WriteLine(output.ToJson()); - Assert.IsTrue(output.VersionName.Contains(engine), $"VersionName contains engine {engine}"); - Assert.IsTrue(output.FrameworkName.Contains(frameworkNameContain), $"FrameworkName contains framework {frameworkNameContain}"); - Assert.IsTrue(output.Reference.Contains(referenceContain), $"Reference contains reference {referenceContain}"); + Assert.IsTrue(output.VersionName.Contains(engine), $"VersionName {output.VersionName} not contains engine {engine}"); + Assert.IsTrue(output.FrameworkName.Contains(frameworkNameContain), $"FrameworkName {output.FrameworkName} not contains framework {frameworkNameContain}"); + Assert.IsTrue(output.Reference.Contains(referenceContain), $"Reference {output.Reference} not contains reference {referenceContain}"); if (output.AddInName is not null) - Assert.IsTrue(output.AddInName.Contains(addInNameContain), $"AddinName contains addin {addInNameContain}"); + Assert.IsTrue(output.AddInName.Contains(addInNameContain), $"AddinName {output.AddInName} not contains addin {addInNameContain}"); else Assert.AreEqual(addInNameContain, output.AddInName); } diff --git a/ricaun.Revit.DA/DesignApplication.cs b/ricaun.Revit.DA/DesignApplication.cs index ab0d9cb..6a9ce2c 100644 --- a/ricaun.Revit.DA/DesignApplication.cs +++ b/ricaun.Revit.DA/DesignApplication.cs @@ -1,18 +1,50 @@ using Autodesk.Revit.ApplicationServices; using Autodesk.Revit.DB; using DesignAutomationFramework; +using ricaun.Revit.DA.ExternalServer; +using ricaun.Revit.DA.Loader; +using System; namespace ricaun.Revit.DA { public abstract class DesignApplication : IExternalDBApplication, IDesignAutomation { + /// + /// Use ExternalService to execute the IDesignAutomation.Execute, this make the Execute run in the AddIn context. + /// + public virtual bool UseExternalService => true; public ControlledApplication ControlledApplication { get; private set; } public abstract void OnStartup(); public abstract void OnShutdown(); public abstract bool Execute(Application application, string filePath, Document document); + + private IExternalDBApplication designApplication; + private DesignAutomationSingleExternalServer externalServer; public ExternalDBApplicationResult OnStartup(ControlledApplication application) { ControlledApplication = application; + + designApplication = DesignApplicationLoader.LoadVersion(this); + + if (designApplication is IExternalDBApplication) + { + return designApplication.OnStartup(application); + } + + Console.WriteLine("----------------------------------------"); + Console.WriteLine($"FullName: \t{GetType().Assembly.FullName}"); + Console.WriteLine($"AddInName: \t{ControlledApplication.ActiveAddInId?.GetAddInName()}"); + Console.WriteLine("----------------------------------------"); + + try + { + externalServer = new DesignAutomationSingleExternalServer(this).Register(); + } + catch (Exception ex) + { + Console.WriteLine($"DesignAutomationSingleExternalServer: \t{ex.Message}"); + } + OnStartup(); DesignAutomationBridge.DesignAutomationReadyEvent += DesignAutomationReadyEvent; @@ -22,24 +54,45 @@ public ExternalDBApplicationResult OnStartup(ControlledApplication application) public ExternalDBApplicationResult OnShutdown(ControlledApplication application) { ControlledApplication = application; + + if (designApplication is IExternalDBApplication) + { + try + { + return designApplication.OnShutdown(application); + } + finally + { + DesignApplicationLoader.Dispose(); + } + } + + externalServer?.RemoveServer(); + OnShutdown(); DesignAutomationBridge.DesignAutomationReadyEvent -= DesignAutomationReadyEvent; return ExternalDBApplicationResult.Succeeded; } + private void DesignAutomationReadyEvent(object sender, DesignAutomationReadyEventArgs e) { DesignAutomationBridge.DesignAutomationReadyEvent -= DesignAutomationReadyEvent; var data = e.DesignAutomationData; + Console.WriteLine("--------------------------------------------------"); + Console.WriteLine($"RevitApp: {data.RevitApp} \tFilePath: {data.FilePath} \tRevitDoc: {data.RevitDoc} \tAddInName:{data.RevitApp.ActiveAddInId?.GetAddInName()}"); + Console.WriteLine("--------------------------------------------------"); + + if (externalServer is not null && UseExternalService) + { + e.Succeeded = externalServer.ExecuteService(data.RevitApp, data.FilePath, data.RevitDoc); + return; + } + e.Succeeded = Execute(data.RevitApp, data.FilePath, data.RevitDoc); } } - - public interface IDesignAutomation - { - bool Execute(Application application, string filePath, Document document); - } } diff --git a/ricaun.Revit.DA/ExternalServer/DesignAutomationExternalData.cs b/ricaun.Revit.DA/ExternalServer/DesignAutomationExternalData.cs new file mode 100644 index 0000000..f6fa46b --- /dev/null +++ b/ricaun.Revit.DA/ExternalServer/DesignAutomationExternalData.cs @@ -0,0 +1,13 @@ +using Autodesk.Revit.ApplicationServices; +using Autodesk.Revit.DB; +using Autodesk.Revit.DB.ExternalService; + +namespace ricaun.Revit.DA.ExternalServer +{ + public class DesignAutomationExternalData : IExternalData + { + public Application Application { get; set; } + public string FilePath { get; set; } + public Document Document { get; set; } + } +} \ No newline at end of file diff --git a/ricaun.Revit.DA/ExternalServer/DesignAutomationSingleExternalServer.cs b/ricaun.Revit.DA/ExternalServer/DesignAutomationSingleExternalServer.cs new file mode 100644 index 0000000..ec37b62 --- /dev/null +++ b/ricaun.Revit.DA/ExternalServer/DesignAutomationSingleExternalServer.cs @@ -0,0 +1,123 @@ +using Autodesk.Revit.ApplicationServices; +using Autodesk.Revit.DB; +using Autodesk.Revit.DB.ExternalService; +using System; +using System.Collections.Generic; + +namespace ricaun.Revit.DA.ExternalServer +{ + /// + /// DesignAutomationSingleExternalServer is a single server with a default single service. + /// + /// + /// This external server is used to execute IDesignAutomation when DesignAutomationReadyEvent is triggered. + /// Because the external server is registered before the Revit finish initialize the executed service run in the same ActiveAddIn when the external service is registered. + /// Fix the issue that DesignAutomationReadyEvent triggers without the ActiveAddIn context. + /// + public class DesignAutomationSingleExternalServer : ISingleServerService, IDesignAutomationExternalServer + { + private readonly IDesignAutomation designAutomation; + public DesignAutomationSingleExternalServer(IDesignAutomation designAutomation) + { + this.designAutomation = designAutomation; + } + public ExternalServiceId ServiceId { get; } = new ExternalServiceId(Guid.NewGuid()); + public Guid ServerId { get; } = Guid.NewGuid(); + + #region ExecuteService + + public bool ExecuteService(Application application, string filePath, Document document) + { + var externalData = new DesignAutomationExternalData() + { + Application = application, + FilePath = filePath, + Document = document, + }; + return ExecuteService(externalData); + } + + public bool ExecuteService(DesignAutomationExternalData externalData) + { + var service = ExternalServiceRegistry.GetService(ServiceId) as SingleServerService; + var result = ExternalServiceRegistry.ExecuteService(service.GetPublicAccessKey(), externalData); + Console.WriteLine($"ExecuteService: \t{result} \t{externalData}"); + return result == ExternalServiceResult.Succeeded; + } + + #endregion + + #region Register/Unregister + public DesignAutomationSingleExternalServer Register() + { + var options = new ExternalServiceOptions() + { + IsPublic = true, + }; + var privateKeyExecute = ExternalServiceRegistry.RegisterService(this, options); + return this.AddServer(); + } + public DesignAutomationSingleExternalServer AddServer(IDesignAutomationExternalServer designAutomationExternalServer = null) + { + designAutomationExternalServer ??= this; + var service = ExternalServiceRegistry.GetService(ServiceId) as SingleServerService; + if (!service.IsRegisteredServerId(designAutomationExternalServer.GetServerId())) + { + service.AddServer(designAutomationExternalServer); + service.SetActiveServer(designAutomationExternalServer.GetServerId()); + } + return this; + } + public DesignAutomationSingleExternalServer RemoveServer() + { + var service = ExternalServiceRegistry.GetService(ServiceId) as SingleServerService; + foreach (var guid in service.GetRegisteredServerIds()) + { + service.RemoveServer(guid); + } + return this; + } + #endregion + + #region ISingleServerService + public bool Execute(IExternalServer server, Document document, IExternalData data) + { + if (server is IDesignAutomationExternalServer designAutomationExternalServer) + { + return designAutomationExternalServer.Execute(data as DesignAutomationExternalData); + } + return false; + } + public bool IsValidServer(IExternalServer server) + { + return server is IDesignAutomationExternalServer; + } + #endregion + + #region IDesignAutomationExternalServer + public bool Execute(DesignAutomationExternalData data) + { + return designAutomation.Execute(data.Application, data.FilePath, data.Document); + } + + public Guid GetServerId() => ServerId; + #endregion + + #region IExternalService + public string GetDescription() => GetType().Name; + public string GetName() => GetType().Name; + public ExternalServiceId GetServiceId() => ServiceId; + public string GetVendorId() => "ricaun"; + + /// + /// This method may only be invoked in a recordable service. Services registered as non-recordable never receive this call. + /// + public void OnServersChanged(Document document, ServerChangeCause cause, IList oldServers) { } + + /// + /// This method may only be invoked in a recordable service. Services registered as non-recordable never receive this call. + /// + public DisparityResponse OnServersDisparity(Document document, IList oldServers) => DisparityResponse.ApplyDefaults; + #endregion + } +} \ No newline at end of file diff --git a/ricaun.Revit.DA/ExternalServer/IDesignAutomationExternalServer.cs b/ricaun.Revit.DA/ExternalServer/IDesignAutomationExternalServer.cs new file mode 100644 index 0000000..40c759d --- /dev/null +++ b/ricaun.Revit.DA/ExternalServer/IDesignAutomationExternalServer.cs @@ -0,0 +1,10 @@ +using Autodesk.Revit.DB.ExternalService; +using ricaun.Revit.DA.ExternalServer; + +namespace ricaun.Revit.DA.ExternalServer +{ + public interface IDesignAutomationExternalServer : IExternalServer + { + public bool Execute(DesignAutomationExternalData data); + } +} \ No newline at end of file diff --git a/ricaun.Revit.DA/IDesignAutomation.cs b/ricaun.Revit.DA/IDesignAutomation.cs new file mode 100644 index 0000000..e9b40bc --- /dev/null +++ b/ricaun.Revit.DA/IDesignAutomation.cs @@ -0,0 +1,10 @@ +using Autodesk.Revit.ApplicationServices; +using Autodesk.Revit.DB; + +namespace ricaun.Revit.DA +{ + public interface IDesignAutomation + { + bool Execute(Application application, string filePath, Document document); + } +} diff --git a/ricaun.Revit.DA/Loader/DesignApplicationLoader.cs b/ricaun.Revit.DA/Loader/DesignApplicationLoader.cs new file mode 100644 index 0000000..0cc0063 --- /dev/null +++ b/ricaun.Revit.DA/Loader/DesignApplicationLoader.cs @@ -0,0 +1,87 @@ +using Autodesk.Revit.DB; +using System; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Runtime.Versioning; + +namespace ricaun.Revit.DA.Loader +{ + internal static class DesignApplicationLoader + { + private static Assembly loadAssembly; + public static IExternalDBApplication LoadVersion(T designApplication) where T : DesignApplication + { + var type = designApplication.GetType(); + + var similar = AppDomain.CurrentDomain.GetAssemblies().Where(e => e.FullName == type.Assembly.FullName); + if (similar.Count() >= 2) + { + return null; + } + + var location = type.Assembly.Location; + var revitAssemblyReference = type.Assembly.GetReferencedAssemblies().FirstOrDefault(e => e.Name.Equals("RevitAPI")); + var revitAssembly = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(e => e.GetName().Name.Equals("RevitAPI")); + + var revitReferenceVersion = revitAssemblyReference.Version.Major + 2000; + var revitVersion = revitAssembly.GetName().Version.Major + 2000; + + Console.WriteLine("--------------------------------------------------"); + Console.WriteLine($"DesignApplicationLoader: \t{revitVersion} -> {revitReferenceVersion}"); + + for (int version = revitVersion; version > revitReferenceVersion; version--) + { + var directory = Path.GetDirectoryName(location); + var directoryVersionRevit = Path.Combine(directory, "..", version.ToString()); + var fileName = Path.Combine(directoryVersionRevit, Path.GetFileName(location)); + + //Console.WriteLine($"DesignApplicationLoader Try: \t{version}"); + + if (File.Exists(fileName)) + { + fileName = new FileInfo(fileName).FullName; + Console.WriteLine($"DesignApplicationLoader File Exists: \t{fileName}"); + Console.WriteLine($"DesignApplicationLoader Version: \t{version}"); + Console.WriteLine($"DesignApplicationLoader LoadFile: \t{Path.GetFileName(fileName)}"); + AppDomain.CurrentDomain.AssemblyResolve += LoadAssemblyResolve; + loadAssembly = Assembly.LoadFile(fileName); + break; + } + } + + Console.WriteLine("----------------------------------------"); + + if (loadAssembly is not null) + { + var loadType = loadAssembly.GetType(type.FullName); + + Console.WriteLine($"DesignApplicationLoader Type: {loadType}"); + Console.WriteLine($"DesignApplicationLoader FrameworkName: \t{loadType.Assembly.GetCustomAttribute()?.FrameworkName}"); + Console.WriteLine("----------------------------------------"); + + return Activator.CreateInstance(loadType) as IExternalDBApplication; + } + + return null; + } + + private static Assembly LoadAssemblyResolve(object sender, ResolveEventArgs args) + { + var assemblyName = new AssemblyName(args.Name); + var assemblyPath = Path.Combine(Path.GetDirectoryName(loadAssembly.Location), assemblyName.Name + ".dll"); + if (File.Exists(assemblyPath)) + { + var folderName = Path.GetFileName(Path.GetDirectoryName(assemblyPath)); + Console.WriteLine($"AssemblyResolve LoadFile: {folderName}\\{assemblyName.Name + ".dll"}"); + return Assembly.LoadFile(assemblyPath); + } + return null; + } + + public static void Dispose() + { + AppDomain.CurrentDomain.AssemblyResolve -= LoadAssemblyResolve; + } + } +} \ No newline at end of file