Events and exceptions in 3-layer architecture - vb.net

I have another question considering events and exceptions
A small introduction to my project: I'm working in a 3 layer architecture, so I have a GUI(form), BLL(Business logic layer) and a DAL(data acces layer).
So my GUI is linked to a BLL which is connected to several DAL classes. For example my BLL has a class clsCSV (which reads and writes CSV files)
I now want to raise events and/or catch exceptions from this class to inform the user and log exceptions. There are 3 events: "ErrorLoad", "ErrorWrite", "Ready"
Public Class clsCSV
Public Sub New()
End Sub
Public Function Load(sFilePath As String)
Dim oFileHelper As New DelimitedFileEngine(Of clsItem)
Dim oList As New List(Of clsItem)
' Load a CSV file
Try
oList = oFileHelper.ReadFileAsList(sFilePath)
Catch ex As Exception
'RaiseEvent ErrorLoad("")
clsErrorLog.Log(ex)
Finally
End Try
' If the list is empty return nothing
If Not IsNothing(oList) Then
Return oList
Else
Return Nothing
End If
End Function
Public Sub Write(sFilePath As String, oList As List(Of clsTranslatedItem))
Dim oFileHelper As New DelimitedFileEngine(Of clsTranslatedItem)
oFileHelper.WriteFile(sFilePath, oList)
RaiseEvent Ready()
End Sub
Public Event ErrorLoad(ByVal sender As Object, ByVal e As EventArgs)
Public Event ErrorWrite(sMessage As String)
Public Event Ready()
End Class
There is a try-catch in my public sub load which catches any exception and logs it to a text file by calling clsErrorLog.log(ex) which then calls this class and logs the error:
Public Shared Sub Log(ex As Exception)
Try
Dim oBestand As New System.IO.StreamWriter(My.Computer.FileSystem.SpecialDirectories.Desktop & "\ErrorLog.txt", True)
If Not IsNothing(oBestand) Then
oBestand.WriteLine("")
oBestand.WriteLine(My.Computer.Clock.LocalTime.ToString)
oBestand.WriteLine(ex.Message)
oBestand.WriteLine(ex.StackTrace)
oBestand.WriteLine("_________________________________________")
oBestand.Close()
End If
Catch exc As Exception
End Try
End Sub
This works fine, but now I want to inform the user that an error occured, so I was thinking to raise an event called "ErrorLoad" (in clsCSV) and catch this in my BLL. But what can I do then? Add a sub that handles this event, which then raises an event in the BLL, caught by the GUI? or is this a long way to this?
I also want to do this for the other events offcourse. But the whole event system in the layered architecture is a bit confusing.
Anyone who can enlighten me?
Thanks in advance!

Without knowing the implementation details of your UI, I can only give you some general advice.
You need some kind of two directional communication between your GUI and your BLL. For example in WPF this is usually done using INotifyPropertyChanged interface and the event it defines.
So the BLL raises a event and the GUI is listening. The basically only has the task of informing the GUI that it has to check a specific property or function or anything like this from the BLL. The rest is up to the GUI to do so and inform the user.
I wouldn't do that using exceptions. These are there to handle the error path of specific functions. This path is done once your BLL noticed the error and prepared it for the UI handling.

Related

how to pass commandlinearguments from to a Running Application [duplicate]

