I am trying to run acad code from a WCF service in process. I get most everything work just fine but for some reason I get this cryptic error when I try to loop through a selection set of AcadEntity objects. The weird part is that if I place the same loop in a simple IExtensionApplication command it works fine.
Does anyone have any suggestions how to make this work? I assume something to do with wcf service behavior/acad threading model but not sure what.
Here is the full source code, note that I am not using Editor.WriteMessage because that's one of the things I cannot get working from WCF so I am relying on NLog to generate the output on the bottom of this message. If you have an idea how to make Editor.WriteMessage work from WCF I'd appreciate your help as well.
Thanks in advance,
Steve
Imports System.ServiceModel
Imports System.ServiceModel.Description
Imports Autodesk.AutoCAD.Runtime
Imports Autodesk.AutoCAD.Interop
Imports Autodesk.AutoCAD.Interop.Common
Imports Autodesk.AutoCAD.ApplicationServices
Imports MgdAcApplication = Autodesk.AutoCAD.ApplicationServices.Application
Imports NLog
#Region "WCF SERVICE"
<ServiceContract()>
Public Interface ISelectItems
<OperationContract()>
Sub SelectItems()
End Interface
<ServiceBehavior(InstanceContextMode:=InstanceContextMode.Single, ConcurrencyMode:=ConcurrencyMode.Single)>
Public Class wcfSelectItems
Implements ISelectItems
Private log As Logger = LogManager.GetCurrentClassLogger
Public Sub SelectItems() Implements ISelectItems.SelectItems
log.Info("This will crash")
DoSelection()
End Sub
Public activeDoc As Document = MgdAcApplication.DocumentManager.MdiActiveDocument
Public Sub DoSelection()
Try
Using acDocLck As DocumentLock = activeDoc.LockDocument()
Dim td As AcadDocument = DocumentExtension.GetAcadDocument(activeDoc)
Dim ssetObj As AcadSelectionSet = td.SelectionSets.Add("SSALL0") ' creates named selection set
ssetObj.Select(AcSelect.acSelectionSetAll)
log.Trace("We have a selection of size = {0}", ssetObj.Count)
Try
For Each ent As AcadEntity In ssetObj
log.Trace("{0}", ent.ObjectName)
Next
Catch
log.Error("This is the crash: '{0}'", Err.Description)
End Try
End Using
Catch
log.Error(Err.Description)
End Try
End Sub
End Class
#End Region
Public Class clsSelectedItems
Implements IExtensionApplication
Private log As Logger = LogManager.GetCurrentClassLogger
#Region "ACAD COMMAND"
<CommandMethod("DoSelection")> _
Public Sub DoSelection_Method()
log.Info("This is OK")
DoSelection()
End Sub
#End Region
Public m_DefaultService As String = "ISelectItems"
Public m_DefaultIP As String = "127.0.0.1"
Public m_DefaultPort As String = "7200"
Public m_DefaultProtocol As String = "http"
Private m_serviceHost As ServiceHost = Nothing
Public Sub Initialize() Implements Autodesk.AutoCAD.Runtime.IExtensionApplication.Initialize
hostService(m_DefaultIP, m_DefaultPort)
End Sub
Public Sub Terminate() Implements Autodesk.AutoCAD.Runtime.IExtensionApplication.Terminate
If Not m_serviceHost Is Nothing Then
m_serviceHost.Close()
End If
End Sub
Public activeDoc As Document = MgdAcApplication.DocumentManager.MdiActiveDocument
'Do selection routine, exact same as above. This one works:
Public Sub DoSelection()
Try
Using acDocLck As DocumentLock = activeDoc.LockDocument()
Dim td As AcadDocument = DocumentExtension.GetAcadDocument(activeDoc)
Dim ssetObj As AcadSelectionSet = td.SelectionSets.Add("SSALL1") ' creates named selection set
ssetObj.Select(AcSelect.acSelectionSetAll)
log.Trace("We have a selection of size = {0}", ssetObj.Count)
Try
For Each ent As AcadEntity In ssetObj
log.Trace("{0}", ent.ObjectName)
Next
Catch
log.Error("This is the crash: '{0}'", Err.Description)
End Try
End Using
Catch
log.Error(Err.Description)
End Try
End Sub
' Set up a WCF service endpoint and start service
Public Sub hostService(ByVal _hostIP As String, ByVal _port As String)
Dim wcfEndpoint As String = String.Format("{0}://{1}:{2}/{3}", m_DefaultProtocol, _hostIP, _port, m_DefaultService)
Try
Dim baseAddress As Uri = New Uri(wcfEndpoint)
m_serviceHost = New ServiceHost(GetType(wcfSelectItems), baseAddress)
Dim binding = CreateNewHttpBinding(GetType(wcfSelectItems).FullName)
m_serviceHost.AddServiceEndpoint(GetType(ISelectItems), binding, "IAcadInProc")
Dim smb As New ServiceMetadataBehavior()
smb.HttpGetEnabled = True
m_serviceHost.Description.Behaviors.Add(smb)
Catch ex As Exception
log.Error("EX 2. {0}", ex.ToString)
End Try
m_serviceHost.Open()
log.Info("Listening on {0}", wcfEndpoint)
End Sub
'We avoid using config files for now:
Private Shared Function CreateNewHttpBinding(ByVal name As String) As WSHttpBinding
Dim result As New WSHttpBinding
result.Name = name
result.OpenTimeout = New TimeSpan(0, 1, 0)
result.ReceiveTimeout = New TimeSpan(0, 10, 0)
result.SendTimeout = New TimeSpan(0, 1, 0)
result.BypassProxyOnLocal = False
result.TransactionFlow = False
result.HostNameComparisonMode = HostNameComparisonMode.StrongWildcard
result.MaxBufferPoolSize = 2147483647
result.MaxReceivedMessageSize = 2147483647
result.MessageEncoding = WSMessageEncoding.Text
result.TextEncoding = System.Text.Encoding.UTF8
result.UseDefaultWebProxy = True
result.AllowCookies = False
result.ReaderQuotas.MaxStringContentLength = 2147483647
result.ReaderQuotas.MaxDepth = 12
result.ReaderQuotas.MaxArrayLength = 16384
result.ReaderQuotas.MaxBytesPerRead = 4096
result.ReaderQuotas.MaxNameTableCharCount = 16384
result.ReliableSession.Ordered = False
result.ReliableSession.InactivityTimeout = New TimeSpan(0, 10, 0)
result.ReliableSession.Enabled = False
result.Security.Mode = SecurityMode.None
result.Security.Transport.ClientCredentialType = HttpClientCredentialType.None
result.Security.Message.ClientCredentialType = MessageCredentialType.None
Return (result)
End Function
End Class
OUTPUT:
Info clsSelectedItems Listening on http://127.0.0.1:7200/ISelectItems
Solved! Go to Solution.
Solved by sszabo. Go to Solution.
Solved by Alexander.Rivilis. Go to Solution.
How do you get the first example that does not work to run? It looks like you might be trying to use a mix of ObjectARX .NET and COM and run out of process.
If you are not netloading the assembly in-process then the ObjectARX .NET will not work.
Change the first example to only use COM, or do as you did in the second and get your service running through the initialize method to allow you to use the .NET API.
If you copy and paste the code into a new VB Class Project and add the following references it should work as advertised, that's how I got the output.
AcCoreMgd
AcDBMgd
AcMgd
Autodesk.AutoCAD.Interop
Autodesk.AutoCAD.Interop.Common
NLog
System.Runtime.Serialization
System.ServiceModel
When you say "it doesn't work" please let me know where are you getting stuck?
After compiled this DLL should be netloaded and it should host the WCF service automatically on http://127.0.0.1:7200/ISelectItems endpoint as the logs indicate.
That's my problem, as a newbie I am not very good with the .NET API and there are things I simply couldn't make work with it. For instance AcadSelectionSet. If you have some code example showing what you mean I'd appreciate your effort.
My idea of a quick and dirty solution to this problem for now is to just create ACAD commands for what doesn't work directly and send these commands in the service requests instead of trying to make ACAD play nice with WCF because I can still call ActiveDocument.SendCommand() from WCF.
How do you load and get this part of the code to run on its own:
Imports System.ServiceModel Imports System.ServiceModel.Description Imports Autodesk.AutoCAD.Runtime Imports Autodesk.AutoCAD.Interop Imports Autodesk.AutoCAD.Interop.Common Imports Autodesk.AutoCAD.ApplicationServices Imports MgdAcApplication = Autodesk.AutoCAD.ApplicationServices.Application Imports NLog #Region "WCF SERVICE" <ServiceContract()> Public Interface ISelectItems <OperationContract()> Sub SelectItems() End Interface <ServiceBehavior(InstanceContextMode:=InstanceContextMode.Single, ConcurrencyMode:=ConcurrencyMode.Single)> Public Class wcfSelectItems Implements ISelectItems Private log As Logger = LogManager.GetCurrentClassLogger Public Sub SelectItems() Implements ISelectItems.SelectItems log.Info("This will crash") DoSelection() End Sub Public activeDoc As Document = MgdAcApplication.DocumentManager.MdiActiveDocument Public Sub DoSelection() Try Using acDocLck As DocumentLock = activeDoc.LockDocument() Dim td As AcadDocument = DocumentExtension.GetAcadDocument(activeDoc) Dim ssetObj As AcadSelectionSet = td.SelectionSets.Add("SSALL0") ' creates named selection set ssetObj.Select(AcSelect.acSelectionSetAll) log.Trace("We have a selection of size = {0}", ssetObj.Count) Try For Each ent As AcadEntity In ssetObj log.Trace("{0}", ent.ObjectName) Next Catch log.Error("This is the crash: '{0}'", Err.Description) End Try End Using Catch log.Error(Err.Description) End Try End Sub End Class #End Region
This is just the WCF interface. You have to host it in the clsSelectedItems class by calling the hostService() Sub and if you do that from Initialize() that will happen automatically when you netload the DLL. Again, I put all these classes in 1 file intentionally so that people can simply copy/paste into a new class project and should be ready to go. If that doesn't work for you please tell me what's the error you are getting.
Just a followup on my previous idea: sending the command from the WCF class still gives me this error which is unbelievable because it's executing directly in my IExtensionApplication class and if I invoke the VERY SAME function from the acad command directly it works fine. This is the strangest thing I've ever seen.
Here is what I am talking about:
If you add the following helper function to wcfSelectItems, you should be able to invoke any ACAD command from WCF remotely:
Private ReadOnly Property Acad() As AcadApplication
Get
Return DirectCast(Application.AcadApplication, AcadApplication)
End Get
End Property
Public Sub SendCommand(ByVal cmd As String)
Using acDocLck As DocumentLock = activeDoc.LockDocument()
Try
Dim cmdStr As String = "(command " + Chr(34) + cmd + Chr(34) + " " + ")" + vbCr
Acad.ActiveDocument.SendCommand(cmdStr)
Catch
log.Error("EX 5. {0}", Err.Description)
End Try
End Using
End Sub
And this works with any standard commands such as "RIBBONCLOSE" or "NETLOAD" etc. However when I call it for the DoSelection command above it gives me the SAME EXACT E_UNEXPECTED error on the SAME EXACT LINE! Any ideas?!
ps. To Invoke this from the wcf interface you'll have to also add the following to ISelectItems:
<OperationContract()>
Sub AcadCmdSelectItems()
and the following to wcfSelectItems:
Public Sub AcadCmdSelectItems() Implements ISelectItems.AcadCmdSelectItems
SendCommand("DoSelection")
End Sub
It just occured to me that this post doesn't contain some basic info on how to test a WCF server out of the box. After compiling the code above you netload the DLL and then ACAD should be hosting and listening for WCF clients. To actually invoke DOSELECTION from WCF you can use WCF Test Client that comes with .NET:
http://msdn.microsoft.com/en-us/library/bb552364.aspx
I have not been able to get this code running to reproduce your error.
When I navigate to the page I get the service info.
The logger does not log.
When I try to use WcfTestClient.exe I get exception Object Reference not set to an instance of an object for any URL that I enter.
Ok, I see. I appreciate your effort, if you still have some patience try these tips:
A.The logger does not log.
This is important because this is how you know that your service is hosted correctly in ACAD and what's the exact endpoint it's listening on. Try the following steps:
1) Install the latest NLog from here: http://nlog-project.org/download
2) Add a New Item to your VB Class Project and select NLog Configuration File. This will create an NLog.config file with default target going to "${basedir}/log.txt" where ${basedir} is your ACAD directory (For me that's C:\Program Files\Autodesk\AutoCAD 2013). Copy this config file in your autocad directory. (Alternatively you can specify to Copy Always to your project output directory and set that to ACAD path). You also have to copy C:\Program Files (x86)\NLog\.NET Framework 4.0\NLog.dll in your ACAD directory (provided you are using NLog 4.0 in your project)
Optionally you can also view your logs in real time as opposed to opening log.text all the time:
http://log2console.codeplex.com/
B) When I try to use WcfTestClient.exe I get exception Object Reference not set to an instance of an object for any URL that I enter.
You can open log.txt in your ACAD directory and copy the exact URL from the first line:
2013-01-29 13:50:36.7247|INFO|clsWcfTestPrj.clsSelectedItems|Listening on http://127.0.0.1:7200/ISelectItems
In WCF test client click File -> Add Service and paste in the blue URL above. When connected you should be able to see the SelectItems request and you should be able to push the Invoke button. In the logs you will see the error code. Then you can go to ACAD command line and type in DOSELECTION and see all the selection objects listed provided you had a drawing open with objects in it.
Hope this will work, let me know if you need any more help.
WcfTestClient.exe will not work on my setup. It always crashes and never loads up the service. I got WCFStorm and got the logger going (config file needed to be in autoCAD directory for it to start writing files).
I now have the same error you were getting and it is getting logged.
When you say "crashes" what do you mean?! It goes away or just throws some exception. If the latter what does it say?! Also, you should be using the one that comes with VS
C:\Program Files (x86)\Microsoft Visual Studio 10.0\Common7\IDE\WcfTestClient.exe
If that's crashing you have an Visual Studio installation issue.
Also make sure the cmd window you are using to launch WcfTestClient.exe knows about the correct .NET path. Mine is
C:\Windows\Microsoft.NET\Framework64\v4.0.30319
I get the Exception: Object reference not set to an instance of an object
************** Exception Text **************
System.NullReferenceException: Object reference not set to an instance of an object.
at Microsoft.Tools.Common.SdkPathUtility.GetRegistryValue(String registryPath, String registryValueName)
at Microsoft.Tools.Common.SdkPathUtility.GetSdkPath(Version targetFrameworkVersion)
at Microsoft.Tools.TestClient.ToolingEnvironment.get_MetadataTool()
etc.....
Ok, I don't know. But good thing you can at least see it with WCFStorm. The weird stuff is when you add that SendCommand I posted above. That invokes the DOSELECTION command that works when invoked from the command line and still results in the same exact error on the same line of code when called through wcf SendCommand...
This works for me:
Public Sub DoSelection() Try activeDoc = MgdAcApplication.DocumentManager.MdiActiveDocument Using acDocLck As DocumentLock = activeDoc.LockDocument() Dim td As AcadDocument = DocumentExtension.GetAcadDocument(activeDoc) Dim ssetObj As AcadSelectionSet = td.SelectionSets.Add("SSALL0") ' creates named selection set ssetObj.Select(AcSelect.acSelectionSetAll) log.Trace("We have a selection of size = {0}", ssetObj.Count) activeDoc.Editor.WriteMessage("We have a selection of size = {0}", ssetObj.Count) Autodesk.AutoCAD.ApplicationServices.Application.ShowAlertDialog("We have a selection of size = " & ssetObj.Count.ToString()) Try For Each ent As AcadEntity In ssetObj log.Trace("{0}", ent.ObjectName) Next Catch log.Error("This is the crash: '{0}'", Err.Description) End Try End Using Catch log.Error(Err.Description) End Try End Sub
At the beginning right after try I added this line:
activeDoc = MgdAcApplication.DocumentManager.MdiActiveDocument
When you step through the code it looks like active document is alright, but this class gets instantiated by the WCF system and when this property is initialized something is not connecting properly.
To ensure this is working on the current active document maybe this should be a method instead of a property so it always checks and returns the active document?
Results:
2013-01-29 14:22:03.3589 INFO Listening on http://127.0.0.1:7200/ISelectItems
2013-01-29 14:22:18.6964 INFO This will crash
2013-01-29 14:22:18.7094 TRACE We have a selection of size = 13
2013-01-29 14:22:21.1977 TRACE AcDbLine
2013-01-29 14:22:21.1977 TRACE AcDbLine
2013-01-29 14:22:21.1977 TRACE AcDbLine
2013-01-29 14:22:21.1977 TRACE AcDbLine
2013-01-29 14:22:21.1977 TRACE AcDbLine
2013-01-29 14:22:21.1977 TRACE AcDbLine
2013-01-29 14:22:21.1977 TRACE AcDbLine
2013-01-29 14:22:21.1977 TRACE AcDbLine
2013-01-29 14:22:21.1977 TRACE AcDbLine
2013-01-29 14:22:21.1977 TRACE AcDbLine
2013-01-29 14:22:21.1977 TRACE AcDbLine
2013-01-29 14:22:21.1977 TRACE AcDbLine
2013-01-29 14:22:21.1977 TRACE AcDbLine
I think the difference is because the Initialize plugin method is run by AutoCAD so when the current document is set it is correct. The WCF deals with the wcfSelectItems class and when this class is created it is not in the same context as when AutoCAD loads the plugin class.
I tried this solution but I think you are right somehow I have to force autocad to activate the current document. If I make the change you are suggesting I get a Object reference not set to an instance of an object. I also tried to make it a function that returns the document: same result.
Private Function activeDoc() As Document
Return MgdAcApplication.DocumentManager.MdiActiveDocument
End Function
Public Sub DoSelection()
Try
Dim activeDoc As Document = Autodesk.AutoCAD.ApplicationServices.Application.DocumentManager.MdiActiveDocument
Using acDocLck As DocumentLock = activeDoc.LockDocument()
Is there any difference if you leave your property how you originally had it and change the line line after try to:
activeDoc = MgdAcApplication.DocumentManager.MdiActiveDocument
My code works if I take your original sample and just add this one line, I still have the property in my code exactly how you had yours in the first post, I just update the activeDoc at the start of the method just after try
I have AutoCAD Electrical 2013, but should work the same for this example. I am attaching my project that works.