UWP adding UI control to list in non-uit thread - vb.net

So as i'm learning more and more stuff of UWP and XAML i bumped into two issues, one is (i think) "navigation" related and the second a threading issue. What i'm trying to achieve is simple. I have two pages, one "home" and one "Settings". On the Home page i show the connected clients as Custom_Buttons. On the Settings page i can change some settings regarding the app and Connected Clients
Navigation Issue
On my MainPage is setup all my declarations and object classes i need. When i navigate to a page i pass me (that is the MainPage) through to the page i'm loading so i can use the properties and objects in the that i declared on the MainPage. Then when i load the page i use the page event OnNavigatedTo to handle the passed MainPage and do local stuf with it. When i switch often between the pages the app crashes and opens the page app.g.i.vb and point to the following code:
#If Debug AndAlso Not DISABLE_XAML_GENERATED_BREAK_ON_UNHANDLED_EXCEPTION Then
AddHandler Me.UnhandledException,
Sub(sender As Global.System.Object, unhandledExceptionArgs As Global.Windows.UI.Xaml.UnhandledExceptionEventArgs)
If Global.System.Diagnostics.Debugger.IsAttached Then
**Here--->>>** Global.System.Diagnostics.Debugger.Break()
End If
End Sub
#End If
And the navigation code:
Private Sub ListBox_SelectionChanged(sender As Object, e As SelectionChangedEventArgs)
If Home.IsSelected AndAlso Not ScenarioFrame.CurrentSourcePageType Is GetType(Home) Then
BackButton.Visibility = Visibility.Collapsed
ScenarioFrame.Navigate(GetType(Home), Me)
ElseIf Settings.IsSelected AndAlso Not ScenarioFrame.CurrentSourcePageType Is GetType(Settings) Then
BackButton.Visibility = Visibility.Visible
ScenarioFrame.Navigate(GetType(Settings), Me)
End If
End Sub
Threading Issue
On the MainPage I declare a class i wrote called TCP_Server. This class has a StreamSocketListener that uses the event ConnectionReceived to accept new incoming clients. I then simply create a new Object that represents a UI form of the client and pass it the StreamSocket that comes in the Event Args in the sub new. In this way each object can handles it's own Read and Write directly from the StreamSocket Then i add this new object to a ObservableCollection(Of Object) which is held in the TCP_Server Class. This list is bound to the ItemsSource of a Canvas that i use on the HomePage which is not my MainPage.
Protected Overrides Sub OnNavigatedTo(e As NavigationEventArgs)
MyBase.OnNavigatedTo(e)
If ButtonsList.ItemsSource = Nothing Then ButtonsList.ItemsSource = DirectCast(e.Parameter, MainPage).TCP_Server.Clients
End Sub
When i create this new object in the ConnectionReceived i get an error System.Exception: 'The application has called an interface that has been marshalled for another thread. (Exception from HRESULT: 0x8001010E (RPC_E_WRONG_THREAD)) '. It only works when i use the Dispatcher.RunAsync
Private Async Sub TCP_Listener_ConnectionReceived(sender As StreamSocketListener, args As StreamSocketListenerConnectionReceivedEventArgs) Handles TCP_Listener.ConnectionReceived
'Check if the client already excists or not.
Dim client As Client_Button = Clients.FirstOrDefault(Function(x) x.IPaddr = args.Socket.Information.RemoteAddress.ToString)
rootPage.NotifyUser("New Client connected! : [" & args.Socket.Information.RemoteAddress.ToString & "] Total Connected clients = " & Clients.Count, NotifyType.Message)
If client Is Nothing Then
Await CoreApplication.MainView.CoreWindow.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, Function()
'Create New object
Dim x As New Client_Button(args.Socket)
'Create new task that runs Async to process incomming data
Dim tsk As Task = Task.Run(Sub() x.ProcessClientAsync())
'Add to the task list so we can stop it later on
ClientTasks.Add(tsk)
'Add it to the Clients List so we can work with the objects
Clients.Add(x)
Return True
End Function)
Else
Await CoreApplication.MainView.CoreWindow.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, Function()
client = Nothing
Clients.Remove(client)
'Create New object
Dim x As New Client_Button(args.Socket)
'Create new task that runs Async to process incomming data
Dim tsk As Task = Task.Run(Sub() x.ProcessClientAsync())
'Add to the task list so we can stop it later on
ClientTasks.Add(tsk)
'Add it to the Clients List so we can work with the objects
Clients.Add(x)
Return True
End Function)
End If
End Sub

