Events

 <  Day Day Up  >  

For a FoxPro developer, events constitute one of the strangest things in Visual Basic .NET. I looked for the equivalent in FoxPro for the longest time, and I just couldn't find it. It turns out that there are two kinds of events in Visual Basic .NET, and they bear little resemblance to one another. FoxPro only has one kind of event. Visual Basic actually uses events to call methods in other objects; hence the confusion. I hope this part of the chapter will clear it up for you.

Events in Visual FoxPro

FoxPro's classes have properties, events, and methods. Properties are variables , and methods are functions and procedures. But you can't add events. When you subclass a base class, you can add properties and methods at will. You can also add your own code to the existing methods. For example, when you refresh a form, you can add code to SEEK() records in all related tables so that bound fields displayed on the form will be coordinated.

Events are different. Events happen. When they do, any code that you add to the event's code window is executed, in addition to the default behavior of the event. But events happen when they happen ”when you click, or double-click, or mouse over, or whatever. You don't add events in FoxPro.

Each of FoxPro's base classes comes with a generous but fixed list of events. You can't add to FoxPro's events; you're stuck with the list of events that each class was born with. In FoxPro, the name of an event implies what it handles. TextBox1.Click tells you which object the Click event responds to. There is no Handles clause at the end. It's not needed. You don't even see the mechanism that handles it. It's transparent.

To add code for an event, you pick the event from the Method Name/Members combo box of the code window navigation bar (at the upper-right side of the screen).

FoxPro's IDE supplies any necessary parameters. The code, in the form of an LPARAMETERS statement, is inserted when the code window opens. If you erase the LPARAMETERS line and leave the code window empty, the LPARAMETERS line will be put back in the next time you open the code window. For example, open a new MouseDown event code window and you get this:

 

 LPARAMETERS nButton, nShift, nXCoord, nYCoord 

NOTE

We'll look at the event argument parameter generated for this same event in Visual Basic by the .NET IDE in the next section. It does essentially the same thing.


