0

I am currently using some primitive animations in vb.net to improve the GUI for the end client. It's a thin panel strip that loops to extend underneath the textbox when it's entered.

It works pretty well. The problem I have is that the animation has to complete before the textbox will accept text. This means that when a user tabs between the "username" and "password" text boxes, they sometimes start typing too quickly for the animation to complete and therefore miss the first few characters of their password.

I have tried adding the "animation" code to a background worker, but this completely freezes the UI for around 20 seconds when tabbing between the two textboxes (it works fine when clicking, but tabbing causes the worker thread to freeze).

I have done some research and despite the suggestion, I'm not sure if a background worker would work in this instance, as ultimately, it'll just be updating the UI controls and that seems like it's against what the background worker is there to do.

This is how I had things setup:

Private Sub bwUsernameLines_DoWork(sender As Object, e As DoWorkEventArgs) Handles bwUsernameLines.DoWork
        Dim x As Integer = 0

        pnlUsername_under.Width = x
        pnlUsername_under.Visible = True

        'loop speed 1
        Do Until x = 180
            pnlUsername_under.Width = x
            Threading.Thread.Sleep(5)
            pnlUsername_under.Refresh()
            x += 10
        Loop

        bwUsernameLines.CancelAsync()
End Sub

Private Sub txtUsername_Enter(sender As Object, e As EventArgs) Handles txtUsername.Enter
        bwUsernameLines.RunWorkerAsync()
        txtUsername.Text = vbNullString
End Sub

Tabbing into the textbox causes the UI to freeze whilst clicking into the textbox doesn't. Is there something I'm doing wrong to allow this to work, or is there a completely different way of doing this?

Essentially, all I want is for the loop animation to continue to play, whilst the textbox allows the entering of text still, rather than waiting for it to complete.

RazorKillBen
  • 561
  • 2
  • 20
  • 2
    You shouldn't update the UI in the DoWork handler. There is ReportProgress method that when called raises the ProgressChanged event and this handler runs in the UI thread – Steve Aug 04 '19 at 12:45
  • Thanks @Steve - I had assumed the DoWork handler wasn't for UI updates but wasn't quite sure. Are you able to elaborate on how I can implement that in the above at all? – RazorKillBen Aug 04 '19 at 12:49
  • It shouldn't even be possible to touch the UI from a background thread/background worker as it _should_ throw an `InvalidOperationException`. Have you by any chance set `Control.CheckForIllegalCrossThreadCalls = False` anywhere in your code? If so you need to remove that as you've disabled important protections that are there to stop you from doing bad things. – Visual Vincent Aug 05 '19 at 05:18
  • Yes @VisualVincent - you got me... I did turn this off to allow the app to call the UI thread. I followed a bad tutorial that mentioned this. I can't seemingly work out how to utilise the background worker for the UI though. – RazorKillBen Aug 05 '19 at 09:49
  • Well, seeing as the background worker wasn't meant to work with the UI it's not surprising that you find it a bit difficult. Marshalling calls or synchronizing data between _any_ two threads is difficult due to the fact that they run independently of each other and are therefore prone to race conditions. When it comes to multithreading, you should strive to keep the threads as separate from each other _as possible_. .NET Framework does however offer many different ways for us to synchronize threads when you need them to communicate at some point. – Visual Vincent Aug 05 '19 at 10:36
  • In your case when working with animations I'd recommend you do _everything_ on the UI thread. You can use a `System.Windows.Forms.Timer` which executes code on the UI thread at a set interval, and do your animation in there. However, for future reference when you might indeed need to access/exchange information with the UI from a background thread, [this answer of mine](https://stackoverflow.com/a/45571728) might be of help to you (ignore the first part and start at _**The basics**_). Be sure to read to the end as I begin by explaining how the basics work, then how you can make it easier. – Visual Vincent Aug 05 '19 at 10:44
  • Thanks for this @VisualVincent - it's a really interesting read and thorough answer. I think you're right however; to complete an animation on the UI thread without blocking it, doesn't seem like the BackgroundWorker is really the right way to go. Using a Timer seems a far more appropriate option - can it execute the animation without blocking the UI thread / input? At the moment, it's an Event driving a loop which prevents input until completed. Can the timer event on click, perform the same loop without blocking input? Thanks for your help. – RazorKillBen Aug 06 '19 at 14:32
  • Unfortunately no. You have to refactor your code so that it only increments once every timer tick. Think of the timer _as_ your loop. Declare `x` outside the timer and increment it inside, remove the sleep + refresh and change `Do Until x = 180` to `If x < 180 Then`. – Visual Vincent Aug 06 '19 at 14:54

1 Answers1

1

First thing to do is to change where you handle your panel resize. Has you have correctly understood, changing UI objects from a NON-UI thread is problematic and BackgroundWorker has been created to solve this problem in a WinForms application.

So we need to set something on the BackgroundWorker to prepare it for its task.
(You can do these changes through the Winform Designer or in code in your Form constructor but after the InitializeComponent)

' Define the event handler that runs the resize in the UI thread
AddHandler bwUsernameLines.ProgressChanged, AddressOf bwUsernameLines_SizeChanged
' Make sure that the backgroundworker reports the progress
bwUsernameLines.WorkerReportsProgress = True

Now your DoWork can be changed to just

Private Sub bwUsernameLines_DoWork(sender As Object, e As DoWorkEventArgs) 
    Dim x As Integer = 0
    'loop speed 1
    Do Until x = 500
        Threading.Thread.Sleep(5)

        ' Raises the Progress_Changed event in the UI thread
        ' Notice that this overload takes an value that represent your progress so far
        bwUsernameLines.ReportProgress(x)
        x += 10
    Loop
End Sub

Finally just add the event handler for the Progress_Changed event

Private Sub bwUsernameLines_SizeChanged(sender As Object, e As ProgressChangedEventArgs)
   ' Get that x passed above from the property  ProgressPercentage
   pnlUsername_under.Width = e.ProgressPercentage

End Sub 
Steve
  • 213,761
  • 22
  • 232
  • 286
  • Thanks @Steve - I've added this in and then set the Enter event handler to RunWorkerAsync but it has the same results as the previous setup - it clicks in and leaves just fine, but tabbing between textboxes freezes the UI for around 20 seconds. – RazorKillBen Aug 04 '19 at 14:30
  • Sorry but I am unable to reproduce this problem. I have tested this approach with a code written in LinqPad and I don't have any delay. Is it possible that there something different at play here. I suggest you to write a test code with only two textboxes in the right tab order to see if you are able to reproduce your problem ourside the current code. – Steve Aug 04 '19 at 14:59