I have already implemented context menu to appear when a user right-clicks a file in windows explorer using Registry. The file address will be passed to the application as command lines. Parsing it is no problem.
How can I implement that is similar to "Add To Windows Media Player Playlist"? It does not open another instance of the app but works on the same open window and adds it to a list?
There are 2 ways to do this depending on how your app starts.
Method 1: Using VB App Framework and a MainForm
This is the easiest because you mainly just need to add some code for an Application event. First, add a method to your main form to receive new arguments from subsequent instances of your app:
Public Class MyMainForm ' note the class name of the form
...
Public Sub NewArgumentsReceived(args As String())
' e.g. add them to a list box
If args.Length > 0 Then
lbArgs.Items.AddRange(args)
End If
End Sub
Next:
Open Project Properties
Check the "Make Single Instance" option
At the bottom, click View Application Events
This will open a new code window like any other; Select MyApplication Events in the left drop down; and StartupNextInstance in the right one.
Here, we find the main form and send the command line arguments to the method we created:
Private Sub MyApplication_StartupNextInstance(sender As Object,
e As ApplicationServices.StartupNextInstanceEventArgs) _
Handles Me.StartupNextInstance
Dim f = Application.MainForm
' use YOUR actual form class name:
If f.GetType Is GetType(MyMainForm) Then
CType(f, MyMainForm).NewArgumentsReceived(e.CommandLine.ToArray)
End If
End Sub
Note: Do not try to fish the main form out of Application.OpenForms. A few times I've had it fail to find an open form in the collection, so I have quit relying on it. Application.MainForm is also simpler.
That's it - when a new instance runs, its command line args should be passed to the form and displayed in the listbox (or processed however your method sees fit).
Method 2: Starting From Sub Main
This is more complicated because starting your app from a Sub Main means that the VB Application Framework is not used, which provides the StartupNextInstance event. The solution is to subclass WindowsFormsApplicationBase to provide the functionality needed.
First, give your main form a meaningful name and add something like the NewArgumentsReceived(args As String()) as above.
For those not aware, here is how to start your app from Sub Main():
Add a module named 'Program' to your app
Add a Public Sub Main() to it.
Go to Project -> Properties -> Application
Uncheck Enable Application Framework
Select your new "Sub Main" as the Startup Object
The module can actually be named anything, Program is the convention VS uses for C# apps. The code for Sub Main will be later after we create the class. Much of the following originated from an old MSDN article or blog or something.
Imports Microsoft.VisualBasic.ApplicationServices
Imports System.Collections.ObjectModel
Public Class SingleInstanceApp
' this is My.Application
Inherits WindowsFormsApplicationBase
Public Sub New(mode As AuthenticationMode)
MyBase.New(mode)
InitializeApp()
End Sub
Public Sub New()
InitializeApp()
End Sub
' standard startup procedures we want to implement
Protected Overridable Sub InitializeApp()
Me.IsSingleInstance = True
Me.EnableVisualStyles = True
End Sub
' ie Application.Run(frm):
Public Overloads Sub Run(frm As Form)
' set mainform to be used as message pump
Me.MainForm = frm
' pass the commandline
Me.Run(Me.CommandLineArgs)
End Sub
Private Overloads Sub Run(args As ReadOnlyCollection(Of String))
' convert RO collection to simple array
' these will be handled by Sub Main for the First instance
' and in the StartupNextInstance handler for the others
Me.Run(myArgs.ToArray)
End Sub
' optional: save settings on exit
Protected Overrides Sub OnShutdown()
If My.Settings.Properties.Count > 0 Then
My.Settings.Save()
End If
MyBase.OnShutdown()
End Sub
End Class
Note that the three main things the App Framework can do for us ("Enable XP Styles", "Make Single Instance" and "Save Settings on Exit") are all accounted for. Now, some modifications to Sub Main:
Imports Microsoft.VisualBasic.ApplicationServices
Imports System.Collections.ObjectModel
Module Program
' this app's main form
Friend myForm As MyMainForm
Public Sub Main(args As String())
' create app object hardwired to SingleInstance
Dim app As New SingleInstanceApp()
' add a handler for when a second instance tries to start
' (magic happens there)
AddHandler app.StartupNextInstance, AddressOf StartupNextInstance
myForm = New MyMainForm
' process command line args here for the first instance
' calling the method you added to the form:
myForm.NewArgumentsReceived(args)
' start app
app.Run(myForm)
End Sub
' This is invoked when subsequent instances try to start.
' grab and process their command line
Private Sub StartupNextInstance(sender As Object,
e As StartupNextInstanceEventArgs)
' ToDo: Process the command line provided in e.CommandLine.
myForm.NewArgumentsReceived(e.CommandLine.ToArray)
End Sub
End Module
The SingleInstanceApp class can be reused with any Sub Main style app, and the code in that method is mainly a copy-paste boilerplate affair except for perhaps the form reference and actual name of the NewArgumentsReceived method.
Testing
Compile the app, then using a command window, send some commandline arguments to the app. I used:
C:\Temp>singleinstance "First Inst" apple bats cats
This starts the app as normal, with the arguments shown. Then:
C:\Temp>singleinstance "Next Inst" ziggy zoey zacky
C:\Temp>singleinstance "Last Inst" 111 222 3333
It doesnt matter which approach you use - they both work the same. The Result:
Note that depending on security settings, your firewall may request permission for apps using either method to connect to other computers. This is a result of how an instance sends or listens for the arguments from others. At least with mine, I can deny permission to connect and everything still works fine.
#Plutonix solution is quite efficient and elegant.
However if you program goes through multiple Forms i.e. if the Main Form can change during program execution, for example if you have a login form and then a main form, or a sequence of non-modal forms, Application.MainForm won't always be the same form and may not be known beforehand (hard coded).
Plutonix code assumes it is known and hard codes it.
In this case, you may want to be able to receive the NewArguments at all times, in whichever form is active at the time in your application.
There are 2 solutions to extend Plutonix solution:
1) Repeatedly force Application.MainForm to a specific form in code (I haven't tested this but Application.MainForm is Read/Write so it could work).
2) The most elegant is to implement an Interface on all forms that can possibly become the MainForm:
Create the Basic interface:
Public Interface INewArgumentsReceived
Sub NewArgumentsReceived(args As String())
End Interface
Modify #Plutonix code for MyApplication_StartupNextInstance to:
Private Sub MyApplication_StartupNextInstance(sender As Object, e As ApplicationServices.StartupNextInstanceEventArgs) Handles Me.StartupNextInstance
Dim f = Application.MainForm
If f.GetType.GetInterfaces.Contains(GetType(INewArgumentsReceived)) Then
CType(f, INewArgumentsReceived).NewArgumentsReceived(e.CommandLine.ToArray)
Else
MsgBox("The current program state can't receive new requests.",, vbExclamation)
End If
Now on all possible forms that can become the Main Form, implement the INewArgumentsReceived Interface:
Public Class FormA: Implements INewArgumentsReceived
Public Sub NewArgumentsReceived(args As String()) Implements INewArgumentsReceived.NewArgumentsReceived
MsgBox("Got new arguments")
End Sub
The other advantage of using the Interfaces is that we can check if the current Application.MainForm implements it and is able to receive it.
If the current Application.MainForm does not implement the Interface it fails gracefully with an informational message.

