Running winforms in a SAP Addon
Running winforms in a SAP Addon I don’t know if you ever tried running a WinForm in a SAP addon, but if you did, you found out that the WinForms will not function when you start them from a SAP event. The reason for this is that a WinForm must run on the base thread (the UI thread) of the application and events from SAP are running on separate threads that have their origins outside your application. This means that a WinForm can not function when it is created from one of these threads.
To get a WinForm running we have to ensure that it is running on the UI thread.
What we need is:
- a way to create a form on the UI thread
- a way to link the form to the SAP main form (this makes modal dialogs possible)
The get the first point done, we need to know the IWin32Window of the SAP Main form. The following code retrieves this:
Imports System.Runtime.InteropServices
Friend Class AppWindow
Implements System.Windows.Forms.IWin32Window
<DllImport(“user32.dll”, EntryPoint:=”FindWindow”, SetLastError:=True, CharSet:=CharSet.Auto)>
Private Shared Function FindWindowByCaption(ByVal zero As IntPtr,ByVal lpWindowName As String) As IntPtr
End Function
Public ReadOnly Property Handle As System.IntPtr Implements System.Windows.Forms.IWin32Window.Handle
Get
Static SB1Handle As IntPtr = IntPtr.Zero
If SB1Handle = IntPtr.Zero Then
Dim originalCaption As String = SBO_Application.Desktop.Title
SBO_Application.Desktop.Title = “###FindWindow###”
SB1Handle = FindWindowByCaption(IntPtr.Zero, “###FindWindow###”)
SBO_Application.Desktop.Title = originalCaption
End If
Return SB1Handle
End Get
End Property
End Class
The second point requires a control that is – for sure – created on the UI thread, so we can invoke this control to gain access to this thread.
To do this, we have to create a WinForm in the Sub Main of the extension and we have to make sure that the handle of this form is created; otherwise we can not use this form to invoke the UI thread. To create the handle we have to show the WinForm and reference the Handle property:
Friend Sub InitializeMainThreadForm()
MyBase.Show()
Dim x As IntPtr = Me.Handle ‘force handle create
Me.Hide()
End Sub
The reason for the mybase.show instead of me.show lays in the fact that we will reimplement the show method for the class that will contain this code.
Now we need a way to invoke the WinForm in such a way that we will get the exceptions on the calling thread instead of silent failing or crashing.
The following code does just that:
Public Shadows Function invoke(method As [Delegate]) As Object
Return invoke(method, New Object() {})
End Function
Public Shadows Function invoke(method As [Delegate], args() As Object) As Object
Dim ReturnValue As Object
JustInvoked = True
If args.Count = 0 Then
ReturnValue = MyBase.Invoke(method)
Else
ReturnValue = MyBase.Invoke(method, args)
End If
JustInvoked = False
If InvokeException IsNot Nothing Then
Dim ex As Exception = InvokeException
InvokeException = Nothing
Throw ex
Else
Return ReturnValue
End If
End Function
The code reimplements the invoke method of the WinForm and when an exception occurred during the invoke execution, this exception is thrown on the SAP thread. The JustInvoked flag is to inform the running code that is is running from an invoke call.
Now we can reimplent the Show method for the form:
Public Shadows Sub Show()
If JustInvoked Then
‘Running in an invoke call -> to prevent crashes, we need to catch and save exceptions
Try
MyBase.Show(New AppWindow)
Catch ex As Exception
InvokeException = ex
End Try
Else
‘not from an invoke, so call the original code
MyBase.Show(New AppWindow)
End If
End Sub
With “New AppWindow”, we make sure that the SAP main window is the owner of the form and the try-except block makes sure that when an exception occures it is saved instead of thrown to prevent the application from crashing.
And we can reimplement the ShowDialog method. But a ShowDialog is a blocking call, in which case we have to call RemoveWindowsMessage to prevent errors in SAP. To do this we use a timer (not a windows forms timer, a windows form timer will not function, because it needs to run on the UI thread!):
Private WithEvents tmr As Timers.Timer
Public Shadows Function ShowDialog() As DialogResult
Return ShowDialog(Nothing)
End Function
Public Shadows Function ShowDialog(owner As System.Windows.Forms.Form) As DialogResult
If JustInvoked Then
Return ShowDialogFromInvoke(owner)
Else
If owner Is Nothing Then
Return MyBase.ShowDialog(New AppWindow)
Else
Return MyBase.ShowDialog(owner)
End If
End If
End Function
Private Function ShowDialogFromInvoke(owner As System.Windows.Forms.Form) As DialogResult
tmr = New Timers.Timer
tmr.Interval = 1000
tmr.AutoReset = True
tmr.Start()
Dim returnvalue As DialogResult
Try
If owner Is Nothing Then
returnvalue = MyBase.ShowDialog(New AppWindow)
Else
returnvalue = MyBase.ShowDialog(owner)
End If
Catch ex As Exception
InvokeException = ex
End Try
tmr.Stop()
tmr.Dispose()
Return returnvalue
End Function
Private Sub tmr_Elapsed(sender As Object, e As System.Timers.ElapsedEventArgs) Handles tmr.Elapsed
SBO_Application.RemoveWindowsMessage(SAPbouiCOM.BoWindowsMessageType.bo_WM_TIMER, True)
End Sub
During the blocking ShowDialog call, every second a RemoveWindowsMessage will is send to SAP to keep it alive. The SAP main window will be the owner of the form and exceptions are saved instead of thrown when it is called from an invoke.
And at last, we need an easy way to access the main thread to create our forms. For this we create a shared method RunOnMainThread:
Public Function RunOnMainThread(method As [Delegate]) As Object
If MainThreadForm IsNot Nothing Then
If MainThreadForm.InvokeRequired Then
Return MainThreadForm.invoke(method)
Else
Return method.DynamicInvoke()
End If
Else
Return method.DynamicInvoke()
End If
End Function
When we put add all together, we get the class SapBaseWinForm.vb, which is attached to this document.
Add this file to your project and add the following lines to your Sub Main (before any WinForm is created):
MainThreadForm = New SapBaseWinForm
MainThreadForm.InitializeMainThreadForm()
Now you can create and run any WinForm that inherits from SapBaseWinForm with the following code:
RunOnMainThread(Sub()
Dim sw As New frmYourForm
sw.ShowDialog() ‘or sw.Show() if you want a non-modal form
End Sub)
The created windows are not contained within the SAP main window but will be invisible when the SAP application is minimized.
Modal forms that you create from a SAP thread will block access to the complete SAP main window as long as these forms are open.
Update:
To create a new form from within another WinForm, just call show or showdialog, just as you always did; you are already running on the UI thread when the code in your form executes – so there is no need to call RunOnMainThread when you are in a WinForm.
Update 2:
Updated the attached file; the class AppWindow was missing in the file.
Update 3:
Modal WinForms that you create from a SAP thread will block the SAP main window, but modal WinForms that you create from another non-modal WinForm will only block the underlying WinForms, not the complete SAP main window.
Hope you have fun using WinForms in SAP.
New NetWeaver Information at SAP.com
Very Helpfull