views:

800

answers:

2

I'm trying to use a loading overlay on top of a Windows form that adds a 50% opaque layer on top of a windows form with a loading GIF, while it does what it needs to do in a background thread. The overlay is a windows form that I'm drawing in the onPaint event.

The loading overlay works fine on multiple different form loads, but fails to work properly when summoned to ensure patience during a 30 second upload process (That Prints a Word Document to PDF, then uploads that PDF to a SQL Server). This upload collects some data from the form, puts it into an Object, then operates entirely on a background thread. The loading overlay will appear, show the first frame loading GIF, then just freeze. The onPaint is getting fired and the image frame is being updated, but it isn't visible

Constructor sets the form to be UserPainted:

Sub New()
    InitializeComponent()
    SetStyle(ControlStyles.UserPaint Or ControlStyles.Opaque, True) 
End Sub

Then, in the Form.Shown event the ImageAnimator.Animate method is called:

Private Sub LoadingOverlay_Shown(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Shown
    If Not currentlyAnimating Then
        ImageAnimator.Animate(animatedImage, AddressOf Me.OnFrameChanged)
        currentlyAnimating = True
    End If
End Sub

The onFrameChanged Event Handler just Invalidates the form:

Private Sub OnFrameChanged(ByVal sender As Object, ByVal e As System.EventArgs)
    'Force a call to onPaint
    Me.Invalidate()
End Sub

Then onPaint is overridden and does the drawing:

Protected Overrides Sub OnPaint(ByVal e As PaintEventArgs)
    MyBase.OnPaint(e)

    'Get the next frame ready for rendering
    ImageAnimator.UpdateFrames()

    'Draw the next frame in the animation.
    e.Graphics.DrawImage(Me.animatedImage, GetCenter(Me.animatedImage.Size))
    TextRenderer.DrawText(e.Graphics, strStatus, Me.Font, GetTextLocation(Me.animatedImage.Size), Color.White, Color.Black)

End Sub

Finally, the ImageAnimator.StopAnimate method is called in the Form Closing event:

Private Sub LoadingOverlay_FormClosed(ByVal sender As Object, ByVal e As System.Windows.Forms.FormClosedEventArgs) Handles Me.FormClosed
    If currentlyAnimating Then
        ImageAnimator.StopAnimate(animatedImage, AddressOf Me.OnFrameChanged)
        currentlyAnimating = False
    End If
End Sub

Here's how the loading overlay is being called and closed:

Private Sub ShowLoadingOverlay()
    If Not blnLoadingOverlayVisible Then

        Me.Enabled = False

        patience = New LoadingOverlay()
        patience.Location = Point.Add(parent.PointToScreen(Me.Location), New Size(0, parent.ToolStrip.Height + parent.MenuStrip.Height))
        patience.Size = Me.Size
        patience.Show()
        patience.BringToFront()

        blnLoadingOverlayVisible = True

    End If
End Sub

Private Sub HideLoadingOverlay()
    If blnLoadingOverlayVisible Then
        'Close loading overlay'
        patience.Close()
        patience.Dispose()
        patience = Nothing
        Me.Enabled = True
        blnLoadingOverlayVisible = False
    End If
End Sub
A: 

Initially everything is happening in a single background thread. There are 3 methods total that run. When I moved the upload thread (which does about 1/2 the work) into another background thread, everything is "ok" (not perfect, GIF still jumps a little at the beginning).

The upload method that was moved into a second background thread doesn't interact with the GUI thread at all, it just does some COM (word) and SQL stuff. Long story short, it's working, but I still have no idea why. Any insight would be greatly appreciated!

Joey
+1  A: 

"some COM" is relevant. You'll hit COM's apartment threading rulez. A COM object like Word must be created on an STA thread. Your main UI thread qualifies, it starts at Main() and it has the [STAThread] attribute. Any method calls you make on another thread are automatically marshaled by COM to the STA thread. Gumming up your animation.

This isn't easy to fix. You'd need a background thread that must be an STA thread as well, use Thread.SetApartmentState(). And pump a message loop, use Application.Run(). Getting your code started and exiting the loop is awkward, try using a form that overrides SetVisibleCore() so you can avoid making it visible.

Hans Passant
This sounds great. Just to clarify: I should try putting all the background work (COM) into sort of a dummy form thats started by calling Application.Run(dummyForm) from a bg STA thread. Thanks for the help!
Joey