Update control on main form via Background Worker with method in another class VB.Net

I have been banging my head against the wall all day trying to figure this one out.
I am finishing up a program to simply delete files in specific temp folders. I have read that it is sometimes good practice to create separate classes for methods and variables. So I have created a separate class for a couple methods to delete files and folders in a specified directory. I am using a Background Worker in my Form1 class and am calling my deleteFiles() method from my WebFixProcesses class in the DoWork event in the Form1 class. I am using a Background Worker so that I can easily report progress back to a progress bar on my main form.
The files get deleted without an issue but I just can't get the label on my main form to reflect the current file being deleted. the label doesn't change in any way.
I know the formula is correct as I can get this working if the method is in the Form1 class. and I simply use:
Invoke(Sub()
lblStatus.Text = File.ToString
lblStatus.Refresh()
End Sub)
here is my method that I am calling from the WebFixProcesses class:
Public Shared Sub deleteFiles(ByVal fileLocation As String)
For Each file As String In Directory.GetFiles(fileLocation)
Try
fileDisplay.Add(file)
For i = 1 To fileDisplay.Count
file = fileDisplay(i)
Form1.BackgroundWorker1.ReportProgress(CInt(i / fileDisplay.Count) * 100)
Next
IO.File.Delete(file)
Form1.labelText(file)
Form1.labelRefresh()
Catch ex As Exception
MessageBox.Show(ex.Message)
End Try
Next
End Sub
labelText() and labelRefresh() are methods from my main form which are using delegates to try to pass information to the control:
Public Sub labelText(ByVal file As String)
If lblStatus.InvokeRequired Then
Dim del As New txtBoxDelegate(AddressOf labelText)
Me.Invoke(del, file)
Else
lblStatus.Text = file.ToString()
End If
End Sub
Public Sub labelRefresh()
If lblStatus.InvokeRequired Then
Dim del As New txtBoxRefDelegate(AddressOf labelRefresh)
Me.Invoke(del)
Else
lblStatus.Refresh()
End If
End Sub
If anyone can help me out to inform me what I may be doing wrong it would be immensely appreciated as my head is in a lot of pain from this. And maybe I am going at it all wrong, and just being stubborn keeping my methods in their own class. But any help would be awesome. Thanks guys!
What Hans wrote on the question comment is true: Form1 is a type, not an instance, but to make things easier to newbye programmes (coming from VB6), M$ did a "mix", allowing you to use the form name as the instance of the form in the main thread.
This however works only if you are on that thread.
If you reference Form1 from another thread, a new instance of Form1 is created.
To solve the issue, add this code to the form:
Private Shared _instance As Form1
Public ReadOnly Property Instance As Form1
Get
Return _instance
End Get
End Property
We will use this property to store the current instance of the form. To do so, add this line to the Load event:
Private Sub Form1_Load(sender As Object, e As System.EventArgs) Handles Me.Load
_instance = Me
'other code here
End Sub
Now, from every class, in any thread, if you use
Form1.Instance
...you get the actual form. Now you can Invoke, even from the same form:
Me.instance.Invoke(Sub()
Me.lblStatus.Text = "Hello World"
End Sub)

