Updating UI from another thread with VB in WPF - wpf

I am trying to use a timer to scan my Xbox 360 controller. But I cannot directly update my UI like the code I wrote below.
I would get a exception when I try to run this code.
An exception of type 'System.InvalidOperationException' occurred in WindowsBase.dll but was not handled in user code
Additional information: The calling thread cannot access this object because a different thread owns it.
XButton is a radiobutton on the GUI that I want to toggle.
Imports Microsoft.Xna.Framework
Imports Microsoft.Xna.Framework.Input
Imports System.Timers
Imports System.Windows.Threading
Public Class XboxControllerStatus
Friend WithEvents Timer1 As Timer
Private Sub Timer1_Tick(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Timer1.Elapsed
Dim currentState As GamePadState = GamePad.GetState(PlayerIndex.One)
If currentState.IsConnected Then
If currentState.Buttons.X.Pressed Then
XButton.IsChecked = True
Else
XButton.IsChecked = False
End If
End If
End Sub
End Class

This works for me, all the time
Control.Invoke(sub()
'Put code here
End Sub)

First you need to set up a delegate
Delegate Sub SetCheckBoxCallback(ByVal value As Boolean)
Friend Sub SetCheckBox(ByVal value As Boolean)
XButton.IsChecked = value
End Sub
after that all you need to do is call the following code from within your timer to invoke it:
Dim DesiredValue as Boolean = True
Me.Dispatcher.Invoke(New SetCheckboxCallback(AddressOf SetCheckbox), New
Object() {DesiredValue})

Related

How do I declare arry of objects outside a soubroutine in VB.net

I wrote a code to demonstrate the issue:
Public Class Form1
Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
doSomething()
End Sub
Dim controlArr() As Object = {NumericUpDown1, NumericUpDown2, NumericUpDown3, NumericUpDown4, CheckBox1, CheckBox2, CheckBox3, CheckBox4}
Private Sub doSomething()
Dim testStr As String = ""
For Each control In controlArr
Select Case control.GetType
Case GetType(NumericUpDown)
control.value = 1
Case GetType(CheckBox)
control.checked = True
End Select
Next
End Sub
End Class
When I run the code I receive Null Referenece Exception "Object reference not set to an instance of an object", the error disapears when I declare the controlArr array inside DoSomething subroutine. Anyway I would prefer having it declared outside since I am using it in many functions. I would like to understand it better so if you provided me with a topic I could read up on I would be very grateful. Thank you very much for your help.
The issue is that declarations are processed before the constructor. That means that this line:
Dim controlArr() As Object = {NumericUpDown1, NumericUpDown2, NumericUpDown3, NumericUpDown4, CheckBox1, CheckBox2, CheckBox3, CheckBox4}
is executed before the code that creates all the controls on the form and assigns them to those fields. As such, all the fields are Nothing at the time that code is executed and so your array contains a whole lotta nothin'. There's no issue creating objects to initialise fields like that and that code does successfully create an array. It's just that you are implicitly setting every element of that array to Nothing so that's what you get to use later on.
If you want to reference any control then you have to wait until after the form's controls are created. That means, at the earliest, after the call to InitializeComponent in the constructor. More generally, you should do it in the Load event handler, e.g.
Dim controlArr As Object()
Private Sub Form1_Load(sender As Object, e As EventArgs) Handles Form1.Load
controlArr = {NumericUpDown1, NumericUpDown2, NumericUpDown3, NumericUpDown4, CheckBox1, CheckBox2, CheckBox3, CheckBox4}
End Sub

vb.Net: Creating UI in a BackgroundWorker

I'm working on an application to be able to monitor production information on a 3x3 (so 9 screen) video wall. One of the screen sets that I'm working on right now retrieves information and then formats for display on the screen. It takes about 2 seconds to retrieve and format this data (just a rough guess, not actually measured). Because it does 9 screen, one after the other, there is a very noticeable amount of time to switch to this screen set. The PC driving this video wall has 8 processing cores, so while one processor is chugging away doing all this work, there's plenty of processing power just sitting idle.
My first thought is that I need to use multi-threading. Unfortunately, I'm very new to this concept. I really have only used it one other time. I tried creating a BackgroundWorker and having the DoWork routine generate my UI. Unfortunately, it crashes the first time I try to create a UI element (Dim grLine as New Grid). I did manage to get around that by having a dummy DoWork routine and generating all my UI in the RunWorkerCompleted routine. This does allow my blank window to show up immediately, but none of the UI I'm generating shows up until it has all been rendered.
Here's a very cleaned up version of what I'm trying to do:
For i As Integer = 1 to 9
Dim win As New MyCustomWindow
win.DisplayScreen = i ' This function in MyCustomWindow sets the Bounds
win.MyShow({1, 2}) ' Sample args
Globals.VideoWall.Windows(i) = win
Next
The MyCustomWindow Class:
Class MyCustomWindow
Public Sub MyShow(a() as Integer)
Me.Show() ' Has a "Loading..." TextBlock
Dim bw as New ComponentModel.BackgroundWorker
AddHandler bw.DoWork, AddressOf Generate_UI_DoWork
AddHandler bw.RunWorkerCompleted, AddressOf Generate_UI_Complete
bw.RunWorkerAsync(a)
End Sub
Private Sub Generate_UI_DoWork((sender As Object, e As ComponentModel.DoWorkEventArgs)
' Pass our arguments to the Complete routine.
e.Result = e.Argument
End Sub
Private Sub Generate_OpsMarket_Complete(sender As Object, e As ComponentModel.RunWorkerCompletedEventArgs)
Dim IDs() as Integer
IDs = e.Result
Dim grLine As New Grid ' We crash here if this code is in DoWork instead of RunWorkerCompleted
For Each id As Integer In IDs
grLine.RowDefinitions.Add(New RowDefinition)
Dim txt as New TextBlock ' For a header
grLine.Children.Add(txt)
grLine.RowDefinitions.Add(New RowDefinition)
Dim MyCtrl as New MyCustomControl()
MyCustomControl.GetData(id)
grLine.Children.Add(MyCtrl.MyGrid)
txt.Text = MyCtrl.Header
Next
txLoading.Visibility = Visibility.Hidden
grRoot.Children.Add(grLine)
End Sub
End Class
I tried to leave enough detail in the code so hopefully it'll be evident what I'm trying to accomplish, but keeping it small enough to not be overwhelming.
Edited to add:
The bulk of the work happens in MyCustomControl.GetData(id) ... that Sub downloads the data from a web server (in JSON format), parses the JSON, then generates the rows (3) and columns (30 or 31, depending on the month) for the Grid and fills in the data it received from the web server.
Your current BackgroundWorker implementation give you no benefits, as you noticed by your self.
Your main problem is that your current code/logic tightly depend on UI controls. Which strict you with UI thread. Because creating/updating UI controls can be done only on UI thread - that's why you got Exception when trying create/update UI controls in BackgroundWorker.DoWork handler.
Suggest to separate logic which retrieve monitoring information in parallel and then you can create/update control on UI with already formatted data.
This is raw/pseudo example
Class DataService
Public Function GetData(ids As Integer()) As YourData
' Get data from web service
' Validate and Format it to YourData type or List(Of Yourdata)
Return data
End Function
End Class
Class MyCustomWindow
Public Sub MyShow(a() as Integer)
Me.Show() ' Has a "Loading..." TextBlock
Dim bw as New ComponentModel.BackgroundWorker
AddHandler bw.DoWork, AddressOf Generate_UI_DoWork
AddHandler bw.RunWorkerCompleted, AddressOf Generate_UI_Complete
bw.RunWorkerAsync(a)
End Sub
Private Sub Generate_UI_DoWork((sender As Object, e As ComponentModel.DoWorkEventArgs)
Dim service As New DataService()
Dim data = service.GetData(e.Argument)
e.Result = data
End Sub
Private Sub Generate_OpsMarket_Complete(sender As Object,
e As ComponentModel.RunWorkerCompletedEventArgs)
Dim data As Yourdata = DirectCast(e.Result, YourData)
'Update UI controls with already formatted data
End Sub
End Class
Update on Sub downloads the data from a web server
In this case you don't need multi-threading/parallel at all. Because you loading time is waiting time for response. In this case my advice will be using async/await approach, which will release UI thread(make it responsive) while you waiting for response from web-service.
Class DataService
Public Async Function GetDataAsync(ids As Integer()) As Task(Of YourData)
Using client As HttpClient = New HttpClient()
Dim response As HttpResponseMessage = Await client.GetAsync(yourUrl)
If response.IsSuccessStatusCode = True Then
Return Await response.Content.ReadAsAsync<YourData>()
End If
End Using
End Function
End Class
Then in the view you don't need BackgroundWorker
Class MyCustomWindow
Public Async Sub MyShow(a() as Integer) As Task
Me.Show() ' Has a "Loading..." TextBlock
Dim service As New DataService()
Dim data As YourData = Await service.GetDataAsync(a)
UpdateControlsWithData(data)
End Sub
Private Sub UpdateControlsWithData(data As YourData)
' Update controls with received data
End Sub
End Class
For what it's worth, here are a few examples which do 9 x 500ms of data operations, then simple UI operation.
The first example runs on a background thread but the main loop runs in sequence. The messagebox at the end shows that it takes around 4500 ms because of the 500 ms thread sleep is run sequentially. Notice how the DoWork method has nothing to do with the UI. It uses two threads: the UI thread and one background worker. Since it's not doing work on the UI, the form is responsive while the background worker is working.
Private bw_single As New BackgroundWorker()
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
AddHandler bw_single.DoWork, AddressOf bw_single_DoWork
AddHandler bw_single.RunWorkerCompleted, AddressOf bw_single_Complete
bw_single.RunWorkerAsync()
End Sub
Private Sub bw_single_DoWork(sender As Object, e As DoWorkEventArgs)
' runs on background thread
Dim data As New List(Of Integer)()
Dim sw As New Stopwatch
sw.Start()
For i As Integer = 1 To 9
' simulate downloading data, etc.
Threading.Thread.Sleep(500)
data.Add(i)
Next
sw.Stop()
e.Result = New Result(data, sw.ElapsedMilliseconds)
End Sub
Private Sub bw_single_Complete(sender As Object, e As RunWorkerCompletedEventArgs)
RemoveHandler bw_single.DoWork, AddressOf bw_single_DoWork
RemoveHandler bw_single.RunWorkerCompleted, AddressOf bw_single_Complete
' runs on UI thread
Dim res = CType(e.Result, Result)
Me.DataGridView1.DataSource = res.Data
MessageBox.Show(
String.Format("Performed on bw (single), took {0} ms, data: {1}",
res.Elapsed, String.Join(", ", res.Data)))
End Sub
(This is the class which holds the result of the background worker)
Private Class Result
Public Property Data As IEnumerable(Of Integer)
Public Property Elapsed As Long
Public Sub New(data As IEnumerable(Of Integer), elapsed As Long)
Me.Data = data
Me.Elapsed = elapsed
End Sub
End Class
The second example runs on a background thread but the main loop runs in parallel. The messagebox at the end shows that it takes around 1000 ms ... why? Because my machine like yours has 8 logical cores but we are sleeping 9 times. So at least one core is doing two sleeps and this will gate the entire operation. Again, there is one thread for the UI, one for the background worker, but for the parallel loop, the OS will allocate CPU time from the remaining cores to each additional thread. The UI is responsive and it takes a fraction of the time of the first example to do the same thing
Private bw_multi As New BackgroundWorker()
Private Sub Button2_Click(sender As Object, e As EventArgs) Handles Button2.Click
AddHandler bw_multi.DoWork, AddressOf bw_multi_DoWork
AddHandler bw_multi.RunWorkerCompleted, AddressOf bw_multi_Complete
bw_multi.RunWorkerAsync()
End Sub
Private Sub bw_multi_DoWork(sender As Object, e As DoWorkEventArgs)
' runs on background thread
Dim data As New ConcurrentBag(Of Integer)()
Dim sw As New Stopwatch
sw.Start()
Parallel.For(1, 9,
Sub(i)
data.Add(i)
Threading.Thread.Sleep(500)
End Sub)
sw.Stop()
e.Result = New Result(data, sw.ElapsedMilliseconds)
End Sub
Private Sub bw_multi_Complete(sender As Object, e As RunWorkerCompletedEventArgs)
RemoveHandler bw_multi.DoWork, AddressOf bw_multi_DoWork
RemoveHandler bw_multi.RunWorkerCompleted, AddressOf bw_multi_Complete
' runs on UI thread
Dim res = CType(e.Result, Result)
Me.DataGridView1.DataSource = res.Data
MessageBox.Show(
String.Format("Performed on bw (multi), took {0} ms, data: {1}",
res.Elapsed, String.Join(", ", res.Data)))
End Sub
Since the above two examples utilize background workers to do their work, they will not freeze the UI thread. The only code running on the UI is in the button click handlers and the RunWorkerCompleted handler.
Lastly, this example uses only a single UI thread. It will freeze the UI while it's running for 4500 seconds. Just so you know what to avoid...
Private Sub Button3_Click(sender As Object, e As EventArgs) Handles Button3.Click
Dim data As New List(Of Integer)()
Dim sw As New Stopwatch
sw.Start()
For i As Integer = 1 To 9
' simulate downloading data, etc.
Threading.Thread.Sleep(500)
data.Add(i)
Next
sw.Stop()
Dim res = New Result(data, sw.ElapsedMilliseconds)
Me.DataGridView1.DataSource = res.Data
MessageBox.Show(
String.Format("Performed on bw (single), took {0} ms, data: {1}",
res.Elapsed, String.Join(", ", res.Data)))
End Sub
Summarily, you should figure out how to separate the data layer from the UI. See separation of concerns and the SO question, Why is good UI design so hard for some Developers?, and this one What UI design principles like “separation of concerns” can I use to convince developers that the UI needs fixing?

DispatcherObject cast woes and Async / ObservableCollection issues in WPF

The code below pulls out a bunch of records from an Access 2010 database; hence rolling my own connector bits. I've succeeded in doing the observablecollection and made it all bind up with nice drag and drop data sources, from my own objects. However, like a daft person, I want to do this Asynchronously. Yet, I've got a small cast monster problem, and I don't know what to feed it! Can anyone advise me - I've tried a lot of reading around, but the concepts are just a little too many at once on a Friday afternoon and I'm struggling to make any real headway.
The line I'm having trouble with is:
Dim dispatcherObject As DispatcherObject = CType (handler.Target, DispatcherObject )
The exception is:
Unable to cast object of type '_Closure$__2[SomeRecord_Viewer.SomeRecord]' to type 'System.Windows.Threading.DispatcherObject'.
I've managed to make a WPF listbox populate via the code below, however only by commenting out a part of the ObservableCollectionEx class. This causes synchronisation problems and a crash after a few hundred records are entered.
Class that builds the threaded list of entities - in this case, an ObservableCollectionEx(Of SomeRecord):
Class SomeRecordSet
Inherits ObservableCollectionEx( Of SomeRecord)
Private Shared Property _SomeRecordList As New ObservableCollectionEx(Of SomeRecord )
Public Shared ReadOnly Property SomeRecordList As ObservableCollectionEx(Of SomeRecord )
Get
If _SomeRecordList.Count = 0 Then BuildSomeRecordListAsync()
Return _SomeRecordList
End Get
End Property
Public Shared ReadOnly Property ReturnSingleSomeRecord(id As Integer) As SomeRecord
Get
Return ( From SomeRecord In _SomeRecordList Where SomeRecord.id = id Select SomeRecord).First()
End Get
End Property
Private Shared Async Sub BuildSomeRecordListAsync()
Await Task.Run( Sub() BuildSomeRecordList())
Return
End Sub
Private Shared Sub BuildSomeRecordList()
Db.newcmd( "Select * from RecordList ")
While Db.read
Dim SomeRecord As New SomeRecord
With SomeRecord
.id = Db.dbint( "ID")
.type = Db.dbin( "type")
End With
_SomeRecordList.Add(SomeRecord)
End While
End Sub`
Partial code for the SomeRecord class:
Class SomeRecord
Implements INotifyPropertyChanged
Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged
Private Sub NotifyPropertyChanged( ByVal info As String)
RaiseEvent PropertyChanged(Me , New PropertyChangedEventArgs (info))
End Sub
...'lots of simple properties.
End Class
The threaded collection class code - translated from another online source.
'I use PostSharp for try catch stuff.
`
Public Class ObservableCollectionEx (Of T )
Inherits ObservableCollection( Of T)
' Override the event so this class can access it
Public Shadows Event CollectionChanged As System.Collections.Specialized.NotifyCollectionChangedEventHandler
Protected Overrides Sub OnCollectionChanged( ByVal e As System.Collections.Specialized.NotifyCollectionChangedEventArgs )
Using BlockReentrancy()
Dim eventHandler As System.Collections.Specialized.NotifyCollectionChangedEventHandler = Sub () RaiseEvent CollectionChanged(Me , e)
If (eventHandler Is Nothing) Then Return
Dim delegates() As [Delegate] = eventHandler.GetInvocationList
*******If I comment this out I can populate the Listbox via a CollectionView, however it dies with issues to do with the list not staying synchronised :).
'Walk thru invocation list
For Each handler As System.Collections.Specialized.NotifyCollectionChangedEventHandler In delegates
Dim dispatcherObject As DispatcherObject = CType (handler.Target, DispatcherObject)
' If the subscriber is a DispatcherObject and different thread
If (( Not (dispatcherObject) Is Nothing) AndAlso (dispatcherObject.CheckAccess = False )) Then
' Invoke handler in the target dispatcher's thread
dispatcherObject.Dispatcher.Invoke(DispatcherPriority .DataBind, handler, Me, e)
Else
handler( Me, e)
End If
Next
*******End of stuff I comment out to get working partially***
End Using
End Sub
End Class
From what I can see, you have two problems.
You're assigning the local variable eventHandler to an anonymous method, rather than the actual event handler. It should be:
Dim eventHandler As NotifyCollectionChangedEventHandler = CollectionChangedEvent
NB: You need to use CollectionChangedEvent in VB, not CollectionChanged.
You're using CType to cast the target to a DispatcherObject, which won't work if the target isn't a DispatcherObject. Use TryCast instead:
Dim dispatcherObject As DispatcherObject = TryCast(handler.Target, DispatcherObject)
You can also tidy up the test on the next line by using IsNot:
If dispatcherObject IsNot Nothing AndAlso Not dispatcherObject.CheckAccess Then
WARNING - The code below acts differently to the C# version. The key difference seems to be that in VB you can't Override an Event (Why on earth not?) yet in C# you can.
The result is the Handler is Nothing in VB but not in C# :(.
So the syntax builds without error but the VB version doesn't ever do anything.
Redone with the updated answer in VB. Thank you!
Note I cannot make this work with Entity Framework, yet. But I think that a me and EF issue, not the collection.
The code itself is here for anyone interested. My list DOES populate perfectly fine now. However, I would take this answer of mine with a small pinch of salt until I update saying how I've extensively tested perhaps :)
However the omens are good - here is the original C# author's site: Original Site
Public Class ObservableCollectionEx(Of T)
Inherits ObservableCollection(Of T)
'Override the event so this class can access it
Public Shadows Event CollectionChanged As NotifyCollectionChangedEventHandler
Protected Overrides Sub OnCollectionChanged(ByVal e As NotifyCollectionChangedEventArgs)
Using BlockReentrancy()
Dim eventHandler As System.Collections.Specialized.NotifyCollectionChangedEventHandler = CollectionChangedEvent
If eventHandler Is Nothing Then
Return
End If
Dim delegates() As [Delegate] = CollectionChangedEvent.GetInvocationList
'Walk thru invocation list
For Each handler As NotifyCollectionChangedEventHandler In delegates
Dim dispatcherObject As DispatcherObject = TryCast(handler.Target, DispatcherObject)
' If the subscriber is a DispatcherObject and different thread
If dispatcherObject IsNot Nothing AndAlso Not dispatcherObject.CheckAccess Then
' Invoke handler in the target dispatcher's thread
dispatcherObject.Dispatcher.Invoke(DispatcherPriority.DataBind, handler, Me, e)
Else
handler(Me, e)
End If
Next
End Using
End Sub
End Class

The calling thread cannot access this object because a different thread owns it and ApartmentState.STA

at first when i started the thread which calls the following function DrawDTSRanksChart() it gave me an error "The calling thread must be STA, because many UI components require this"
after adding the follwing line to the thread call
UpdateThread.SetApartmentState(ApartmentState.STA)
"The calling thread cannot access this object because a different thread owns it"
the commented line throws both exception
Public Update Thread As System.Threading.Thread
Private Sub DrawDTSRanksChart()
Dim DTSRankingDiagram As New XYDiagram2D
Dim series1 As New AreaFullStackedSeries2D
ChartControl1.Diagram = DTSRankingDiagram 'this line throws the exption
DTSRankingDiagram.Series.Add(series1)
series1.Points.Add(New SeriesPoint("Ranked 1.", DTSr1counter))
series1.Points.Add(New SeriesPoint("Ranked 2.", DTSr2counter))
series1.Points.Add(New SeriesPoint("Ranked 3.", DTSr3counter))
series1.Points.Add(New SeriesPoint("Ranked >3.", DTSrm3counter))
End Sub
Private Sub save_Clicked(ByVal sender As System.Object, ByVal e As System.Windows.RoutedEventArgs) Handles NormalUpdate.ItemClick
UpdateThread = New System.Threading.Thread(AddressOf update)
UpdateThread.SetApartmentState(ApartmentState.STA)
UpdateThread.Start()
End Sub
i just need a background worker so my UI doesnt hang
Solution
Dim MyDispatcher = Windows.Threading.Dispatcher.CurrentDispatcher
Private Delegate Sub UpdateUIDelegate()
and call the function as
MyDispatcher.BeginInvoke(DispatcherPriority.Background, New UpdateUIDelegate(AddressOf DrawDTSRanksChart))

Closing form from different thread happening too soon, causing exception

I've got the following situation,
Private Sub MyButton_Click(sender as Object, args as EventArgs) Handles MyButton.Click
Me.pleaseWaitFrm = New PleaseWaitForm()
' Fire up new thread to do some work in (AddressOf DoMyWork)
Me.pleaseWaitFrm.ShowDialog()
End Sub
Private Sub DoMyWork()
Dim log = Me.DoTheActualWork()
Me.pleaseWaitFrm.Close()
Using logFrm as New LogViewer(log)
logFrm.ShowDialog()
End Using
End Sub
If the DoTheActualWork() call exits fast enough, the Me.pleaseWaitFrm.Close() call is happening during the Me.pleaseWaitFrm.ShowDialog() call. The result, no surprise, is this exception:
An unhandled exception of type 'System.InvalidOperationException' occurred in System.Windows.Forms.dll
Additional information: Value Close() cannot be called while doing CreateHandle().
Obviously this is the old "you cannot .Close() a WinForm while it's in the process of loading" problem. But what isn't obvious to me is how best to prevent that from happening in this case? How does one safely and reliably delay the .Close() until it is safe to do so in this situation?
Me.pleaseWaitFrm.Close()
That's an illegal call, you are not allowed to close a form from another thread. Not sure how you got away with it, that should raise an IllegalOperationException when you run with a debugger. Review your code and delete any assignment to the Control.CheckForIllegalCrossThreadCalls property.
This exception you're getting is a clear side-effect of this. You must use Control.Invoke() to get the dialog closed. This automatically solves your problem, the delegate target cannot execute until the dialog is loaded.
Leverage the BackgroundWorker class, it makes this easy. You can close the dialog in a RunWorkerCompleted event handler.
Why not set a "ShouldClose" flag which can be checked when it's safe to close the form - and close it if required?
With regards to a code example, I'd implement it slightly differently but let me know if this breaks any other requirements and we can modify it...
''in PleaseWaitForm:
Public Property ShouldClose as boolean = false
Private Sub frmSplash_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
Me.Close()
End Sub
''Your posted code
Private Sub MyButton_Click(sender as Object, args as EventArgs) Handles MyButton.Click
Me.pleaseWaitFrm = New PleaseWaitForm()
'' Fire up new thread to do some work in (AddressOf DoMyWork)
Me.pleaseWaitFrm.ShowDialog()
End Sub
Private Sub DoMyWork()
Dim log = Me.DoTheActualWork()
Me.pleaseWaitFrm.ShouldClose = True
If Me.pleaseWaitFrm.Created Then
Me.pleaseWaitFrm.Created.Close
End If
Using logFrm as New LogViewer(log)
logFrm.ShowDialog()
End Using
End Sub
In short, if we Can close the form, we do - otherwise, set a flag and the form will do it when it finishes loading.
I tested and didn't get any issues calling Me.Close inside the frmSplash.Load() but if you do encounter any problems, you can make it cast-iron by having a worked on frmSplash which checks the value rather than use the Load event
Edit: Spotted and fixed a bug

Resources