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:

  1. a way to create a form on the UI thread
  2. 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.

To report this post you need to login first.

Be the first to leave a comment

You must be Logged on to comment or reply to a post.

Leave a Reply