How to make a loader in a separate thread?

I have a main form wich is expected to perfom some long operations. In parallel, I'm trying to display the percentage of the executed actions.
So I created a second form like this:
Private Delegate Sub DoubleFunction(ByVal D as Double)
Private Delegate Sub EmptyFunction()
Public Class LoaderClass
Inherits Form
'Some properties here
Public Sub DisplayPercentage(Value as Double)
If Me.InvokeRequired then
dim TempFunction as New DoubleFunction(addressof DisplayPercentage)
Me.Invoke(TempFunction, Value)
Else
Me.PercentageLabel.text = Value
End if
End sub
Public Sub CloseForm()
If Me.InvokeRequired Then
Dim CloseFunction As New EmptyFunction(AddressOf CloseForm)
Me.Invoke(CloseFunction)
Else
Me.Close()
End If
FormClosed = True
End Sub
End class
My main sub, the one which is expected to perform the long operations is in another form as follows:
Private Sub InitApplication
Dim Loader as new LoaderClass
Dim LoaderThread as new thread(Sub()
Loader.ShowDialog()
End sub)
LoaderThread.start()
Loader.DisplayPercentage(1/10)
LoadLocalConfiguration()
Loader.DisplayPercentage(2/10)
ConnectToDataBase()
Loader.DisplayPercentage(3/10)
LoadInterfaceObjects()
Loader.DisplayPercentage(4/10)
LoadClients()
...
Loader.CloseForm()
End sub
The code works almost 95% of the time but sometimes I'm getting a thread exception somewhere in the sub DisplayPercentage. I change absolutely nothing, I just hit the start button again and the debugger continues the execution without any problem.
The exception I get is: Cross-thread operation not valid: Control 'LoaderClass' accessed from a thread other than the thread it was created on event though I'm using : if InvokeRequired
Does anyone know what is wrong with that code please ?
Thank you.
This is a standard threading bug, called a "race condition". The fundamental problem with your code is that the InvokeRequired property can only be accurate after the native window for the dialog is created. The problem is that you don't wait for that. The thread you started needs time to create the dialog. It blows up when InvokeRequired still returns false but a fraction of a second later the window is created and Invoke() now objects loudly against being called on a worker thread.
This requires interlocking, you must use an AutoResetEvent. Call its Set() method in the Load event handler for the dialog. Call its WaitOne() method in InitApplication().
This is not the only problem with this code. Your dialog also doesn't have a Z-order relationship with the rest of the windows in your app. Non-zero odds that it will show behind another window.
And an especially nasty kind of problem caused by the SystemEvents class. Which needs to fire events on the UI thread. It doesn't know what thread is the UI thread, it guesses that the first one that subscribes an event is that UI thread. That turns out very poorly if that's your dialog when it uses, say, a ProgressBar. Which uses SystemEvents to know when to repaint itself. Your program will crash and burn long after the dialog is closed when one of the SystemEvents now is raised on the wrong thread.
Scared you enough? Don't do it. Only display UI on the UI thread, only execute slow non-UI code on worker threads.
Thank you for your proposal. How to do that please ? Where should I
add Invoke ?
Assuming you've opted to leave the "loading" code of the main form in the main UI thread (probably called from the Load() event), AND you've set LoaderClass() as the "Splash screen" in Project --> Properties...
Here is what LoaderClass() would look like:
Public Class LoaderClass
Private Delegate Sub DoubleFunction(ByVal D As Double)
Public Sub DisplayPercentage(Value As Double)
If Me.InvokeRequired Then
Dim TempFunction As New DoubleFunction(AddressOf DisplayPercentage)
Me.Invoke(TempFunction, Value)
Else
Me.PercentageLabel.text = Value
End If
End Sub
End Class
*This is the same as what you had but I moved the delegate into the class.
*Note that you do NOT need the CloseForm() method as the framework will automatically close your splash screen once the main form is completely loaded.
Now, over in the main form, you can grab the displayed instance of the splash screen with My.Application.SplashScreen and cast it back to LoaderClass(). Then simply call your DisplayPercentage() method at the appropriate times with appropriate values:
Public Class Form1
Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
InitApplication()
End Sub
Private Sub InitApplication()
Dim Loader As LoaderClass = DirectCast(My.Application.SplashScreen, LoaderClass)
Loader.DisplayPercentage(1 / 10)
LoadLocalConfiguration()
Loader.DisplayPercentage(2 / 10)
ConnectToDataBase()
Loader.DisplayPercentage(3 / 10)
LoadInterfaceObjects()
Loader.DisplayPercentage(4 / 10)
LoadClients()
' Loader.CloseForm() <-- This is no longer needed..."Loader" will be closed automatically!
End Sub
Private Sub LoadLocalConfiguration()
System.Threading.Thread.Sleep(1000) ' simulated "work"
End Sub
Private Sub ConnectToDataBase()
System.Threading.Thread.Sleep(1000) ' simulated "work"
End Sub
Private Sub LoadInterfaceObjects()
System.Threading.Thread.Sleep(1000) ' simulated "work"
End Sub
Private Sub LoadClients()
System.Threading.Thread.Sleep(1000) ' simulated "work"
End Sub
End Class
If all goes well, your splash screen should automatically display, update with progress, then automatically close when your main form has finished loading and displayed itself.
Me.Invoke(TempFunction, Value)
Should be:
Me.Invoke(TempFunction, new Object(){Value})
because the overload with parameters takes an array of parameters.
Value is on the stack of the function in the current thread. You need to allocate memory on the GC heap and copy the value to that memory so that it is available to the other thread even after the local stack has been destroyed.