There is no Event statement in FoxPro. (There are three new functions [ BindEvent , RaiseEvent and ReleaseEvents ] related to events in Visual FoxPro8, but it's difficult to come up with a reason to use them.) Looking at the list of events for a typical FoxPro control, I can't imagine why I'd want to add more events. That's why the Event statement in Visual Basic has always confused me. And why "raise" an event? Aren't events things that just happen, like "Click"?

NOTE

The closest things we have to events in FoxPro are the Assign and Access methods for form properties. They allow you to trap the instant when a value is assigned to or retrieved from a class property. But that's unrelated to the use of RaiseEvents in Visual Basic. It's interesting that in FoxPro we use the Visual Basic naming convention. If you add a MyPropName_Assign method to your class, the code in it will fire when the property is assigned a value.


This is at the heart of the reason that FoxPro developers can't easily understand Visual Basic events. The events that are declared for the purpose of calling them with RaiseEvent have nothing to do with the events that come with controls. That's not what RaiseEvent is used for in Visual Basic.

Events in Visual Basic .NET

One of the really huge differences between FoxPro and Visual Basic .NET is Visual Basic's use of events. FoxPro objects have events, and you can write code in the event snippets that will run when the event fires. It's transparent. In Visual Basic, event handling is not transparent. You get to watch.

If you create a Windows Form project and add a text box to the form, you'll find the following generated code in the form's codebehind:

 

 Friend WithEvents TextBox1 As System.Windows.Forms.TextBox ...     'TextBox1     '     Me.TextBox1.Location = New System.Drawing.Point(110, 54)     Me.TextBox1.Name = "TextBox1"     Me.TextBox1.TabIndex = 0     Me.TextBox1.Text = "TextBox1"     Me.Controls.Add(Me.TextBox1) 

If the WithEvents clause were not included, the program would ignore any of the control's events. Presumably, doing so causes the program to run faster; otherwise , why would the WithEvents clause exist? This allows us to add a Handles clause to a subroutine, regardless of its name, that "reacts" when the named event occurs. So what is the Visual Basic default ” without events ? Oddly enough, it is .

The IDE writes the code for you if you open the code window and select an object from the Objects drop-down list at the upper-left corner of the window, and then select an event from the drop-down list at the upper-right corner of the same window.

For example, the following code changes the text box back to black text on a white background when the cursor leaves the control:

 

 Private Sub TextBox1_LostFocus( _        ByVal sender As Object, _        ByVal e As System.EventArgs) _  Handles TextBox1.LostFocus  TextBox1.ForeColor = Color.Black     TextBox1.BackColor = Color.White End Sub 

Although a subroutine name that references the event name is generated when you add the code, the name is in fact functionally unrelated to what the routine does. You can change the name, and the program will work exactly the same. The Handles clause at the end is what causes the subroutine to execute when the event fires. Or, as they like to say in Redmond, when the event is raised .

RANT

<rant>That's actually part of the problem as well. The guys who developed Visual Basic took considerable liberties with the language. You throw and catch an error. (Why? Because they had seen the Mariners the night before, and because they could. There were no language police. The first little guy high on Coca-Cola at three a.m. gets to pick a name.) Similarly, you raise an event. Don't even get me started on persisting , depersisting, and consuming XML. By capriciously assigning terms that don't particularly reflect what's being done, they made the language that much harder to understand. But I digress </rant>


The two parameters in parentheses that follow the subroutine name are always ByVal sender As Object (a reference to the calling object) and an event argument whose type is based on what kind of parameters the called event needs. For example, if you add a MouseDown event routine, you get the following code:

 

 Private Sub Form1_MouseDown( _   ByVal sender As Object, _   ByVal e As System.Windows.Forms.MouseEventArgs) _  Handles MyBase.MouseDown  Debugger.Break   ' like SET STEP ON End Sub 

I've included a breakpoint so that you can type "e" into the Watch window and see what it is. It's a structure containing the values needed to respond to a mousedown event: X and Y coordinates, among others. So it's pretty much the same thing that FoxPro is doing with its generated LPARAMETERS statements. One more mystery solved .

If this were the only use for events in Visual Basic .NET, you could simply ignore them and let the IDE generate event-related code whenever it felt like it. The developers of FoxPro apparently felt that the confusion resulting from optionally exposing events in this way was not worth the bother. Generally, they were correct.

RaiseEvent

Events in Visual Basic are also used as a workaround for a characteristic of the Visual Basic compiler that prevents it from calling functions and procedures that aren't resolved at compile time. In particular, although you can instantiate an object based on a class and call the object's methods at will from within a form, you can't do the opposite in Visual Basic. For example, in FoxPro, you can't call THISFORM.Refresh from inside a class, like this:

 

 DEFINE CLASS Utilities AS CUSTOM PROCEDURE CoordinateTables THISFORM.Refresh        && in whatever form is active ENDPROC ENDDEFINE 

You can, however, use _Screen.ActiveForm.CoordinateTables . You can even use PEMSTATUS ( Object, MethodName, 5 ) to determine whether the method exists before calling it. So we have workarounds in FoxPro. But in Visual Basic they don't. (Well, there is reflection, but it's new. RaiseEvents has been around since Visual Basic4 at least, so that's how they do it.)

In Visual Basic you can't call a method in a form from inside an object without resorting to some trick. RaiseEvent is that trick. That's why it's so confusing. Here we are trying to think up events that might go beyond clicking or double-clicking. The use of RaiseEvent is a trick with mirrors to overcome a limitation in the compile process for Visual Basic. You don't need to try to invent events in FoxPro. There's no reason to. You can do what you need to do without them. In Visual Basic, you can't.

This is why FoxPro developers have a hard time understanding events in Visual Basic. All of FoxPro's events are always trappable; there is no need to use WithEvents to declare that the events for a control are subject to handling; they just are . And the second reason for using events in Visual Basic is unnecessary in Visual FoxPro!

In Visual Basic, Public Sub XXX Handles ObjectName.EventName can be used to respond to the line RaiseEvent EventName in a class. So a form can contain a subroutine that is called when the event named in the Handles clause of the subroutine fires (or is raised, as they like to say).

How to Declare an Event in a Class

To declare an event in a class, include the statement

 

 Public Event <Name> ( Parameters ) 

Instantiate your object:

 

 Dim oObj as New <ClassName> 

Then, within the code, invoke it using the RaiseEvent command:

 

 RaiseEvent <Name> ( Parameters ) 

This "raises" the event and passes any parameters to it. Finally, trap the event using the Handles clause of a subroutine in the form:

 

 Public sub xxx ( Parameters as string ) Handles oObj.EventName 

Using this mechanism, I've created a "hook" that lets me call any method in the form when something happens in the class. Note that I could simply call a method in the class using oObj.MethodName ( Parameters ) , as I do in the Update button code in Listing 1.5. But if I want to call form methods from the class, this is actually a pretty clean way to do it.

Here's an example, based on the "Visual Basic .NET How-To N-Tier Data Form App" sample included in the "101 Visual Basic .NET Samples" library available for free from the MSDN site. I can't say enough about this code library, which gives you good examples of how to do a huge variety of things in Visual Basic .NET. I've added a few properties so that it's more flexible and easier to configure.

Using Events in a Data Access Class

The benefits of separating the implementation of data access from the form are immediate. The data access class is external to the form, so you can change the data source without making any changes to the form. This allows (for example) using MSDE during development, then changing to SQL Server at a later date with minimal disruption. Listing 1.5 shows the example class.

Listing 1.5. A Data Access Class
 Option Strict On Imports System.Data.SqlClient Namespace DataAccessLayer Public Class DataAccess     Protected Const CONNECTION_ERROR_MSG As String = "Couldn't connect to SQL"     Protected Const SQL_CONNECTION_STRING As String = _                     "server=(local);database=Northwind;uid=sa;pwd=;"     Public da As SqlDataAdapter     Public ds As DataSet     Public _MainTable As String     Public _KeyField As String     Protected DidPreviouslyConnect As Boolean = False     Protected strConn As String = SQL_CONNECTION_STRING     Public Event ConnectionStatusChange(ByVal status As String)     Public Event ConnectionFailure(ByVal reason As String)     Public Event ConnectionCompleted(ByVal success As Boolean)     Public Property MainTable() As String         Get             Return _MainTable         End Get         Set(ByVal Value As String)             _MainTable = Value         End Set     End Property     Public Property KeyField() As String         Get             Return _KeyField         End Get         Set(ByVal Value As String)             _KeyField = Value         End Set     End Property     Public Function CreateDataSet() As DataSet         Dim ds As DataSet         If Not DidPreviouslyConnect Then             RaiseEvent ConnectionStatusChange("Connecting to SQL Server")         End If         Dim IsConnecting As Boolean = True         While IsConnecting             Try                 Dim scnnNW As New SqlConnection(strConn)                 Dim strSQL As String = "SELECT * FROM " + MainTable                 Dim scmd As New SqlCommand(strSQL, scnnNW)                 da = New SqlDataAdapter(scmd)                 Dim cb As New SqlCommandBuilder(da)                 ds = New DataSet                 da.Fill(ds, MainTable)                 IsConnecting = False                 DidPreviouslyConnect = True             Catch exp As Exception                 If strConn = SQL_CONNECTION_STRING Then                     RaiseEvent ConnectionFailure(CONNECTION_ERROR_MSG)                 End If             End Try         End While         RaiseEvent ConnectionCompleted(True)         da.Fill(ds, MainTable)         Return ds     End Function     Public Sub UpdateDataSet(ByVal inDS As DataSet)         If inDS Is Nothing Then             Exit Sub         End If         Try             If (da Is Nothing) Then                 CreateDataSet()             End If             inDS.EnforceConstraints = False             da.Update(inDS, MainTable)         Catch exc As Exception             RaiseEvent ConnectionFailure("Unable to update the data source.")         End Try     End Sub End Class End Namespace 

There are four RaiseEvent calls in this code. Each call is made when the result of an attempt to connect to the database is known. Because you don't know which form is going to call these methods, or even what the form is going to do when this occurs, all you provide is the mechanism for notifying the form that it needs to do something.

This is called messaging . Internally, Windows threads send each other messages. Each thread listens for a message, and responds when the message is received.

RANT

<rant>There was once a book written about FoxPro that went on and on about "messaging." I thought it was silly. When I start my car, it causes thousands of gasoline explosions per second. But I don't say "I'm going to explode some gasoline." I say "I'm going to drive to the store." The less I know about what's under the hood, the better. In fact, the one criticism I have of .NET at this point is that it gives me way too much information. I can't imagine why I need to see the code that instantiates text boxes and positions them on the form.</rant>


So in summary, the stage is set for the class code to "call" related methods in a form to be named later. In the form code shown in Listing 1.6, you'll see how this is done.

The Form

The form code answers the question "What do I do when these events occur?" The Data Access Layer class is instantiated as object m_DAL . The events are referred to with the prefix m_DAL because they belong to the object. See Listing 1.6.

Listing 1.6. A Form That Uses a DAL Component with Events
 Option Strict On Imports System.Data.SqlClient Imports DataAccessLayer Public Class frmMain     Inherits System.Windows.Forms.Form ...Windows Form Designer generated code goes here...     Protected DidPreviouslyConnect As Boolean = False     Private ds As DataSet     Private dt As DataTable     Private dv As DataView     Public MainTable As String = "Customers"     Public KeyField As String = "CustomerID"     Protected WithEvents m_DAL As DataAccess     Dim frmStatusMessage As New frmStatus     Private Sub btnNext_Click( _       ByVal sender As System.Object, _       ByVal e As System.EventArgs) _      Handles btnNext.Click         NextRecord()     End Sub     Private Sub btnPrevious_Click( _       ByVal sender As System.Object, _       ByVal e As System.EventArgs) _      Handles btnPrevious.Click         PreviousRecord()     End Sub     Private Sub btnRefresh_Click( _       ByVal sender As System.Object, _       ByVal e As System.EventArgs) _      Handles btnRefresh.Click         frmMain_Load(Me, New System.EventArgs)     End Sub ' Call the UpdateDataSet method:     Private Sub btnUpdate_Click( _       ByVal sender As System.Object, _       ByVal e As System.EventArgs) _      Handles btnUpdate.Click      m_DAL.UpdateDataSet(ds.GetChanges())         frmMain_Load(Me, New System.EventArgs)     End Sub     Protected Sub dt_PositionChanged( _       ByVal sender As Object, _       ByVal e As System.EventArgs)         BindGrid()     End Sub     Private Sub frmMain_KeyDown( _       ByVal sender As Object, ByVal e _       As System.Windows.Forms.KeyEventArgs) _      Handles MyBase.KeyDown         If e.KeyCode = Keys.Right Then NextRecord()         If e.KeyCode = Keys.Left Then PreviousRecord()     End Sub     Private Sub frmMain_Load( _       ByVal sender As Object, _       ByVal e As System.EventArgs) _      Handles MyBase.Load         frmStatusMessage = New frmStatus         GetDataSet()         BindGrid()     End Sub     Private Sub grd_CurrentCellChanged( _       ByVal sender As Object, _       ByVal e As System.EventArgs) _      Handles grd.CurrentCellChanged         grd.Select(grd.CurrentCell.RowNumber)         If TypeOf (grd.Item(grd.CurrentRowIndex, 2)) Is DBNull Then             grd.Item(grd.CurrentRowIndex, 2) = _                 grd.Item(grd.CurrentRowIndex, 0)         End If     End Sub     Private Sub grdClick( _       ByVal sender As System.Object, _       ByVal e As System.EventArgs) _      Handles grd.Click         BindGrid()     End Sub * Handle the ConnectionCompleted event:     Private Sub m_DAL_ConnectionCompleted( _       ByVal success As Boolean) _      Handles m_DAL.ConnectionCompleted         frmStatusMessage.Close()     End Sub * Handle the ConnectionFailure event:     Private Sub m_DAL_ConnectionFailure( _       ByVal reason As String) _      Handles m_DAL.ConnectionFailure         MsgBox(reason, MsgBoxStyle.Critical, Me.Text)         End     End Sub * Handle the ConnectionStatusChanged event:     Private Sub m_DAL_ConnectionStatusChange( _       ByVal status As String) _      Handles m_DAL.ConnectionStatusChange         frmStatusMessage.Show(status)     End Sub     Sub BindGrid()         With grd             .CaptionText = MainTable             .DataSource = dv         End With     End Sub     Sub GetDataSet()         frmStatusMessage.Show("Retrieving Data From Data Access Layer")         ds = m_DAL.CreateDataSet()         dt = ds.Tables(MainTable)         dv = dt.DefaultView     End Sub     Public Sub NextRecord()         grd.UnSelect(grd.CurrentRowIndex)         grd.CurrentRowIndex += 1         grd.Select(grd.CurrentRowIndex)     End Sub     Public Sub PreviousRecord()         grd.UnSelect(grd.CurrentRowIndex)         If grd.CurrentRowIndex > 0 Then             grd.CurrentRowIndex -= 1         End If         grd.Select(grd.CurrentRowIndex)     End Sub End Class 

frmStatus is just a simple form with a label control in the center of the screen and an override of the form's Show method code:

 

 Public Overloads Sub Show(ByVal Message As String)     lblStatus.Text = Message     Me.Show()     System.Threading.Thread.CurrentThread.Sleep(500)     Application.DoEvents() End Sub 

This displays the form with a message and resumes execution after a half-second delay.

To summarize: If you want to use events in your classes to call methods in forms when you don't yet know which form and which methods, do this:

  1. Create a solution containing a project named, say, "A", output type "Class Library", which contains a class named, say, "B".

  2. In class B, define an event like this:

     

     PUBLIC Event ABC ( ByVal xxx As String ) 

  3. In the class, call using this:

     

     RaiseEvent ABC ( parms ) 

  4. Add a Windows Form project.

  5. In the form, include this:

     

     Imports A                      'so that it will know to use the class 

  6. Add this:

     

     Protected Withevents C AS B    'instantiate an object based on the class 

  7. In the form, add a subroutine or function with a Handles clause:

     

     Private Sub XYZ ( parms ) Handles C.ABC 

  8. Use the sub/function's parameter list to pass the event's parameters.

 <  Day Day Up  >  


Visual Fox Pro to Visual Basic.NET
Visual FoxPro to Visual Basic .NET
ISBN: 0672326493
EAN: 2147483647
Year: 2004
Pages: 130
Authors: Les Pinter

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net