For "Navigation Issue" you described here, navigation between pages several times it will crash, please try to set NavigationCacheMode of the page to Required or Enabled as follows:
Public Sub New()
Me.InitializeComponent()
Me.NavigationCacheMode = NavigationCacheMode.Required
End Sub
Details please reference remarks of Page class. If you still have issues please provide the details about the "UnhandledException" .
For "Threading Issue", using Core​Dispatcher is the correct way and this is by design. ConnectionReceived triggered in a non-UI thread, but you invoked UI thread inside this event handle, so you need Dispatcher.RunAsync. More details you can reference this similar thread.

Related

VB.Net Visual basic Adding a custom event in the Webbrowser control that calls a routine in the main application

I'm trying to create and call a custom event in the webbrowser control and everything that I've tried to do causes one error or another when the webpage executes the code. What I'm doing is adding a button on each row of a table to facilitate removing that row. However, the master list of data is in the application. When the script in the web page executes, I need to update the master list in the application. My thoughts were to call a custom event that will be fired in my application where I can do everything that I need to do. I just can't make this work. here are more details of what I have right now. Here is the html code for a given row:
Dim M As String = "</TD><TD>"
RetStr.Append("<TR ID='" & Me.Manifest & "' name='" & Me.Manifest & "'>")
RetStr.Append("<TD>").Append(CompanyID).Append(M).Append(CompanyName).Append(M)
RetStr.Append(ContactName).Append(M).Append(Address1).Append(M).Append(Address2)
RetStr.Append(M).Append(City).Append(M).Append(State).Append(M)
RetStr.Append(Zip).Append(M).Append(Phone).Append("</TD>")
RetStr.Append("<TD><button onclick='deleteRow(""" & Me.Manifest & """)'>Remove</button></TD>")
Return Replace(RetStr.ToString(), "<TD></TD>", "<TD> </TD>")
Here is the code that is in the function:
Dim HTMLOut As New List(Of String)
HTMLOut.Add("<HEAD>")
HTMLOut.Add(" <SCRIPT language=""VBScript"">")
HTMLOut.Add(" Function deleteRow(rowid)")
HTMLOut.Add(" set row = document.getElementById(rowid)")
HTMLOut.Add(" row.parentNode.removeChild(row)")
HTMLOut.Add(" dispatchEvent(Row)")
HTMLOut.Add(" End Function")
HTMLOut.Add(" </SCRIPT>")
HTMLOut.Add("</HEAD>")
HTMLOut.Add("<BODY>")
HTMLOut.Add(" <TABLE border='1' style='font-size:12;' NAME='Table' ID='TABLE'>")
Here is the code that I have in the application:
Private Sub WB_DocumentCompleted(sender As Object, e As
WebBrowserDocumentCompletedEventArgs) Handles WB.DocumentCompleted
WB.Document.AttachEventHandler("UpdateList", New EventHandler(
Function(ByVal s As Object, ByVal k As EventArgs)
MsgBox("BOO")
Return True
End Function))
End Sub
Any help in any direction, even if it means I need to change how I'm doing all of this, is very welcomed! There is more code then this, it's stripped down to what is needed to convey what I'm doing. I know I'm missing something, I just can't figure out what it is. The end goal is to update the master list in the application hosting the web browser; ideas suggestions and comments are always welcome. As a side note, I'm using the web browser control because the final part of the process is to create a file and sftp it to the vender (the application will do this), and print the report. Thanks!
I figured this out. I needed to create a class object, with the comvisible attribute set and add this to the objectForScripting property of the web browser control.
Imports System.Runtime.InteropServices
<ComVisible(True)> Public Class WBClassCode
Public Sub UpdateStuff(ByVal Data)
'My code goes here... called from the web page.
MsgBox("boo")
End Sub
End Class

vb.net TextBox does not change

I'm not able to update the TextBox content from another module.
The TextBox is in a Form called frm_main and the EventHandler in another module called md_zeiss.
Init() is called by a button on frm_main.
Problem:
If I directly call Test() from frm_main it does change the text.
If called by the event, it does not change the text, but displays the correct MessageBox.
Code:
Module md_zeiss
Sub Init()
Dim fsw As New FileSystemWatcher
fsw.Path = "C:\Output"
fsw.Filter = "*.txt"
fsw.NotifyFilter = NotifyFilters.Attributes Or NotifyFilters.CreationTime Or NotifyFilters.DirectoryName _
Or NotifyFilters.FileName Or NotifyFilters.LastAccess Or NotifyFilters.LastWrite Or NotifyFilters.Security Or NotifyFilters.Size
fsw.EnableRaisingEvents = True
AddHandler fsw.Changed, AddressOf md_zeiss.Main
End Sub
Sub Main(sender As Object, e As IO.FileSystemEventArgs)
Do While IsLocked(e.FullPath) = True
Application.DoEvents()
Loop
Dim fs As New FileStream(e.FullPath, FileMode.Open, FileAccess.Read)
Dim sr As New StreamReader(fs, System.Text.Encoding.Default)
Dim textline As String = vbNullString
Dim nr As String
Dim gi As String
Dim le As String
Do Until sr.Peek = -1
textline = sr.ReadLine
Select Case True
Case InStr(textline, vbTab & "Ø MOLDING_NR_SIDE" & vbTab)
nr = ReadVal(textline, 5)
Case InStr(textline, vbTab & "LENGTH" & vbTab)
gi = ReadVal(textline, 5)
Case InStr(textline, vbTab & "Ø MOLDING_GI_SIDE" & vbTab)
le = ReadVal(textline, 5)
End Select
Loop
Test()
End Sub
Sub Test()
frm_Main.TextBox1.Text = "Test"
MsgBox(frm_Main.TextBox1.Text)
End Sub
The FileSystemWatcher raises it events on a secondary thread by default. Default instances of forms are thread-specific so if you display the default instance of a form on the UI thread and then try to access the default instance from the handler of a FileSystemWatcher event (or a method called from that handler) then you're actually referring to two different form objects.
The simplest option is to set the SynchronizingObject property of the FileSystemWatcher on the UI thread. You can assign a form or other control to that property and the FileSystemWatcher will then raise its events on the thread that owns that control, i.e. the UI thread. If you do go down that route, just be sure that your event handler is executed quickly. You don;t want to tie up the UI thread with long-running code, which is why a secondary thread is used by default.
Another option is to use the SynchronizationContext class in your module to allow you to marshal a method call to the UI thread. You would continue to have the FileSystemWatcher raise its events on a secondary thread and do the background work there, then call Send or Post on the SynchronizationContext to invoke a method on the UI thread, where using the default instance of that form would refer to the same instance as you have already displayed.
Basically though, the architecture there is bad. You should almost certainly be using a class rather than a module and the form can then keep a reference to an instance of that class. The class could then raise an appropriate event that the form could handle and then the form could update its own TextBox. If you are access the controls on a form outside that form then the code is inherently bad. Default form instances make doing that easier, which is one reason that seasoned developers generally don't like them. They make it easier for beginners to get up and running, but it also makes it easier for beginners to paint themselves into a corner when things get remotely complex.

Updating Variable in Multithreading in VB.NET

I've wrote a program which on startup loads the computer list from Active Directory. This takes about 10 seconds. If the user has started the program with a specific host as parameter, it should be usable immediately.
So to don't interrupt the user I want to load the computer list in a different thread. The problem is that it writes to a variable (the computer list) which is also used in the main thread.
You may think, I could simply use a temporary variable and when its done overwrite the main variable. But I have to keep existing data of the main variable.
'hosts list
Private Shared hosts As New SortedDictionary(Of String, HostEntry)
'Get all computers in Active Directory
'Will run in a extra thread
Private Delegate Sub GetADcomputersDelegate()
Private Sub GetADcomputers()
If Me.InvokeRequired Then
Me.Invoke(New GetADcomputersDelegate(AddressOf GetADcomputers), Nothing)
Else
lblStatusAD.Text = "Getting Computers..."
Try
Dim search As New DirectorySearcher(ActiveDirectory.Domain.GetCurrentDomain().GetDirectoryEntry(), "(objectClass=computer)")
For Each host As SearchResult In search.FindAll()
'AddHost creates a new HostEntry object and adds it to my "global" hosts variable
'It also checks if a host is already present in the list and only updates it.
AddHost(host.GetDirectoryEntry().Properties("cn").Value.ToLower(), host.GetDirectoryEntry().Properties("description").Value)
Next
Catch ex As Exception
Debug.WriteLine("GetADcomputers() Exception: " & ex.Message)
End Try
ThreadPool.SetMaxThreads(hosts.Count, hosts.Count)
Dim ah As String = activehost
'Fill my ListBox with the computers
lstHosts.DataSource = New BindingSource(hosts, Nothing)
'Select the computer that was selected before
UseHost(ah)
lblStatusAD.Text = ""
End If
End Sub
So when GetADcomputers() runs in its own thread, the main thread is also blocked. I guess because auf the hosts variable.
So what could I change to make the thread do it's work and after that apply the updated computer list without losing data of entries in old hosts list? And all this in a fast and efficient way.
That code is very wrong. If you call that method on a secondary thread then it immediately marshals a call back to the UI thread and does EVERYTHING on the UI thread. What you should be doing is executing all the background work on the secondary thread and then marshalling to the UI thread ONLY to update the UI.
Get rid of that If...Else block and just make the entire body of the method what's current ly in the Else block. Next, identify all the lines that specifically interact with the UI and remove each of those to their own method. You then add If...Else blocks to each of those methods so that only the code that actually touches the UI is executed on the UI thread.
Here's a start:
Private Sub GetADcomputers()
UpdateStatusADLabel("Getting Computers...")
Try
Dim search As New DirectorySearcher(ActiveDirectory.Domain.GetCurrentDomain().GetDirectoryEntry(), "(objectClass=computer)")
For Each host As SearchResult In search.FindAll()
'AddHost creates a new HostEntry object and adds it to my "global" hosts variable
'It also checks if a host is already present in the list and only updates it.
AddHost(host.GetDirectoryEntry().Properties("cn").Value.ToLower(), host.GetDirectoryEntry().Properties("description").Value)
Next
Catch ex As Exception
Debug.WriteLine("GetADcomputers() Exception: " & ex.Message)
End Try
ThreadPool.SetMaxThreads(hosts.Count, hosts.Count)
Dim ah As String = activehost
'Fill my ListBox with the computers
lstHosts.DataSource = New BindingSource(hosts, Nothing)
'Select the computer that was selected before
UseHost(ah)
lblStatusAD.Text = ""
End Sub
Private Sub UpdateStatusADLabel(text As String)
If lblStatusAD.InvokeRequired Then
lblStatusAD.Invoke(New Action(Of String)(AddressOf UpdateStatusADLabel), text)
Else
lblStatusAD.Text = text
End If
End Sub

How to update main form from thread utilizing a module creates new mainform?

So my use for a module is so I can use the same functions across different programs that I develope for my employer. They also want my module to be distributed amongst other programmers so they can use it as well. The programs need to know when there is a thread still running (SQL code is running (there are no problems with the sql side) and it needs to notify the user when all work is done but the user needs to be able to queue work)
From the main form I am using this code:
Dim thread1 As New System.Threading.Thread(AddressOf ModuleTesting.Testing)
thread1.SetApartmentState(Threading.ApartmentState.STA)
thread1.IsBackground = True
thread1.Name = "ModuleLabelCrossThreading"
thread1.Start()
This is the code for my module:
Public Sub Testing()
Form1.threadsrunning += 1
Form1.accesscontrolsmoduletesting()
'THIS IS WHERE THE PROGRAM DOES STUFF ILLUSTRATED BY SLEEPING'
System.Threading.Thread.Sleep(2000)
Form1.threadsrunning -= 1
Form1.accesscontrolsmoduletesting()
end sub
The code to access the controls on the main form is
Public Sub accesscontrolsmoduletesting()
If Me.InvokeRequired = True Then
Me.Invoke(New MethodInvoker(AddressOf accesscontrolsmoduletesting))
Else
If threadsrunning > 0 Then
Label4.Text = threadsrunning & " threads running"
Else
Label4.Text = "0 threads running"
End If
End If
End Sub
I already know the issue is the new thread is creating a new form. I tested this by showing the form and making it wait so it didnt immediately dispose itself and I seen the label was updated. How do I make this thread update the main form instead of just creating a new mainform and then disposing itself after the thread dies?
To reiterate on my Comment you need to get the actual Form1 that is being shown, you should change your Testing Method to accept a Parameter of Form1, then you can use a Parameterized Thread.Start to pass in the Calling Form. You are running into a feature that was left in place to placate Vb6 programmers transitioning to VB.net as this answer by Hans states. And you may find this Blog Post by John Mcllhinney an interesting read.
From Second Link(emphasize mine):
In order to access a form from a secondary thread you generally need to test its InvokeRequired property and then call its Invoke method. I said earlier that there is only ever one default instance of a form class. That’s not strictly true. In fact, default instances are thread-specific, so there is only ever one default instance per thread. As such, if you test the InvokeRequired property of the default instance you will always be accessing the default instance for the current thread, not the one that was displayed on the main thread.
So in response to above I would change your Module Test Method to:
Public Sub Testing(myForm As Form1)
myForm.threadsrunning += 1
myForm.accesscontrolsmoduletesting()
'THIS IS WHERE THE PROGRAM DOES STUFF ILLUSTRATED BY SLEEPING'
System.Threading.Thread.Sleep(2000)
myForm.threadsrunning -= 1
myForm.accesscontrolsmoduletesting()
End Sub
And I would change your Form1's Thread Start Code to look like this.
Dim thread1 As New System.Threading.Thread(AddressOf ModuleTesting.Testing)
thread1.SetApartmentState(Threading.ApartmentState.STA)
thread1.IsBackground = True
thread1.Name = "ModuleLabelCrossThreading"
thread1.Start(Me) 'Note the passing in the instance of the calling Form
After making these few changes your code will work

Weird behavior using the Observer pattern

Ok, so I have an application that reads another processes memory. I initially had multiple scanning threads for the various areas I needed to read. This was processor intensive so I decided to go with the observer pattern. All was well except that I am having a weird behavior.
Here is what is happening
I have 2 radars (overlay and mapped) Both have a watcher class that attaches to the memory scanner and is notified on a new list of mobs.
so I open radar 1 (mapped) it attaches it's watcher to the scanner and waits for mob list update notifications
Open radar 2 (overlay). same thing happens and another watcher is attached.
all is well and good so far
Now there are properies on the mobs in the list, one of which is IsFilteredOut. This property is set in the radar code after it receives the list.
Now the weird behavior is that no matter what I do, the second radar to be opened changes all the properties of the mobs in the list of both radars. It is as if I am passing the list by ref, but I am not. I actually create a new instance of the moblist class every time I pass the list.
Here is the notify code. As you can see I create a new instance of the moblist class each pass.
Private Sub NotifyMobListUpdated(ByVal Mobs As List(Of MobData))
If Mobs IsNot Nothing Then
For Each w As Watcher In _watchers
If w.Type And WatcherTypes.MobList = WatcherTypes.MobList OrElse w.Type And WatcherTypes.All = WatcherTypes.All Then
w.MobListUpdated(New MobList(Mobs))
End If
Next
End If
End Sub
This is where it is handled in the Watcher class
''' <summary>
''' IWatcher MoblistUpdated Implementation
''' </summary>
''' <param name="Mobs">The Updated mob list</param>
''' <remarks></remarks>
Public Sub MobListUpdated(ByVal Mobs As MobList) Implements IWatcher.MobListUpdated
Try
PostNewMobList(Mobs)
Catch ex As Exception
End Try
End Sub
Public Sub PostNewMobList(ByVal Mobs As MobList)
_sync.Post(New SendOrPostCallback(AddressOf OnNewMobList), Mobs)
End Sub
Private Sub OnNewMobList(ByVal state As Object)
Dim mobs As MobList = TryCast(state, MobList)
Try
If mobs IsNot Nothing Then
RaiseEvent NewMobList(mobs)
End If
Catch ex As Exception
End Try
End Sub
This error is driving me nuts and any help would be greatly appreciated.
Thanks
I actually create a new instance of the moblist class every time I pass the list.
Which only prevents the list from changing, not the list elements. You'd have to clone the element objects as well. I don't have a clue with radars and mobs do, you might want to consider using Send instead of Post.