Vb.Net - Invoke in Task

so basically I want my application to run a method that is quite cpu intensive and therewhile it should constantly display status information on a different form. To prevent this status form from freezing, I thought it would be a good idea to outsource the code into a new thread.
First I tried to use basic threadding and invoking the richtextbox control which should display status messages. - Problem is - I need to know when the Thread is finished to carry on with my main thread. Obviously I cannot simply run a loop in my main thread that keeps checking if the process is finished, cause that would make my GUI freeze, too.
So I did a little bit research and found out about Tasks.
That's how it looks:
Dim z as new complexProcessClass
Dim taskA = task.Factory.StartNew(Sub() z.start())
taskA.Wait()
If taskA.IsCompleted Then
MsgBox("finished")
End If
And whenever the process reports a status I use this:
Public Class complexProcessClass
dim statusWindow as statusForm
Public Sub start()
statusWindow = new statusForm
'complex code here
reportStatus("bla")
'complex code here
reportStatus("blabla")
'complex code here
End Sub
Private Delegate Sub UpdateTextHandler(ByVal Text As String)
Private Sub reportStatus(Byval s as String)
If z.RichTextBox1.InvokeRequired Then
Try
z.RichTextBox1.Invoke(New UpdateTextHandler(AddressOf xform.RichTextBox1.AppendText), s)
Catch ex As Exception
MsgBox(ex.ToString())
End Try
Else
z.RichTextBox1.AppendText(s)
End If
End Sub
But it just keeps freezing on the invoke call - no error message - nothing?!
Can anybody tell me the correct way to do this? - and please no backgroundworker solution ;)
thanks in advance
Take a look at the BackgroundWorker class. This post should get you started.
Another approach is to create delegates and call the thread asynchronously and implement an update function to catch when the work is done.
Create a class with the work code as a function
At the top of the class create/add Delegate Function Handler
Inside of your form add a handler to the new class with a call to your class method.
Create a callbackhandler method to receive the status of the thread performing the export functionality.Have the callbackhandler call an update ui function that checks if the thread is running from the UI or is another thread. (Me.InvokeRequired checks this)
Inside of the Form btn click event call the method using
the targetHandler call.
The below code is what the form code would look like.
Public Class Form1
Private targetHandler As ClassName.Handler = AddressOf objNewClass.somework
Private callbackHandler As AsyncCallback _
= AddressOf MyCallbackMethod
Sub MyCallbackMethod(ByVal ar As IAsyncResult)
'*** this code fires at completion of each asynchronous method call
Try
Dim retval As Boolean = targetHandler.EndInvoke(ar)
If retval = True Then
Console.Write(retval)
End If
UpdateUI("Task complete")
Catch ex As Exception
Dim msg As String
msg = "Error: " & ex.Message
UpdateUI(msg)
End Try
End Sub
Sub UpdateUI(ByVal statusMessage As String)
If Me.InvokeRequired Then
Dim handler As New UpdateUIHandler(AddressOf UpdateUI_Impl)
Dim args() As Object = {statusMessage}
Me.BeginInvoke(handler, args)
Else
UpdateUI_Impl(statusMessage)
End If
End Sub
Delegate Sub UpdateUIHandler(ByVal statusMessage As String)
Sub UpdateUI_Impl(ByVal statusMessage As String)
Me.sbMain.Panels("Status").Text = statusMessage
End Sub
'Call to your worker thread
Private Sub btn_Click() Handles Button1.Click
Dim result As IAsyncResult =targetHandler.BeginInvoke(callbackHandler,Nothing)
End Sub
End Class
You have a deadlock situation where taskA.Wait() is blocking the UI thread, and the Invoke() call inside taskA is waiting for the UI thread to finish what it's doing. Which is waiting until it's done waiting. Which is never.
I'm not entirely sure, but try this:
Dim taskA = task.Factory.StartNew(Sub() z.start()).ConfigureAwait(False)

