Code Red > .NET
Targeting Multiple AutoCAD Product Releases: Source Code Portability
TT:
While I won't get into the underlying reasons, I will say that the way that Autodesk restructured ObjectARX and its managed wrapper APIs serves the strategic objective of hindering release-independent source code portability and with that, the ability for developers to target multiple product releases with a single source code stream. In fact, they have pretty much made it nearly impossible for developers to continue to target AutoCAD 2012 and AutoCAD 2013 with the same source code.
Note that Kean Walmsly makes little mention of the fact that the changes in AutoCAD 2013's managed API break existing source code portability, and seems to trivialize that, something that most professional developers regard as a material 'breach of contract'. His article on the subject also seems to confuse 'binary compatibility' with source code compatibility/portability. The two are not the same. Binary compatibility is where the same build of a project can be run on different product releases. Source code portability is the ability to compile the same source code to multiple distributions targeting multiple product releases. While we have come to expect binary compatibility to be broken in each major release, we were not expecting source code to be broken to the extent that it has been broken in AutoCAD 2013, and with that, the ability to continue ongoing development of projects that target both AutoCAD 2010-2012 and 2013 has become more than most will be willing to bear.
This code is provided as an example showing one strategy for maximizing source code portability between AutocAD R18 and R19. The basic strategy involves abstracting out differences between APIs in different releases, by exposing a version-independent wrapper API that determines which underlying native API must be called at runtime, and compiles a delegate that calls the appropriate API. Note that this approach uses compiled expressions to achieve the best performance, but a similar approach could be used that involved the use of standard Reflection for calling the underlying APIs. The code is largely untested and you should use it at your own risk.
--- Code - C#: ---// Copyright (c) 2012 Tony Tanzillo using System;using System.Reflection;using System.Linq;using System.Linq.Expressions;using Autodesk.AutoCAD.ApplicationServices; namespace namespace1{ // Provides version-independent APIs that // can run on AutoCAD R18 and R19. // Note that in order to achieve version-independence // No types or namespaces that are unique to AutoCAD // 2013 can be used directly from this code or the // assembly containing it. // For that reason, this code should live in separate // assemblies (one for each targeted release) and be // referenced and used from other application code. public static class VersionInteropHelper { // Document.GetAcadDocumentEx() Extension Method // // Returns the AcadDocument for the given Document. // // This extension method provides a version-independent // way to obtain the AcadDocument object. Hence, it can // be compiled against and run on any version of AutoCAD // that uses .NET 3.5 or later (which is required for // System.Linq.Expression) // // Usage: // // Include this code in your project and use GetAcadDocumentEx() // exclusively, in lieu of using Document.AcadDocument on AutoCAD // 2012 and earlier, and DocumentExtension.GetAcadDocument() on // AutoCAD 2013 or later. // // This code also serves as an example of a general pattern // that can be applied to abstract out other breaking changes // in source code between AutoCAD 2012 and AutoCAD 2013. public static object GetAcadDocumentEx( this Document doc ) { if( getAcadDocumentMethod == null ) getAcadDocumentMethod = CreateGetAcadDocumentMethod(); return getAcadDocumentMethod( doc ); } static Func<Document, object> getAcadDocumentMethod = null; static Func<Document, object> CreateGetAcadDocumentMethod() { Expression<Func<Document, object>> result = null; var parameters = new ParameterExpression[] { Expression.Parameter( typeof( Document ), "document") }; if( AcadVersion.Major > 18 ) { // AcMgd.dll: Type type = typeof( Autodesk.AutoCAD.Windows.PaletteSet ) .Assembly.GetType( "Autodesk.AutoCAD.ApplicationServices.DocumentExtension", true ); MethodInfo mi = type.GetMethod( "GetAcadDocument", BindingFlags.Public | BindingFlags.Static ); result = Expression.Lambda<Func<Document, object>>( Expression.Call( mi, parameters ), parameters ); } else { MethodInfo method = typeof(Document).GetProperty("AcadDocument").GetGetMethod(); result = Expression.Lambda<Func<Document, object>>( Expression.Call( parameters[0], method ), parameters[0] ); } return result.Compile(); } // Let's all give a big round of applause to Autodesk for // removing the Version property from the Application object // in AcMgd.dll. In order for version-independent code to // branch to different execution paths depending on whether // it is running on R18 or R19, it must check the version of // the application that it's running on at runtime. // // But, because they did not leave the version property on the // Application object in AcMgd.dll, there is no direct version- // independent way to get the application version... LMAO // A version-independent way to get the AutoCAD version at runtime (LOL) public static Version AcadVersion { get { return typeof( Document ).Assembly.GetName().Version; } } }}
TT:
After examining the AutoCAD 2013 migration quagmire further, I've concluded that the above strategy and code is useful mainly when there is binary compatibility between the supported product releases (e.g., where the same build can be loaded into all supported AutoCAD versions).
However, since that is not the case between AutoCAD 2013 and previous releases, here is a far better solution to the problem of maximizing application source code portability between AutoCAD 2013 and AutoCAD 2012 and earlier. The most compelling aspect of this solution is that it completely eliminates the need for release-dependent code and the messy conditional compilation directives (#if/#else/#endif) needed to include/exclude release-specific code depending on the target AutoCAD release.
--- Code - C#: --- // Copyright (c) 2012 Tony Tanzillo using System;using Autodesk.AutoCAD.ApplicationServices;using Autodesk.AutoCAD.Windows;using System.Drawing; // APIs that provide support for elimiation of release-// dependent code in projects targeting multiple AutoCAD// product releases. // This code is designed to maximize application source // code portability between AutoCAD 2013 and AutoCAD// 2012 and earlier. By using this code in projects that// target AutoCAD 2012 or earlier, you can avoid the need// for extensive amounts of additional release-specific // code, and associated use of conditional compilation // directives (#if/#else/#endif).//// These extension methods are identical to the extension// methods introduced in AutoCAD 2013, that replace what// was previously exposed through properties in AutoCAD // 2012 and earlier.//// You can use this code in projects targeting AutoCAD 2012 // or earlier, to allow the same calling code to run on both // AutoCAD 2012 and AutoCAD 2013. Once you've added this code // to your projects targeting AutoCAD 2012 or earlier, you // should replace all references to the 'defunct' properties // which the included extension methods replace with calls to // the included extension methods. Once you've done that, any // code that uses these extension methods can be compiled and // run on AutoCAD 2013 and 2012 or earlier.//// The affected AutoCAD 2012 API properties are://// Document.AcadDocument// Document.StatusBar//// Window.Size// Window.Location// Window.Icon//// You should only include this code in projects targeting // AutoCAD 2012 or earlier, as the included extension methods // are provided by AutoCAD's managed API on AutoCAD 2013.//// Note that these APIs do not eliminate the need to revise// source code to run on AutoCAD 2013. What these APIs do is// allows source code that's been revised to run on AutoCAD// 2013 to also run on AutoCAD 2012. namespace Autodesk.AutoCAD.ApplicationServices{ public static class DocumentExtension { public static object GetAcadDocument( this Document doc ) { return doc.AcadDocument; } public static StatusBar GetStatusBar( this Document doc ) { return doc.StatusBar; } }} namespace Autodesk.AutoCAD.Windows{ public static class WindowExtension { public static Size GetSize( this Window window ) { return window.Size; } public static void SetSize( this Window window, Size value ) { window.Size = value; } public static Icon GetIcon( this Window window ) { return window.Icon; } public static void SetIcon( this Window window, Icon value ) { window.Icon = value; } public static Point GetLocation( this Window window ) { return window.Location; } public static void SetLocation( this Window window, Point value ) { window.Location = value; } }}
[edit] Corrected erronous 'AcadObject' with 'AcadDocument'
TT:
--- Quote from: TheMaster on June 01, 2012, 01:05:02 pm ---
[edit] Corrected erronous 'AcadObject' with 'AcadDocument'
--- End quote ---
Oops again - 'AcadObject' -> 'AcadDocument' corrected.
GTVic:
Hi, I tried this with VB.NET but it doesn't allow "static" and seemed to replace the entire namespace so other classes were not available.
I tried reflection and was successful for getting the Document object on pre-2013 but not the 2013 which seemed to require a query on all assemblies which took a few seconds so wasn't worth it.
Reflection:
--- Code: ---Imports Autodesk.AutoCAD.DatabaseServices
Imports Autodesk.AutoCAD.ApplicationServices
Imports Autodesk.AutoCAD.Windows
Imports System.Reflection
Imports System.Drawing
Imports System.Linq
Imports System
Public Class VersionCode
' these all work
Public Function GetTextStyleId(ByVal Instance As DBText) As ObjectId
If textStyleProperty IsNot Nothing Then
Return DirectCast(textStyleProperty.GetValue(Instance, Nothing), ObjectId)
Else
Throw New MissingMemberException()
End If
End Function
Public Function GetTextStyleId(ByVal Instance As MText) As ObjectId
If mtextStyleProperty IsNot Nothing Then
Return DirectCast(mtextStyleProperty.GetValue(Instance, Nothing), ObjectId)
Else
Throw New MissingMemberException()
End If
End Function
Public Sub SetTextStyleId(ByVal Instance As DBText, ByVal value As ObjectId)
If textStyleProperty IsNot Nothing Then
textStyleProperty.SetValue(instance, value, Nothing)
Else
Throw New MissingMemberException()
End If
End Sub
Public Sub SetTextStyleId(ByVal Instance As MText, ByVal value As ObjectId)
If mtextStyleProperty IsNot Nothing Then
mtextStyleProperty.SetValue(instance, value, Nothing)
Else
Throw New MissingMemberException()
End If
End Sub
Public Sub New()
textStyleProperty = GetType(DBText).GetProperty("TextStyle")
If textStyleProperty Is Nothing Then
textStyleProperty = GetType(DBText).GetProperty("TextStyleId")
End If
mtextStyleProperty = GetType(MText).GetProperty("TextStyle")
If mtextStyleProperty Is Nothing Then
mtextStyleProperty = GetType(MText).GetProperty("TextStyleId")
End If
' this part may be working but the associated code in GetAcadDocument doesn't and the query takes way too long
Dim ns As String = "Autodesk.AutoCAD.ApplicationServices.DocumentExtension"
Dim namespaceFound As Object
namespaceFound = (From assembly In AppDomain.CurrentDomain.GetAssemblies() From type In assembly.GetTypes() Where type.Namespace = ns Select type).Any()
If namespaceFound IsNot Nothing Then
DocumentExtensionClass = Activator.CreateInstance(namespaceFound)
End If
If DocumentExtensionClass IsNot Nothing Then
GetAcadDocumentMethod = DocumentExtensionClass.GetMethod("GetAcadDocument")
End If
' this works for pre-2013
AcadDocumentProperty = GetType(Autodesk.AutoCAD.ApplicationServices.Document).GetProperty("AcadDocument")
End Sub
Public Function GetAcadDocument(ByVal WhichDocument As Document) As Object
GetAcadDocument = Nothing
' this doesn't work I think I have to replace Invoke(Nothing, params) with Invoke(DocumentExtensionClass, params)
' but I abandoned this before testing because the query took so long
If GetAcadDocumentMethod IsNot Nothing Then
Dim params(0 To 0) As Object
params(0) = WhichDocument
Return DirectCast(GetAcadDocumentMethod.Invoke(Nothing, params), Object)
End If
' this works for pre-2013
If AcadDocumentProperty IsNot Nothing Then
Return DirectCast(AcadDocumentProperty.GetValue(WhichDocument, Nothing), Object)
End If
End Function
Private textStyleProperty As PropertyInfo = Nothing
Private mtextStyleProperty As PropertyInfo = Nothing
Private DocumentExtensionClass As Type = Nothing
Private GetAcadDocumentMethod As MethodInfo = Nothing
Private AcadDocumentProperty As PropertyInfo = Nothing
End Class
--- End code ---
Namespace Method
--- Code: ---Namespace Autodesk.AutoCAD.ApplicationServices
Public Class DocumentExtension
Public Function GetAcadDocument(ByRef doc As Document) As Object
Return doc.AcadDocument
End Function
Public Function GetStatusBar(ByRef doc As Document) As Object
Return doc.StatusBar
End Function
End Class
End Namespace
Namespace Autodesk.AutoCAD.Windows
Public Class WindowExtension
Public Function GetSize(ByVal win As Window) As Size
Return win.Size
End Function
Public Sub SetSize(ByVal win As Window, ByVal value As Size)
win.Size = value
End Sub
Public Function GetIcon(ByVal win As Window) As Icon
Return win.Icon
End Function
Public Sub SetIcon(ByVal win As Window, ByVal value As Icon)
win.Icon = value
End Sub
Public Function GetLocation(ByVal win As Window) As Point
Return win.Location
End Function
Public Sub SetLocation(ByVal win As Window, ByVal value As Point)
win.Location = value
End Sub
End Class
End Namespace
--- End code ---
Jeff H:
--- Quote ---Extension methods can be declared only within modules
--- End quote ---
--- Quote ---
extension methods must be marked with the extension attribute <Extension()> from the System.Runtime.CompilerServices namespace.
--- End quote ---
MSDN
Navigation
[0] Message Index
[#] Next page
Go to full version