Running code from a wpf, thread issue?

Running code from a wpf, thread issue?

Anonymous
Not applicable
3,659 Views
15 Replies
Message 1 of 16

Running code from a wpf, thread issue?

Anonymous
Not applicable

I use VB.net for my commands.

I suspect there is a threading issue going on between my wpf and Autocad, (which is hosting the wpf through the palette set), because If i place the code directly under a command it all seems to work perfectly.

 

 

Problem 1:

I have selection code that allows the user to select multiple objects.(No problems here, works perfectly)

If I run the same code from a WPF button click event, the first selected item acts normally, then every selection afterwards lags and only the last item selected exists in the SelectionSet. However, if I preselect the items and then run the selection event, all items are read correctly.

 

Problem 2:

After the selection in Problem 1 is finished. I get all the layer names of the selected entities (OpenMode.ForRead) and store in a list of strings. I then read all the entities in the drawing with the same layernames and change their layer color index (Default is OpenMode.ForRead but UpgradeOpen all the IEnumerable entities). Sometimes this works, most of the time I get a fatal error and a message eNotOpenForWrite even though this entity has just been opened for write and still exists within the same transaction.

 

 

 

' --------------------------------------------------------------
' WPF Event
' --------------------------------------------------------------
Private Sub Button_Select_Click(ByVal sender As Object, ByVal e As EventArgs)

	' Get the Layer names from user selection
	'
	Dim bRC As Boolean = False
	bRC = GetYKCSelection()
	If bRC = False Then
		Exit Sub
	End If

	' Turn LayerGroup entities' color property Red
	'
	TurnLayersRed()

	' Regen drawing
	CurrentEditor.Regen()
End Sub


' --------------------------------------------------------------
' Selection
' --------------------------------------------------------------
Public Function GetYKCSelection() As Boolean

	Dim selectionSet As SelectionSet = Nothing

	If IsSomethingSelected = True Then
		' Get Pre-selection
		selectionSet = GetCurrentSelection()
	Else
		' Get Post-Selection
		selectionSet = GetNewSelection() '------------- Strange selection behavior happens here
	End If

	' Leave if nothing selected
	If selectionSet Is Nothing Then
		Return Nothing
	End If

	LayerNames = New List(Of String)
	Dim ent As Entity = Nothing

	Using acTrans As Transaction = ActiveDocument.Database.TransactionManager.StartTransaction()
		For Each so As SelectedObject In selectionSet
			Try
				ent = acTrans.GetObject(so.ObjectId, OpenMode.ForRead)

				If ent IsNot Nothing Then
					If Not LayerNames.Contains(ent.Layer) Then
						LayerNames.Add(ent.Layer)
					End If
				End If

			Catch e As System.Exception
			End Try
		Next
		acTrans.Commit()
	End Using

	If LayerNames.Count > 0 Then
		Return True
	Else
		Return False
	End If
End Function



Public Sub TurnLayersRed()
	Dim iLayNameToGet As String = String.Empty

	Using gDB = GSTDatabase.FromActiveDocument()
		Using locked As DocumentLock = ActiveDocument.LockDocument
			For Each strLayer As String In LayerNames

				' Get all Entities from the drawing
				' where the layer name is the same as
				' what we want and return all those
				' entities ObjectId
				Dim entLayerList = From ent In gDB.ModelSpace().ForWrite()
								   Where ent.Layer = strLayer
								   Select ent
				
				For Each et As Entity In entLayerList
					Try
						' Set current color layer index to Red
						If et.IsWriteEnabled Then            '------------------ Fatal error happens here
							et.ColorIndex = 1           
						End If
					Catch acEx As Autodesk.AutoCAD.Runtime.Exception
						Continue For
					Catch ex As System.Exception
						Continue For
					End Try
				Next
			Next

		End Using
	End Using
End Sub