VB.Net Sending errors through mail when error is detected

I'm developing an application with an error log when something goes bad. It must send an e-mail with the error details so I can remotely fix and upload a new update with the fix.
I'm using Try Catch Exception but I have a lot of methods to include this option in.
Is there another way to do it without doing so much code?
Since exceptions bubble up to the application instance try using the Application.SetUnhandledExceptionMode Method.
From above MSDN Link:
It is often not feasible to catch all of the exceptions thrown by
Windows Forms. Using this method, you can instruct your application
whether it should catch all unhandled exceptions thrown by Windows
Forms components and continue operating, or whether it should expose
them to the user and halt execution.
Public Shared Sub Main()
' Add the event handler for handling UI thread exceptions to the event.
AddHandler Application.ThreadException, AddressOf Form1_UIThreadException
' Set the unhandled exception mode to force all Windows Forms errors to go through
' our handler.
Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException)
' Add the event handler for handling non-UI thread exceptions to the event.
AddHandler AppDomain.CurrentDomain.UnhandledException, AddressOf CurrentDomain_UnhandledException
' Runs the application.
Application.Run(New Form1()) '' This is your applications Main Form
End Sub
Private Shared Sub Form1_UIThreadException(ByVal sender As Object, ByVal t As ThreadExceptionEventArgs)
'Put Error Handling Code here see the MSDN article for an example implementation
End Sub
Private Shared Sub CurrentDomain_UnhandledException(ByVal sender As Object, _
ByVal e As UnhandledExceptionEventArgs)
''Put Error Handling Code here see the MSDN article for an example implementation
End Sub
Sorry, misunderstood your question. Try putting your logic in a method and just try to call that method in every try catch statement you have.
Example:
Public Shared Sub Method1()
Try
'Method logic here
Catch ex As Exception
EmailError(ex)
End Try
End Sub
Public Shared Sub EmailError(ex As Exception)
'your remote error email logic here
End Sub