' --------------------------------------------------------------
''' <summary>
''' Checks if something has been selected
''' </summary>
''' <returns></returns>
''' <remarks></remarks>
Public Function IsSomethingSelected() As Boolean

	Dim psr As PromptSelectionResult = Nothing
	psr = CurrentEditor.SelectImplied()

	If psr.Status = PromptStatus.OK Then
		Return True
	End If

End Function

' --------------------------------------------------------------
''' <summary>
''' Gets the currently selected items (Pre-Selection)
''' </summary>
''' <returns></returns>
''' <remarks></remarks>
Public Function GetCurrentSelection() As SelectionSet

	Dim psr As PromptSelectionResult = Nothing
	psr = CurrentEditor.SelectImplied()

	If psr.Status = PromptStatus.OK Then
		Return psr.Value
	Else
		Return Nothing
	End If

End Function

' --------------------------------------------------------------
''' <summary>
''' Gets a new selection (Post-Selection)
''' </summary>
''' <returns></returns>
''' <remarks></remarks>
Public Function GetNewSelection() As SelectionSet

	Dim psr As PromptSelectionResult = Nothing
	psr = CurrentEditor.SelectImplied()

	' Clear the selection set
	Dim idarrayEmpty() As ObjectId
	CurrentEditor.SetImpliedSelection(idarrayEmpty)

	' Prompt for selection            
	psr = CurrentEditor.GetSelection()
	If psr.Status = PromptStatus.OK Then
		Return psr.Value
	Else
		Return Nothing
	End If

End Function

The IEnumerable section for entity retrieval can be found at:

https://wtertinek.com/2016/07/06/linq-and-the-autocad-net-api-final-part/

 

This is a piece of a very large project. If a functional version is desired please email me and I will see what I can do.

I believe this is a thread issue because I have seen similar behavior when programming with CATIA (and that solution was to invoke the command)

I am not sure how or where to invoke the selection command nor how to Invoke an UpgradeOpen.

 

Any help would be awesome. Thank you!

 

0 Likes
Accepted solutions (1)
3,660 Views
15 Replies
Replies (15)
Message 2 of 16

FRFR1426
Collaborator
Collaborator

When you're working with a palette set, the recommended approach is to wrap each action in a separate command. This way you can avoid a lot of problems. So create a command for each event handler which interacts with AutoCAD and simply call SendStringToExecute in your handler.

 

And please DON'T swallow exceptions. At least you should log it.

Maxence DELANNOY
Manager
Add-ins development for Autodesk software products
http://wiip.fr
Message 3 of 16

Anonymous
Not applicable

Thank you for your response.

 

I have a second wpf UserControl that has separate commands for each button and this works as you say, but this wpf works a little differently. Button 1 gathers names, button 2 and 3 will use those names to alter/edit the entities in the drawing. I am hoping to avoid using a global storage if at all possible. If the SendStringToExecute is the only way then I guess I have no choice.

 

This code is still in early development and I haven't included all the exception logging yet. However, the eNotOpenForWrite error never gets caught by the exception handler (it never enters the catch block, I had an Editor.WriteMessage() in there to see if it got hit, but it never does. Most of the time it just crashes AutoCAD afterwards with a fatal internal error. Not sure if that's a bug or not. I have a strong feeling it's a threading issue that's causing all these issues.

 

I wish I knew more about the internal workings of AutoCAD.net. Is it possible to even run the wpf commands without the SendStringToExecute command and still get the same results as if it was run as SendStringToExecute?

 

Thanks again!

 

 

0 Likes
Message 4 of 16

FRFR1426
Collaborator
Collaborator

Not sure that it could change something, but you're not locking your document in the GetYKCSelection method. And we don't know what you're doing in your ModelSpace() extension method. And why are you testing IsWriteEnabled?

Maxence DELANNOY
Manager
Add-ins development for Autodesk software products
http://wiip.fr
0 Likes
Message 5 of 16

Anonymous
Not applicable

I tried Locking the document in the selection code but no change.

 

The ModelSpace() code gathers all entities in the model space in Read mode; class code is as follows:

 

Public Class GSTDatabase
	Implements IDisposable

	' ------------------------------------------------------
	Private __db As Database = Nothing
	Private __tr As Transaction = Nothing

	' ------------------------------------------------------
	Public Property db As Database
		Get
			Return Me.__db
		End Get
		Set(value As Database)
			Me.__db = value
		End Set
	End Property

	' ------------------------------------------------------
	Public Property tr As Transaction
		Get
			Return Me.__tr
		End Get
		Set(value As Transaction)
			Me.__tr = value
		End Set
	End Property
	' ------------------------------------------------------

	' ------------------------------------------------------
	' Generic Get all entities in document
	Private Function GetEntities(getBlockID As Func(Of BlockTable, ObjectId)) As IEnumerable(Of Entity)
		Dim blockTable = DirectCast(tr.GetObject(db.BlockTableId, OpenMode.ForRead), BlockTable)
		Dim block = DirectCast(tr.GetObject(getBlockID(blockTable), OpenMode.ForRead), BlockTableRecord)

		Dim oItems As New List(Of Entity)()
		For Each entityID As ObjectId In block
			Dim tEntity = DirectCast(tr.GetObject(entityID, OpenMode.ForRead), Entity)
			oItems.Add(tEntity)
		Next
		Return oItems
	End Function

	' ------------------------------------------------------
	' Returns all Entities in the Model Space
	Public ReadOnly Property ModelSpace() As IEnumerable(Of Entity)
		Get
			Return GetEntities(Function(table) table(BlockTableRecord.ModelSpace))
		End Get
	End Property
	
' ------------------------------------------------------
    Public Shared Function FromActiveDocument() As GSTDatabase
         Return New GSTDatabase(ActiveDocument.Database)
    End Function
' ------------------------------------------------------ Private Sub New(iDb As Database) db = iDb tr = db.TransactionManager.StartTransaction() End Sub ' ------------------------------------------------------ Private disposedValue As Boolean ' To detect redundant calls ' ------------------------------------------------------ ' IDisposable Protected Overridable Sub Dispose(disposing As Boolean) If Not Me.disposedValue Then If disposing Then ' TODO: dispose managed state (managed objects). If Me.__tr IsNot Nothing And Not Me.__tr.IsDisposed Then Me.__tr.Commit() Me.__tr.Dispose() End If End If Me.__tr = Nothing End If Me.disposedValue = True End Sub ' ------------------------------------------------------ ' This code added by Visual Basic to correctly implement the disposable pattern. Public Sub Dispose() Implements IDisposable.Dispose ' Do not change this code. Put cleanup code in Dispose(disposing As Boolean) above. Dispose(True) GC.SuppressFinalize(Me) End Sub End Class

 

To be able to write to the entities I use an IEnumerable extension to UpgradeOpen:

 

 

' --------------------------------------------------------------
' IEnumerable Extension Functions
' --------------------------------------------------------------
Public Module IEnumerableExtensions

	' --------------------------------------------------------------
	' Upgrades entities to be Write
	<System.Runtime.CompilerServices.Extension> _
	Public Function ForWrite(Of T As DBObject)(source As IEnumerable(Of T)) As IEnumerable(Of T)
		Using docLock = ActiveDocument.LockDocument
			For Each item In source
				If Not item.IsWriteEnabled Then
					item.UpgradeOpen()
				End If
			Next
			Return source
		End Using
	End Function

	' --------------------------------------------------------------
	' Downgrades entities to Read
	<System.Runtime.CompilerServices.Extension> _
	Public Function ForRead(Of T As DBObject)(source As IEnumerable(Of T)) As IEnumerable(Of T)
		Using docLock = ActiveDocument.LockDocument
			For Each item In source
				If item.IsWriteEnabled Then
					item.DowngradeOpen()
				End If
			Next
			Return source
		End Using
	End Function

	' --------------------------------------------------------------
	' ForEach extension used with ForWrite and ForRead
	<System.Runtime.CompilerServices.Extension> _
	Public Sub ForEach(Of T)(source As IEnumerable(Of T), action As Action(Of T))
		For Each item In source
			action(item)
		Next
	End Sub
End Module

 

I have also tried bypassing the ForWrite extension by changing the ModelSpace code:

 

' --------------------------------------------------------------
Private Function GetEntitiesForWrite(getBlockID As Func(Of BlockTable, ObjectId)) As IEnumerable(Of Entity)
	Dim blockTable = DirectCast(tr.GetObject(db.BlockTableId, OpenMode.ForRead), BlockTable)
	Dim block = DirectCast(tr.GetObject(getBlockID(blockTable), OpenMode.ForRead), BlockTableRecord)

	Dim oItems As New List(Of Entity)()
	Using docLock = ActiveDocument.LockDocument
		For Each entityID As ObjectId In block
			Dim tEntity = DirectCast(tr.GetObject(entityID, OpenMode.ForWrite), Entity)
			oItems.Add(tEntity)
		Next
	End Using
	Return oItems
End Function

' --------------------------------------------------------------
Public ReadOnly Property ModelSpaceForWrite() As IEnumerable(Of Entity)
	Get
		Return GetEntitiesForWrite(Function(table) table(BlockTableRecord.ModelSpace))
	End Get
End Property

Sadly this still produces the same eNotOpenForWrite error.

 

Another interesting thing I discovered while trying to debug this; the ForWrite extension is UpgradeOpen-ing about 8-12 entities, then the next 8-12 don't change, then the following 8-12 do change, etc. The isWriteEnabled should see these and skip them, but instead it throws the, so far, uncatchable exception. This error also happens whether I test IsWriteEnabled or not.

 

0 Likes
Message 6 of 16

norman.yuan
Mentor
Mentor

I think your issue is not related to WPF itself. Rather, it has much to do with the fact of using modeless/floating form (PaletteSet, in your case). Also you referred to a blog on using Linq in Acad .NET API and used patterns introduced in that blog articles improperly.

 

Firstly, with a floating form/PaletteSet, the user could freely switch AutoCAD  focus between the form and the editor/documents. Thus, each action caused by the form (according to user interaction, such as button click) SHOULD BE wrapped in a transaction with locked document (using SendStringToExecute() is one of the tricks of doing it).

 

From the partial code you showed here in your latest post, as I can see, the ModelSpace in your code s a collection of Entity. It is likely you used the entities outside of the current Transaction. 

 

The LinQ code showed in the said blog articles are meant to be used inside a transaction scope (the article series of that blog actually emphasizes the importance of transaction scope at the beginning of the article series).

 

As I said, because PaletteSet. as floating form, does not lock Acad, it is very important to wrap a chunk of operation based on user interaction inside a give transaction scope.

 

Without seeing complete code, it is hard to say exactly what went wrong, but I think using DBObject/Entity out of the transaction, where they are generated, and in conjunction with floating window is likely the cause of your issue. And it definitely has nothing to do with WPF. If you use similar code with a modal form, you code might work OK.

 

Norman Yuan

Drive CAD With Code

EESignature

Message 7 of 16

FRFR1426
Collaborator
Collaborator

As @norman.yuan says, your Linq stuff make the workflow difficult to follow. It's difficult to check if your entities are not used outside of theirs transaction. And I don't see where is the benefit. If I need to filter the database, I use Editor.SelectAll with a SelectionFilter, which must be more efficient than looping on all the entities.

 

For your selection problem, I don't understand why you are calling SelectImplied & SetImpliedSelection in your GetNewSelection method. You just have tested before if there are an implied selection. If there is no implied selection, you don't need to clear it.

Maxence DELANNOY
Manager
Add-ins development for Autodesk software products
http://wiip.fr
0 Likes
Message 8 of 16

Anonymous
Not applicable

Thank you for the feedback.

 

The Linq stuff is all contained within the same transaction scope as in the code from my first post:

 

 

Public Sub TurnLayersRed()
    Dim iLayNameToGet As String = String.Empty

    Using gDB = GSTDatabase.FromActiveDocument() ' Starts the Linq stuff's transaction
        Using locked As DocumentLock = ActiveDocument.LockDocument
            For Each strLayer As String In LayerNames

                Dim entLayerList = From ent In gDB.ModelSpace().ForWrite()
                                   Where ent.Layer = strLayer
                                   Select ent
				
                For Each et As Entity In entLayerList
                    Try
                        ' Set current color layer index to Red
                        et.ColorIndex = 1  ' <------ Fatal error happens here
                    Catch acEx As Autodesk.AutoCAD.Runtime.Exception
                        Continue For
                    Catch ex As System.Exception
                        Continue For
                    End Try
                Next
            Next

        End Using ' Dispose of locked
    End Using ' Dispose of Linq stuff's transaction
End Sub

It's all contained in the using statement and the transaction is disposed at the end of the using statement. I tried to make sure it is all contained within the same transaction, the only time it might not be is the ForWrite() extension which is in the extensions class outside the GSTDatabase class.

 

 

So I guess that leaves the modeless paletteset. I was starting to lean toward this as the most likely cause. I will try a modal form and if that doesn't work I will have to change to the SendStringToExecute() for these commands since I know that works.

 

The Linq stuff works great when reading, the writing portion has proven to be a bit challenging and not quite as reliable as I had hoped. The Linq stuff has proven to be more of an exercise in "can it be done"; and in this case: Reading?, yes; Writing? not so much, at least from a modeless standpoint.

 

 

Thank you both for your help. If I have any updates in this matter I will post them here.

0 Likes
Message 9 of 16

Anonymous
Not applicable

Okay, so the modeless/floating form is definately the issue with the selection. It seems the form is running on a child thread/process while AutoCAD runs in a parent thread/process. The SendStringToExecute() invokes a call to the parent to perform the action at the parent level. Running code directly form the wpf button click event (as a child) was causing cross thread/process issue.

 

So this brings me back around and makes me ask, is it possible to invoke code to run in the autocad parent from the modeless/floating child without using the SendStringToExecute()? Or better, what exactly are in the internal workings of SendStringToExecute() and can it be reproduced?

 

 

I did find that the documentation regarding IsWriteEnabled which states:

 

Returns true if the object is currently /c> and is not currently sending notification.

So I wonder what exactly happens when it is currently sending notifications? Does it throw an exception or is it just skipped?

 

 

Also regarding Linq code I am using, it is entirely based on the blog link I posted. I read everything on his blog and translated it to VB.net (minus the block import as I already have a working block import class and I didn't need another) I am attaching this class as well as the extension class and a small command that shows how to use it. I do understand the importance of staying within the transaction scope. I may have translated something wrong, c#.net to VB.net isn't exactly straight forward.

0 Likes
Message 10 of 16

FRFR1426
Collaborator
Collaborator

Which version of AutoCAD are you using?

Maxence DELANNOY
Manager
Add-ins development for Autodesk software products
http://wiip.fr
0 Likes
Message 11 of 16

Anonymous
Not applicable

Autocad Mechanical 2014 and Visual Studio 2012

0 Likes
Message 12 of 16

FRFR1426
Collaborator
Collaborator
Accepted solution

OK, so fibers are activated by default in this version of AutoCAD. Fibers are lightweight thread of execution. In the context of AutoCAD, they have been used to manage document switching and command execution. It allows to save/restore the stack when you switch between 2 documents. This way, if you switch to a new document while a command is active, the command can resume its execution when you come back to the initial document.

 

Your palette set is running into the application context, in a fiber which is not the same fiber than the active document which is running in document context. May be it the root of your problems.

 

SendStringToExecute is the only way I know to go from the application context to the document context.

Maxence DELANNOY
Manager
Add-ins development for Autodesk software products
http://wiip.fr
Message 13 of 16

Anonymous
Not applicable

Ahhh, okay. So it is not a cross thread issue but rather a cross fiber issue. After reading your post I remembered seeing something once before involving fibers and Autocad and went looking for it. Through-the-Interface has a small article on the switch from SDI to MDI which is where I first heard about it. This would have been an important thing to remember! I have since switched my wpf commands over to SendStringToExecute() and the selection process now works normally as expected.

 

My LinQ problem is most likely not be related to my other problem as I had first thought. My eNotOpenForWrite error may be how VB.net handles IEnuerable as opposed to c#.net. i.e. The original c# code in the blog has "yield return" while vb.net does not (vb.net requires a new list to be made and then return this list, which may be causing sections of the code to inadvertently go out of the transaction scope). I won't know if this is true or not until I write up a c# version. In the meantime, I will set this LinQ code aside.

 

Thank you so much for your help!

 

 

 

 

0 Likes
Message 14 of 16

Anonymous
Not applicable

After some investigating, I found that my LinQ code works perfectly fine ForWrite when run inside a command using SendStringToExecute from the wpf. So I think I now know what was happening with the strange pattern I saw earilier with the IEnumerable list (First 8-12 items set to Write, next 8-12 not, next 8-12 were set, etc) was due to the fiber being switched within the thread maybe. Without access to the base Autocad code I will never know just exactly what was happening but this does seem to make sense to me.

 

Lessons learned, don't run code directly from a modeless/floating palette set. Instead SendStringToExecute and create a new command for each function of the modeless form. The reason is fibers. If you want to run code without the SendStringToExecute you need access to the fiber control code and for that you need to work at Autodesk.

0 Likes
Message 15 of 16

norman.yuan
Mentor
Mentor

Glad you solved the immediate issue at hand. While SendStringToExecute is one of the solution of making floating window works, I have to say that I have being developing custom PaletteSet (with both Win Form/WPF) since the very beginning of AutoCAD .NET API. Some of them are fairly simple PaletteSets, some are quite complicated (in terms of data models and the views). I hardly use SendStringToExecute with the PaletteSets/modeless forms I developed (maybe I did with some extremely simple PaletteSets I worked in my earlier .NET API development, cannot remember now), and I have not run into issues like yours, be the AutoCAD was "Fiber-ed" (Acad2014, or earlier) or not (Acad2015 or later). The "Fiber" issue arose in Acad2009 to Acad2014 since AutoCAD introduced Ribbon, it only affects AutoCAD debugging with VS, as far as I am concerned. Except feel the inconvenience of having to constantly enabling/disabling it (and remember the status) during the development/debigging/production, I had not experienced its possible impact to running the code I wrote.

 

If one has a complicated UI in WPF and employed MVVM pattern, the coding would be quite difficult if SendStringToExecute is the only solution to PaletteSet/modless form: you have to tie the ViewModal and/or Data Repository to a CommandClass/CommandMethod, (or make the ViewModel/Repository as CommandClass/CommandMethod?). I am not necessarily saying it is very bad practice, because I have never been limited to use SendStringToExecute as the ONLY solution to modeless UI in AutoCAD, or I would not have developed the PaletteSets I have done.

 

Just so you know that for PaletteSet/modeless form, there is life beyond SendStringToExecute.

Norman Yuan

Drive CAD With Code

EESignature

Message 16 of 16

FRFR1426
Collaborator
Collaborator

There are some differences when you're working in the application context. For example, you need to explicitly call Editor.UpdateScreen() to update the display after you made some changes to the database (if you don't do that, the screen only update when the cursor comes over the drawing area).

 

If you only read or change something from your event handlers, it can work (with a standard transaction, not an OpenClose one), but if you are getting some user input (Editor.Getxxx...), I would recommend to use the SendStringToExecute approach because it can avoid focus/screen updating problems.

Maxence DELANNOY
Manager
Add-ins development for Autodesk software products
http://wiip.fr
0 Likes