Writing the PocketChat Application

Team-Fly    

 
eMbedded Visual Basic: Windows CE and Pocket PC Mobile Applications
By Chris Tacke, Timothy Bassett
Table of Contents
Chapter 5.  Using the Windows CE WinSock for IR Communication


To explore socket communications, we're going to write PocketChat, an application that revolves around the WinSock control.

By the end of the chapter, PocketChat will provide you with a good understanding of not just socket communications, but also a plethora of ideas on what else you can use sockets for.

You'll have a small chat application that works over a TCP/IP connection or through the IrDA port and also allows us to send files to a remote user .

PocketChat is meant to be a springboard to bigger, better applications and utilities. The lessons learned here will be the foundation for almost any application you might develop that requires communication.

PocketChat is designed to talk specifically with another copy of PocketChat on another device. Because socket communications require a physical device, and because many developers don't have two devices, you need another client to talk to.

At the end of this chapter (see Listings 5.17 and 5.18), I've included the code listing and form header for a Visual Basic 6 project called PCChat, which you can use as a substitute for a second device. Simply run PCChat on your ActiveSync host PC, with ActiveSync running and connected to your device.

Running PCChat from the VB 6 IDE will actually give you the opportunity to see both sides of the socket communication at the same time by placing break points in both the VB and eVB projects.

PCChat uses a large amount of code copied directly or slightly modified from PocketChat. There are inline comments, so I haven't included any explanation text for the project.

Project Setup

PocketChat uses only one form, frmMain, and one module, modMain.

Listing 5.1 contains heading information for frmMain. For clarity, I have only included listings for properties that I modified from their defaults. You will also need to add the Picturebox, MenuBar, and WinSock controls to the project through the Project Components dialog.

Listing 5.1 Heading Information from frmMain.ebf
 Begin VB.Form frmMain    Caption         =   "PocketChat"    ClientHeight    =   4005    ClientLeft      =   90    ClientTop       =   840    ClientWidth     =   3630    ShowOK          =   -1  'True    Begin PictureBoxCtl.PictureBox picProgress       Height          =   195       Left            =   60       Top             =   3780       Width           =   3495    End    Begin MenuBarLib.MenuBar mnuMain       Left            =   1560       Top             =   120       NewButton       =   0   'False    End    Begin WinSockCtl.WinSock wskMain       Left            =   2460       Top             =   120    End    Begin VBCE.Label lblConnect       Height          =   255       Left            =   2370       Top             =   1020       Width           =   1215       Caption         =   "Disconnected"       ForeColor       =   16777215       Alignment       =   2    End  Begin VBCE.Label lblStatus  Height          =   255         Left            =   30       Top             =   1020       Width           =   2175       BackColor       =   -2147483643       BackStyle       =   1       Caption         =   "Waiting..."       ForeColor       =   8388608    End    Begin VBCE.TextBox txtRecv       Height          =   2415       Left            =   30       Top             =   1320       Width           =   3555       BackColor       =   14737632       Text            =   ""       Alignment       =   0       Locked          =   -1  'True       MultiLine       =   -1  'True       ScrollBars      =   2    End    Begin VBCE.TextBox txtSend       Height          =   915       Left            =   30       Top             =   60       Width           =   3555       BackColor       =   -2147483643       Enabled         =   0   'False       ForeColor       =   16711680       Text            =   ""       MultiLine       =   -1  'True    End End 

Figure 5.3 shows what my project looks like in the IDE.

Figure 5.3. The final project layout, as seen in the eVB integrated development environment (IDE).

graphics/05fig03.gif

Project Flow

Before you begin, let's take a look at what you are trying to accomplish and the general strategies you can take to get there.

First, because you know that you'll be connecting to another PocketChat application, you know that one of the instances will have to request a connection and the other will have to receive and accept that request. It makes good sense, then, to start the application and begin listening for a request immediately, stopping only when you try to make a connection, when a connection request comes in, or when the application is shut down.

It's also going to be useful to display not only the chat text, but also a label or tag for who said it, and you'll be implementing your own properties (or, more correctly, your property workaround) for storing and retrieving this information.

The next step will be making a connection. For the request, you'll prompt users for an IP or host name to connect to, but hard code the port to 5000. You can set this to any valid port that you want, and if you're on a network behind a firewall, your network administrator might want you to use something else.

After you make the connection, you'll need to transfer the chat text to either the remote client or a file. To keep user interaction to a minimum (you don't really want users to have to tell the application after every line comes in what kind of data it is), the receiver will have to be able to distinguish chat text from file text itself. For this, use XML-like tags to bracket the data, and then parse it out on the receiving end. Of course, this makes the transmitted data a bit longer, but makes for a friendlier application.

That's the general plan. Of course, there's a bit more to it, but the specifics will be covered as we come to them, so let's begin.

Initializing the Application

Most projects use global settings and variables that need to be set up right away and persist throughout the application. PocketChat is no exception. Application startup is usually the best time to perform those tasks, and my preference is usually to do these tasks either in Sub Main or in a procedure called from it.

The problem with doing initialization in a Form_Load event is that the form itself isn't fully instantiated during the event and modifying the form in its own Load event handler doesn't always work.

As we've already discussed, you want to start up in a listening state. With the WinSock control, this is handled by calling the Listen method, but before you can begin listening in your application, you want to do a couple of other tasks. First, you need to tell the socket what protocol to listen with: TCP/IP or IrDA. You also need to load the main form so you can inform the user that you are listening.

Startup is also a good time to get the local user's name, because it probably won't be changing during a session, and you might as well begin by writing the methods to store this "property." While you're at it, create a similar pair of methods to store the name of the remote user you are connected to as well.

In modMain's General Declarations section, create two member variables to store the information:

 Private m_LocalHostName As String Private m_RemoteHostName As String 

And then add the implementation procedures directly below the declarations:

 ' the LocalHostName "Property" Public Function GetLocalHostName() As String     GetLocalHostName = m_LocalHostName End Function Public Sub SetLocalHostName(NewName As String)     m_LocalHostName = NewName End Sub ' the RemoteHostName "Property" Public Function GetRemoteHostName() As String     GetRemoteHostName = m_RemoteHostName End Function Public Sub SetRemoteHostName(NewName As String)     m_RemoteHostName = NewName End Sub 

We can also set our port as a global constant, allowing users to easily change it in one place. Add the following to the General Declarations section:

 Public Const SOCKET_PORT = SOCKET_PORT 

Lastly in the General Declarations section, define a variable as the incoming data buffer, the use of which is covered later. Add the following line to general declarations:

 Public m_strRecv As String 

Now look at the application's entry point. Listing 5.2 shows Sub Main.

Listing 5.2 Initializing the WinSock Control
 Public Sub Main()     ' Set our protocol to TCP/IP     frmMain.wskMain.Protocol = sckTCPProtocol     ' Get our local host name and set the property     SetLocalHostName frmMain.wskMain.LocalHostName     ' Show frmMain     frmMain.Show     ' We'll assume we're a socket server to begin with     ' So we put the socket into listen mode     frmMain.wskMain.Listen     ' Set up the MenuBar     frmMain.MenuBarInit End Sub 

Notice that we're simply calling SetLocalHostName with the LocalHostName property of the WinSock control. This seems a bit pointless. Wouldn't it be easier to just use the WinSock control's native property? Well, yes, but only if you intend to use the name of the local device to identify the user. If you want to allow users to enter their names in the future, you'll have to go back and modify every place that you call frmMain.wskMain.LocalHostName. By adding and using your own property, it will make any future modifications much simpler.

Lastly call MenuBarInit, which you still need to write. Not surprisingly, you will use this method to initialize your application's MenuBar control.

Adding the Application's MenuBar

PocketChat's feature set is pretty small, so your MenuBar can be simple and still meet your needs. You'll need a button to connect and disconnect, a button to send a file, and two buttons for the current protocol, only one of which can be selected at a time. To make it a more aesthetically pleasing, Listing 5.3 shows a couple of added separators as well.

Listing 5.3 Initializing the MenuBar in a Single Location
 Public Sub MenuBarInit()     Dim btnTemp As MenuBarLib.MenuBarButton     Dim mnuTemp As MenuBarLib.MenuBarMenu     ' ----- Set up menu buttons -----     ' Add the send button to the menubar     Set btnTemp = mnuMain.Controls.AddButton("Send")     ' Set the button's properties     btnTemp.Caption = "Send"     btnTemp.Enabled = False     btnTemp.Width = 800     ' Add the connect button to the menubar     Set btnTemp = mnuMain.Controls.AddButton("Connect")     ' Set the button's properties     btnTemp.Caption = "Conn..."     btnTemp.Width = 750     Set btnTemp = mnuMain.Controls.AddButton("Xfer")     ' Set the button's properties     btnTemp.Caption = "Xfer..."     btnTemp.Enabled = False     btnTemp.Width = 700     ' Add the button to the menubar     Set btnTemp = mnuMain.Controls.AddButton("Sep1")     ' Set the button's properties     btnTemp.Caption = ""     btnTemp.Style = mbrSeparator     ' Add the button to the menubar     Set btnTemp = mnuMain.Controls.AddButton("TCP")     ' Set the button's properties     btnTemp.Caption = "TCP"     btnTemp.Width = 450     btnTemp.Style = mbrButtonGroup     btnTemp.Value = mbrPressed     ' Add the button to the menubar     Set btnTemp = mnuMain.Controls.AddButton("IrDA")     ' Set the button's properties     btnTemp.Caption = "IR"     btnTemp.Width = 350     btnTemp.Style = mbrButtonGroup   ' Add the button to the menubar     Set btnTemp = mnuMain.Controls.AddButton("Sep2")     ' Set the button's properties     btnTemp.Caption = ""     btnTemp.Style = mbrSeparator     ' Clean up     Set btnTemp = Nothing End Sub 

We've already covered the MenuBar very thoroughly, and PocketChat's use of the control is pretty basic, so I won't spend a whole lot of time explaining how things work here. If you need some clarification or would like to make the MenuBar a little nicer, refer to Chapter 4, "Working with Menu Controls for Pocket PC."

Because all the MenuBar controls are buttons, you can handle all stylus taps in a single event handler, mnuMain_ButtonClick(). Use a Select...Case control handler block to determine the key of which button was clicked, and therefore take appropriate action. The skeleton for the event handler is shown in the following code and you can add implementation code to it as you go.

 Private Sub mnuMain_ButtonClick(ByVal Button As MenuBarLib.MenuBarButton)     ' Determine which button was clicked and take action     Select Case Button.Key         Case "Send"         Case "Connect"         Case "Xfer"         Case "TCP"         Case "IrDA"     End Select End Sub 

Figure 5.4 shows what the final MenuBar looks like.

Figure 5.4. The application MenuBar when the application is in "listen" state.

graphics/05fig04.gif

Changing the Connection Protocol

When PocketChat is in its listening state, the simplest action that users can take is to change the connection protocol by tapping either the TCP or IR buttons on the MenuBar.

To change the protocol, the socket must be closed, so when the user taps either button, close the socket, change the protocol, and start listening again. These tasks are simple enough that they can be handled right in the MenuBar's Click event handler.

The last two cases in mnuMain_ButtonClick now look like this:

 Case "TCP"     ' Close the socket     wskMain.Close     ' Change connection protocol to TCP/IP     wskMain.ServiceName = ""     wskMain.Protocol = sckTCPProtocol     ' Set the socket to listen again     wskMain.Listen Case "IrDA"     ' Close the socket     wskMain.Close     ' Change connection protocol to IrDA     wskMain.Protocol = sckIRDAProtocol     wskMain.ServiceName = "PocketChat"     ' Set the socket to listen again     wskMain.Listen 

The ServiceName property is used only for IrDA mode. ServiceName acts somewhat like the IP address in TCP/IP mode, helping to keep communications straight in the presence of multiple IR signals.

Connecting to a Remote Device

The next task is to make a connection with another device, and this can be initiated in two ways:

  • Users can initiate the connection by tapping the toolbar's Connect button.

  • Remote users can make a connection request that is received by us.

In the first case, when the user requests a connection by tapping Connect, the first thing to do is to handle the MenuBar button tap. This will be another Case in the mnuMain_ButtonClick event handler:

 Case "Connect"     If wskMain.State = sckListening Then         ' User click "Connect"         If Not ConnectToPeer() Then Exit Sub         ' Show the SIP         SIPVisible = True         'Set focus to txtSend         txtSend.SetFocus     End If 

You know that you want to call Connect only if you aren't currently connected, so check the current state of the WinSock control. If you are not connected, first call ConnectToPeer, which I will discuss momentarily. Next, to make this application as user friendly as possible, assume that if users are connecting, the next thing they will probably want to do is send some chat text. To decrease the potential number of screen taps users will need to perform, ensure the built-in keyboard is shown by setting SIPVisible to True and setting the application focus to txtSend where they can begin typing their message.

Next, you need to know a few details. You need to know the host to which users want to connect (either the IP address or the host name), the port through which the connection should be made, and the protocol to use when connecting.

You have already handled the protocol with the protocol buttons, and you have set the port to our global constant (5000), so the only thing left to get is the remote host name or IP. The simplest way to get this is to prompt the user for the information with the InputBox function, which you can see in the ConnectToPeer function in Listing 5.4.

Listing 5.4 Using the WinSock Control to Connect to a Peer
 Private Function ConnectToPeer() As Boolean     Dim Peer As String     ' Set our return value     ConnectToPeer = True     ' Get name of peer to connect to - This can be an IP or a host name     ' With a serial or USB connection, _any_ IP (not host name though)   '    will connect to the ActiveSync host     Peer = InputBox("Enter host to connect to:", "Remote Host", "ctacke") If Peer = "" Then         ConnectToPeer = False         Exit Function     End If     ' Set the remote host properties     wskMain.RemoteHost = Peer     ' Take the socket out of Listen state     wskMain.Close     ' Set the socket remote port     wskMain.RemotePort = SOCKET_PORT     ' Update the status label.     ' Since the Connect call blocks, we must do it now and refresh     lblStatus.Caption = "Connecting..."     ' Refresh to get rid of the InputBox and draw the label     frmMain.Refresh     ' Make a connection     wskMain.Connect End Function 

Let's look more closely at what's happening in ConnectToPeer. First, because it is a function, it will return a value, and it is set in the first line. I like to set the default, or most likely, return value at the outset, so that I don't have to remember to set it at exit.

In this case, the return value is a Boolean indicating whether we've made a successful call to connect. This doesn't necessarily mean the connection is successful, however. That information is handled with the WinSock control's events, which are covered shortly.

Next call the InputBox function:

 Peer = InputBox("Enter host to connect to:", "Remote Host", "ctacke") 

For a default, I'm using "ctacke" so I don't have to type in my own computer name every time I run the application, which is especially useful during debugging. You might want to change this to your computer name, IP, or leave it out completely. Figure 5.5 shows what the InputBox looks like.

Figure 5.5. Prompting users for a remote host name or IP.

graphics/05fig05.gif

Next, check to be sure the user has actually entered a name. If the user taps Cancel or taps OK with nothing in the TextBox, you get the same result: a return value of an empty string. If that's the case, set the return value to False and exit the function:

 If Peer = "" Then     ConnectToPeer = False     Exit Function End If 

The rest of the function is really the "nuts and bolts" of initiating a connection. You must set the RemoteHost property of the WinSock control, because the control uses this when it tries to make the connection. Next take the control out of the "listen" state by calling Close. Calling Connect with a socket already in the listen state results in an error.

Next, set the RemotePort you want to connect to, update the status label, and call the Refresh method for the form. Refreshing here will not only ensure that the label gets drawn properly, but it will also get rid of the "ghost" that the InputBox will likely place over your form.

Finally, make a call to the Connect method. As you can see, Connect takes no parameters. Instead, it depends on the programmer to have previously set the Port, RemoteHost, and Protocol properties.

Note

If you are using a serial or USB connection, any IP or host name will connect to the ActiveSync host.


Accepting a Connection Request

If everything works as planned, we have a valid RemoteHost, Port, and Protocol and there is actually a socket server out there listening, the server's socket will fire a notification event: ConnectionRequest. At this point, all you need to do is call the WinSock control's Accept method to complete the connection process.

Because you're designing PocketChat to essentially connect with itselfalbeit another copy running on another deviceyou need to add the code to handle this event in your project:

 Private Sub wskMain_ConnectionRequest()     ' Accept the request     wskMain.Accept     ' Set the GUI to show we're connected     DisplayConnected End Sub 

I've also added a call to DisplayConnected, which you can see in Listing 5.5. DisplayConnected simply makes some changes to the GUI to let the user know that we've made the connection. It also changes the state of some of the MenuBar buttons.

Listing 5.5 Letting Users Know They're Connected
 Private Sub DisplayConnected()     ' Adjust the connect label     lblConnect.Caption = "Connected"     lblConnect.BackColor = vbGreen     lblConnect.ForeColor = vbBlack     ' Change the "connect" button's caption     mnuMain.Controls("Connect").Caption = "Bye"     ' Set the status label to the remote host     lblStatus.Caption = wskMain.RemoteHost     ' Enable "Send" and "Xfer" buttons     mnuMain.Controls("Send").Enabled = True     mnuMain.Controls("Xfer").Enabled = True     ' Enable txtSend     txtSend.Enabled = True     ' Disable protocol buttons     mnuMain.Controls("TCP").Enabled = False     mnuMain.Controls("IrDA").Enabled = False     ' Show we're connected     txtRecv.Text = txtRecv.Text & "<Connected>" & vbCrLf End Sub 

Because we're connected, the user can now send information, so we enable both the Send and Xfer buttons. We change the Connect button's caption to Bye so we can reuse the button for disconnecting. This saves us some real estate onscreen by eliminating the need for separate buttons for connecting and disconnecting. As a nicety for users, we display the name of the RemoteHost in the lblStatus filed and lastly, because we can't change protocols while connected, we disable both Protocol buttons.

Receiving the Connection Notification

Back on the client, where you initially tapped the Connect button and entered the RemoteHost name, you still have some work to do. You've sent out a request for a connection by calling Connect and the server has accepted the connection by calling Accept. However you still need to let the user know that the connection was accepted.

When the remote socket host accepts the connection, you receive a notification event: Connect. This event fires to let the caller know when and if the connection has been accepted.

Because you have already written a function that changes the GUI to indicate a connected state (DisplayConnected), you might as well make use of it by calling it from your event handler like so:

 Private Sub wskMain_Connect()     ' Called when the remote machine accepts a connection     ' Set the GUI to show we're connected     DisplayConnected End Sub 

After you make the connection, both devices should resemble Figure 5.6.

Figure 5.6. The device display when you are successfully connected to another device.

graphics/05fig06.gif

Disconnecting

After you make a successful connection, it's important to allow a graceful disconnection without requiring the user to disconnect their LAN, USB, or serial cable in the case of a TCP/IP connection or move out of range in the case of an IR connection.

You've already changed the text of the Connect button to Bye, so let's look at the implementation. First, on the device where the user taps Bye, you handle this in the now-familiar mnuMain_ButtonClick event handler. You've already added code for the Case "Connect" for when you are connected; you now need to add an Else clause to the If statement, making the entire Case look like this:

 Case "Connect"     If wskMain.State = sckListening Then         ' User click "Connect"         If Not ConnectToPeer() Then Exit Sub         ' Show the SIP         SIPVisible = True         'Set focus to txtSend         txtSend.SetFocus     Else         ' User clicked "bye"         ' Disconnect         wskMain.Close         ' Inform the user         txtRecv.Text = txtRecv.Text & "<Disconnected>" & vbCrLf         ' Set socket back to listen state         SetListenState     End If 

As you can see, it's rather straightforward. You simply close the socket by calling the WinSock control's Close method. Again, to keep the user informed, we not only display <Disconnected> in txtRecv, we also make a call to SetListenState. SetListenState in Listing 5.6 is essentially the inverse of the DisplayConnected subroutine as far as the GUI goes. The only other function it performs is to call the WinSock control's Listen function to set it back into listen state.

Figure 5.7 shows the GUI after the user disconnects.

Figure 5.7. PocketChat after a disconnect.

graphics/05fig07.gif

Listing 5.6 Encapsulating the "Listen" Code in a Single Method
 Private Sub SetListenState()     On Error Resume Next     ' Set socket back to listen state     wskMain.Listen     ' Set the status label     lblStatus.Caption = "Waiting..."     ' Set connection label     lblConnect.Caption = "Not Connected"     lblConnect.BackColor = vbRed     lblConnect.ForeColor = vbWhite     ' Disable "Send" and "Xfer" buttons     mnuMain.Controls("Send").Enabled = False     mnuMain.Controls("Xfer").Enabled = False     ' Disable txtSend     txtSend.Enabled = False     ' Set "Connect" button caption     mnuMain.Controls("Connect").Caption = "Connect..."    ' Enable protocol buttons     mnuMain.Controls("TCP").Enabled = True     mnuMain.Controls("IrDA").Enabled = True     ' This is a good general error handling method for debugging     If Err.number <> 0 Then         MsgBox "SetListenState:" & vbCrLf & Err.description, _             vbCritical, "Error"         Err.Clear     End If End Sub 

This is a good opportunity to show another example of how you can approach error handling in eVB. eVB doesn't always act as you would expect when it encounters an error. Sometimes, even without calling On Error Resume Next, an error might not get thrown immediately, but might "linger" until the end of another procedure. Sometimes after it throws the error it won't show you the offending line of code or even the offending procedure.

One way around this is to add a general error handler like the one I've added to SetListenState. At the end of the procedure, check to see if the Err object's number is zero. If it isn't, display the procedure name and the error description, and then clear the error object.

Although this doesn't gracefully handle the error, it's a useful tool while debugging and developing to give you an idea of where a more robust error-handling routine should be used.

As with most of the WinSock methods, the other end of the connection fires a notification event when Close is called and you need to handle it. If we don't, our socket will remain in a undesired state and the remote user might not be aware that the connection was closed.

In the case of the Close event being called, the remote device's WinSock control will fire the Close event. This doesn't actually cause the remote socket to close; it just notifies it that its partner has closed, so we must do the cleanup ourselves . Again, it's important to let users know what's going on through the GUI.

 Private Sub wskMain_Close()     ' A request to close was sent, we still must call Close     wskMain.Close     ' Inform the user     txtRecv.Text = txtRecv.Text & "<Disconnected>" & vbCrLf     ' Go back to a Listen state     SetListenState End Sub 

Sending Chat Text

Sending and receiving text through the WinSock control is a pretty simple, straightforward task. The challenge lies in being able to handle the text efficiently . PocketChat will allow users to send and receive not only chat text, but text files. It's important that the application be able to distinguish between the two so it knows what to do with the data. It would be very user-unfriendly if after every line of text was received you asked the user what to do with it. Users expect an application to be at least somewhat smart and these are the details that help make an application successful.

One of the easiest ways to make data distinguishable , especially text data, is to attach metadata to it. Metadata is simply "data about data," or a descriptor of the data within itself. XML is probably the best example of this, and because it works well and there's no point in reinventing the wheel, let's use XML-like tags to identify our data.

By tagging the data, we not only know what the data is, we also know where it begins and ends. This is vital with larger data sets because the WinSock control will start breaking the data into packets. Rather than send 10,000 bytes of text at one time, it will break the data into smaller chunks and send them one at a time for you to reassemble on the other side. Although this sounds somewhat complicated, the WinSock control actually handles most of it for you. We will examine this more in depth when we cover sending and receiving files.

In PocketChat, the user simply enters text into a textbox, txtSend, and taps the Send button and the remote user sees the text appear in txtRecv. Let's start at the beginning of the process.

Again, turn to the mnuMain_ButtonClick event handler. When the user taps Send, you simply step into Case Send and call out to a procedure in modMain called SendText:

 Case "Send"     ' Send chat text     SendText txtSend.Text 

SendText takes the text being sent as an input parameter (see Listing 5.7). It then prefixes the text with your hostname, wraps your tags<PocketChat Text> and </PocketChat Text>around it, and sends it through the WinSock control.

Listing 5.7 Sending Text Through an Active Connection with a Call to SendText
 Public Sub SendText(strSend As String)     Dim ilength As Integer     ' Append our local name     strSend = GetLocalHostName & ": " & strSend     ' Send the data with our custom tags     frmMain.wskMain.SendData "<PocketChat Text>" & strSend & "</PocketChat Text>"     ' Move it to the "recv" textbox     frmMain.txtRecv.Text = frmMain.txtRecv.Text & strSend & vbCrLf     ' Clear the "Send" textbox     frmMain.txtSend.Text = ""     ' Set focus back to the send textbox     frmMain.txtSend.SetFocus End Sub 

Again, to make the application more user-friendly, it copies the text you sent, without tags, to txtRecv, clears txtSend, and sets the focus back to txtSend from the MenuBar. Figure 5.8 shows the screen when sending text.

Figure 5.8. Sending text to a remote PocketChat user.

graphics/05fig08.gif

To make the application even more user-friendly, you can make it behave like most other IM applications and automatically send when the user taps the Return key.

 Private Sub txtSend_KeyPress(ByVal KeyAscii As Integer)     ' If the user presses the Return key, send the text     If KeyAscii = 13 And wskMain.State = sckConnected Then         ' Setting KeyAscii to 0 will NOT prevent the key.         ' We must use a workaround         ' Send the text         SendText txtSend.Text     End If End Sub 

Of course when you send the text, you don't want to send the final Return key as text; you just want to use it to call SendText. This is a great opportunity to look at another known bug in eVB.

Typically in Visual Basic 6, if you had a similar problem, you would just set the KeyAscii value back to 0 in the KeyPress event, and the Return keypress would be ignored. Not so in eVB. In fact, modifying KeyAscii in the KeyPress event in eVB has no effect at all.

What you do know is that when eVB enters this event handler, the TextBox's text hasn't yet appended the KeyAscii value, so in this case, txtSend.Text doesn't have the return (KeyAscii = 13) appended. This means that if you send txtSend.Text directly to SendText from here, the final Return key won't be transferred to the remote device.

The keystroke doesn't disappear though. If you just use this event handler, you will get a Return in txtSend after you send the text, essentially leaving the user on the second line of txtSend. If they don't delete it, it will get sent as the first character of their next line of chat text.

The easy way around the problem in this case is to check to see if txtSend contains only a Return, and the easiest time to check is when txtSend's Text property changes. By checking in the txtSend_Change event handler, if you find that the text is just a return (vbCrLf), you can clear the text.

 Private Sub txtSend_Change()     ' Part of the Keypress(13) workaround     If txtSend.Text = vbCrLf Then         txtSend.Text = ""     End If End Sub 

Receiving Chat Text

After the text has been sent, you get a notification event at the receiving end, DataArrival, and with it you get the number of bytes of data that have arrived. Because the data might come in packets, rather than the entire message at once, the number of bytes received might not be all the bytes being sent. If you are receiving packets, you will receive a DataArrival event for each packet, so you have to be cognizant of this when you code the event handler.

Data coming into the WinSock control is placed in a buffer. You can read the buffer at any time by calling the WinSock control's GetData method. GetData returns a string of the entire contents of the buffer, and it's important to know that DataArrival will only fire after the first time if you have retrieved the data from the WinSock's data buffer with GetData. This means that it's good practice to immediately call GetData in the DataArrival event, even if it is to put the data into another application buffer.

You will follow this model in PocketChat. As data comes in, you will append it to a member variable and then scan the variable for one of our terminating XML tags. Listing 5.8 shows what the event handler looks like.

Listing 5.8 Firing the DataArrival Event When Data Is Received Through the WinSock Control
 Private Sub wskMain_DataArrival(ByVal bytesTotal As Long)     Dim InBuffer As String     ' Get the data in the Winsock's buffer     wskMain.GetData InBuffer     ' Append the received data to our app buffer     m_strRecv = m_strRecv & InBuffer     Do While Len(m_strRecv) > 0         ' If no end tag has been received, we'll keep appending         If InStr(m_strRecv, "</PocketChat Text>") Then             ' We've received a Chat message             DisplayText m_strRecv             ' Remove handled data from buffer             m_strRecv = Mid(m_strRecv, InStr(m_strRecv, __                     "</PocketChat Text>") + _                     Len("</PocketChat Text>"))         ElseIf InStr(m_strRecv, "</PocketChat File>") Then             ' We've received a file             WriteFile m_strRecv             ' Remove handled data from buffer             m_strRecv = Mid(m_strRecv, InStr(m_strRecv, __                     "</PocketChat File>") + _                     Len("</PocketChat File>"))         End If     Loop End Sub 

As the data comes in, we append it to our member variable buffer, m_strRecv. We then look for either </PocketChat Text> or </PocketChat File>. Either tag indicates that this is the end of the data being sent, and so you can handle the data appropriately by calling either DisplayText for chat text or WriteFile for file text. We also wrap the search for </PocketChat Text> or </PocketChat File> in a Do...While loop to handle the possibility of either multiple messages or data coming in after the closing tag.

DisplayText, which follows , simply strips the XML tags from the data because you don't want to display them, and appends it to txtRecv.

 Private Sub DisplayText(TextToDisplay As String)     ' First strip the tags     TextToDisplay = Replace(TextToDisplay, "</PocketChat Text>", "")     TextToDisplay = Replace(TextToDisplay, "<PocketChat Text>", "")     'Display the text     txtRecv.Text = txtRecv.Text & TextToDisplay & vbCrLf End Sub 

Figure 5.9 shows the GUI output for DisplayText. WriteFile is a bit more complex, and we will cover it in the next section.

Figure 5.9. PocketChat, after sending and receiving text. Notice the usernames prefix each line to make the dialog more readable.

graphics/05fig09.gif

Sending a Text File

The final primary task that PocketChat performs is sending a text file through the WinSock control. This is initiated by the user by tapping the Xfer button on the MenuBar. This is also the final MenuBar function you need to write, so it's a good time to look at the full implementation for mnuMain_ButtonClick in one place (see Listing 5.9).

Listing 5.9 Handling the ButtonClick Event for PocketChat's Application Menu
 Private Sub mnuMain_ButtonClick(ByVal Button As MenuBarLib.MenuBarButton)     ' Determine which button was clicked and take action     Select Case Button.Key         Case "Send"             ' Send chat text             SendText txtSend.Text      Case "Connect"             If wskMain.State = sckListening Then                 ' User click "Connect"                 If Not ConnectToPeer() Then Exit Sub                 ' Show the SIP                 SIPVisible = True                 'Set focus to txtSend                 txtSend.SetFocus             Else                 ' User clicked "bye"                 ' Disconnect                 wskMain.Close                 ' Inform the user                 txtRecv.Text = txtRecv.Text & "<Disconnected>" & vbCrLf                 ' Set socket back to listen state                 SetListenState             End If         Case "Xfer"             ' Send a text file             SendFile         Case "TCP"             ' Close the socket             wskMain.Close             ' Change connection protocol to TCP/IP             wskMain.ServiceName = ""             wskMain.Protocol = sckTCPProtocol             ' Set the socket to listen again             wskMain.Listen         Case "IrDA"             ' Close the socket             wskMain.Close             ' Change connection protocol to IrDA             wskMain.Protocol = sckIRDAProtocol             wskMain.ServiceName = "PocketChat"             ' Set the socket to listen again             wskMain.Listen     End Select End Sub 

Notice that the newest additionCase "Xfer"is simply a call to the SendFile function. SendFile is similar to the SendText function in Listing 5.7 in that it creates a full "image" of what you need to send, including XML tags, and pushes it through the WinSock control.

Of course, it isn't quite as simple as sending chat text because you need to ask the user what file they want to send and you need to attach a bit more information, namely the filename, to the file text itself.

Let's take a look at the function in Listing 5.10.

Listing 5.10 Getting a Text File's Contents and Sending It Through the WinSock Control
 Public Sub SendFile()     Dim strPath As String     Dim strFileName As String     Dim strContents As String     Dim iStartPos As Integer     strPath = InputBox("Enter name of file to send (include full path)", _                 "Send File")     ' if the user hit Cancel of OK with a blank input, exit     If strPath = "" Then Exit Sub     ' Refresh frmMain to get rid of InputBox "ghost"     frmMain.Refresh     ' Get the file's contents     If GetFileTextContents(strPath, strContents) = True Then         ' Bracket file contents in tags         ' Add filename tags         iStartPos = InStrRev(strPath, "\")         If iStartPos <= 0 Then iStartPos = 1         strFileName = Mid(strPath, iStartPos)         ' Inform the user we're beginning and hide the SIP         frmMain.txtRecv = frmMain.txtRecv & "Sending file: " _                 & strFileName & vbCrLf         frmMain.Refresh         ' Start building data stream         strContents = "<Filename>" & strFileName & "</Filename>" & strContents         ' Add PocketChat tags         strContents = "<PocketChat File>" & strContents & "</PocketChat File>"         ' Send the file         frmMain.wskMain.SendData strContents         ' Inform the user we've sent the file         frmMain.txtRecv.Text = frmMain.txtRecv.Text & "Sent File: " _             & strFileName & vbCrLf     Else         ' failed to get contents.  Likely file doesn't exist         MsgBox "Cannot locate file to send", vbCritical, "Error sending"     End If End Sub 

First, ask the user for the path and name of the file you want to send (see Figure 5.10). It would be friendlier to add a File Explorer type interface to allow them to navigate to the file. However, you've already covered how to write this type of interface in Chapter 3, "Manipulating Files," and it would add a lot of extraneous code to this project.

Figure 5.10. Sending a file to a remote PocketChat user.

graphics/05fig10.gif

After getting the filename, refresh frmMain. Any time you display either a MsgBox or InputBox, it's good practice to call Refresh on the underlying form. If you don't, and the application must do a little work before it automatically refreshes the form, you'll get a "ghost" that whites out part of your form. Although it won't affect the application itself, it's a bit unsightly and could cause the user to think your application has locked up.

Next get the file's contents by calling GetFileContents (see Listing 5.11).

Listing 5.11 Extracting the Contents of a File Given the File's Full Path
 Public Function GetFileTextContents(Path As String, _                                     ByRef Contents As String) As Boolean     Dim filFile As FILECTLCtl.File     On Error Resume Next     ' Set our return value     GetFileTextContents = True     ' Get our application File object     Set filFile = GetFileObject()     ' Open the File     filFile.Open Path, fsModeInput, fsAccessRead     ' Make sure the call to Open was successful     If Err.number <> 0 Then         GetFileTextContents = False         Exit Function     End If     ' Loop through file, filling our input buffer     Do While Not filFile.EOF         Contents = Contents & filFile.Input(1)     Loop     ' Close the file     filFile.Close     ' Release the File Object     Set filFile = Nothing End Function 

You might recognize this function, and rightfully soit's almost identical to the GetFileContents function we used in Chapter 3. The only difference is that in Chapter 3, the function returned the contents of the file; here, you pass a variable ByRef (although ByRef is the default for function parameters, I've added it explicitly here for clarity) that the function populates with the file contents. Then it returns a Boolean to signify success or failure.

Otherwise, the function is the same as the one used previously. If you need further clarification, feel free to refer to Chapter 3. Note that the function still calls GetFileObject, which again is from Chapter 3 and this function is identical.

Listing 5.12 Minimizing the CreateObject Memory Leak by Wrapping a Global File Object with an Accessor Function
 Public Function GetFileObject() As FILECTLCtl.File     On Error Resume Next     ' If we haven't created the File object yet, do so     If IsEmpty(m_FileObject) Then         ' Create a File control         Set m_FileObject = CreateObject("FILECTL.File")         ' Ensure the object was created successfully         If Err.number <> 0 Then             MsgBox "Failed to create File object." & vbCrLf & _                 "Ensure MSCEFile.dll has been installed and registered.", _                 vbCritical, "Error"             Exit Function         End If     End If     ' Return our global File object     Set GetFileObject = m_FileObject End Function 

Looking back at the SendFile function in Listing 5.10, the next thing you do is inform the user you are about to send the file. Because this could be a lengthy operation, it's nice to let the user know you've begun.

Next, add your XML tags and metadata. Prefix the file data with the filename in its own <Filename> tag set. You'll use this on the receiving end.

Lastly, inform the user that you are finished with a message box (see Figure 5.11). This helps prevent users from assuming the device has either locked up or failed to perform the transfer.

Figure 5.11. Informing users before you send a file and after you're done sending lets them know what the application is up to.

graphics/05fig11.gif

A ProgressBar Workaround

Because sending files can take some time, especially over IR, it would be nice to provide the user with a little more than just a Sending file notice, and the WinSock control can help us.

Whenever you send texteven short amounts that aren't broken into packetsthe WinSock control fires two events: SendProgress and SendComplete. SendProgress provides the number of bytes sent so far and the number of bytes remaining to be sent. This can easily be turned into a percentage representing overall progress.

Displaying this progress to the user as a ProgressBar would be nice, but unfortunately there isn't one in the eVB toolkit. There is a native ProgressBar in the Pocket PC's API. Why Microsoft chose not to expose it through eVB, I'm not sure, but because they didn't, we need a workaround.

Fortunately, this is a simple one, easier than writing API calls to get the native ProgressBar to appear.

First, drop a PictureBox onto your form, giving it the height, width, and positioning of the desired ProgressBar. Now "draw" a colored box in the PictureBox, starting at the left side, a certain distance across the PictureBox using the DrawLine function. The entire procedure looks like Listing 5.13.

Listing 5.13 Adding ProgressBar Functionality by Filling a PictureBox with a Color
 Public Sub DrawProgress(picProgress As PictureBoxCtl.PictureBox, _                         PercentDone As Integer, DrawColor As Long)     Dim sngPercentWidth As Single     Dim sngDrawWidth As Single     ' Check for valid input     If PercentDone < 0 Then PercentDone = 0     If PercentDone > 100 Then PercentDone = 100     ' Allow for decimal representation of percent     If PercentDone < 1 Then PercentDone = PercentDone * 100     ' Determine the width of one percent     sngPercentWidth = picProgress.Width / 100     ' Determine the width to draw     sngDrawWidth = PercentDone * sngPercentWidth     ' Fill the picturebox in White     If PercentDone = 0 Then picProgress.DrawLine 0, 0, picProgress.Width, _             picProgress.Height, RGB(255, 255, 255), True, True     End If     ' Draw the progress bar - essentially a filled rectangle     picProgress.DrawLine 0, 0, sngDrawWidth, _         picProgress.Height, DrawColor, True, True     ' Without Refresh, the progress is not displayed until done     picProgress.Parent.Refresh End Sub 

I've even added a few features to make it more robust by allowing you to tell the function with what color to draw the bar (using an RBG integer or eVB constant) as well as making it semi- intelligent by allowing either whole number percentages (0100) or decimal percentages (01). The only drawback occurs when drawing a progress of 1. Does that mean 1% or 100%? I've opted to assume it means 1%.

Now that you have a ProgressBar, let's see it in action. You know that SendProgress gives you enough information to calculate a percentage, so call DrawProgress from there:

 Private Sub wskMain_SendProgress(ByVal bytesSent As Long, _                                  ByVal bytesRemaining As Long)     Dim iProgress As Integer     ' Determine progress     iProgress = CInt((bytesSent / (bytesSent + bytesRemaining)) * 100)     ' Update the progress bar     DrawProgress picProgress, iProgress, vbBlue End Sub 

And once the data has been fully sent, SendComplete fires, so you can reset the ProgressBar by calling DrawProgress with a progress of 0:

 Private Sub wskMain_SendComplete()     ' Reset the progress bar     DrawProgress picProgress, 0, vbBlue End Sub 

If you send small amounts of data, especially through a USB or LAN connection, the transfer could happen too fast for you to notice any ProgressBar at all. In fact, if the data doesn't get broken into packets, you'll likely see nothing. If you want a really quick test to see it working without having to find or create a large text file, just write a simple loop in Sub Main, something like this:

 Dim pct As Integer For pct = 100 to 10000     DrawProgress pct/100 next pct 

Receiving a Text File

Again, you have to handle to receiving end of your feature, this time the reception of a text file. Remember in our DataArrival event handler you checked for the existence of the </PocketChat File> tag? From there you call Writefile, which handles all the file data (see Listing 5.14).

Listing 5.14 Extracting the File Text from the Incoming Data Buffer and Writing It to Your Local Machine
 Public Sub WriteFile(FileString As String)     Dim filFile As FILECTL.File     Dim strFileName As String     Dim iEndPos As Integer     Set filFile = GetFileObject()     ' First strip the tags     FileString = Replace(FileString, "</PocketChat File>", "")     FileString = Replace(FileString, "<PocketChat File>", "")     ' Next get the filename     FileString = Replace(FileString, "<Filename>", "")     iEndPos = InStr(FileString, "</Filename>")     strFileName = Left(FileString, iEndPos - 1)     ' Strip the filename and tags from the file contents     FileString = Mid(FileString, iEndPos + Len("</Filename>"))     ' Open the file     filFile.Open App.Path & "\" & strFileName, fsModeOutput     ' Write the data     filFile.LinePrint FileString     ' Close the file     filFile.Close End Sub 

Once again you call GetFileObject to retrieve a File object with which you'll write the file. Then you strip off the <PocketChat File> tag set.

Next you extract the filename from the beginning of the data and save the file to the application directory using the same filename.

Handling WinSock Errors

You have covered everything for normal socket operation. Unfortunately, there will always be instances where things won't be quite "normal." What if a user tries to connect to a host that doesn't exist, or his device isn't connected to a network? What if the remote user shuts down his PocketChat application without closing the socket? What if his device crashes?

A whole host of things can go wrong with socket communications, and fortunately handling them isn't difficult. When any error occurs, the WinSock control will fire its Error event.

There are a lot of potential errors (check out the object model in the appendix for a full listing), but with a bit of testing you can determine which ones will most likely occur in your application. From there you can determine which specific errors you want to handle and which ones you'll pass through a generic error handler.

In PocketChat we'll look for a timeout error, an address not available error, and the host not found error. All others will fall into Case Else. Interestingly, the WinSock control adds the error number to the error description, so I've provided some code in Case Else that will extract just the description (see Listing 5.15).

Listing 5.15 Firing the Error Event
 Private Sub wskMain_Error(ByVal number As Long, ByVal description As String)     Dim strMsg As String     Dim iPos As Integer     Dim iProgress As Integer     Select Case number         Case sckTimedout             MsgBox "Timed out.", _                 vbExclamation, "Error Connecting"         Case sckAddressNotAvailable             MsgBox "Could not contact remote IP or Host name.", _                 vbExclamation, "Error Connecting"         Case sckHostNotFoundTryAgain             ' Likely tried to connect to a device in TCP protocol             MsgBox "Couldn't get a response from remote device.  " _                 & "Ensure remote device is set to IrDA", vbExclamation, _                 "Error Connecting"    Case Else             ' Separate the error number from description             iPos = InStr(description, vbLf)             strMsg = Right(description, Len(description) - iPos)             MsgBox strMsg, vbExclamation, "WinSock Error"     End Select     ' Make sure the socket is closed     wskMain.Close     ' We should wait for the socket to finish closing     iProgress = 0     Do While True         ' Increment our progress bar         iProgress = iProgress + 5         If iProgress > 100 Then iProgress = 0         DrawProgress picProgress, iProgress, vbBlue         ' Check the winsock state         If wskMain.State = sckClosed Then             ' Reset the progress bar             DrawProgress picProgress, 0, vbBlue             ' Exit the loop             Exit Do         End If     Loop     ' Set socket back to listen state     SetListenState End Sub 

If the control raises an error for any reason, such as a failed IR connection, you can display a MessageBox like the one shown in Figure 5.12.

Figure 5.12. When an error occurs, the Error event fires. This provides you the opportunity to give the user helpful feedback.

graphics/05fig12.gif

Regardless of the error, the socket connection either is broken or is in an unstable state, so you should gracefully close out the local socket by calling the Close method. Depending on what happened , the WinSock control might take a little while to fully close the socket, and you should always wait for it to complete. Here you sit in a polling loop waiting for it to close, updating the ProgressBar as you wait.

Caution

Sitting in a continuous loop, or polling, is often undesirable. It can cause the active thread to consume large amounts of the processor time and essentially lock your device, especially if you expect the loop to take more than a very short period of time. If you must poll, it's a good idea to allow system events to occur by using the DoEventsCE workaround found in Chapter 9.


Adding Final Touches

As with most applications, there are niceties that you can add to make the user's experience a little better. In PocketChat we've already done some of this type of work, showing the SIP (Soft Input Panel, the onscreen keyboard) when the user connects, for example. When the SIP is shown, though, it overlaps txtRecv, and if the user has done much chatting, the area where data is getting appended will be beneath the SIP. Of course the user can always hide the SIP to see it, but it's far friendlier to simply prevent the SIP from overlapping the textbox.

This is achieved by changing the height of txtRecv whenever the SIP is shown or hidden, and nicely enough eVB exposes an event for the form called SIPChange that fires whenever the SIP state changes.

By adding the code in Listing 5.16, you can avoid the SIP ever overlapping txtRecv.

Listing 5.16 Making Sure the SIP Doesn't Overlap Input or Display Fields
 Private Sub Form_SIPChange(bSIPVisible As Boolean)     If bSIPVisible Then         ' If the SIP is visible, we need to shrink txtRecv         txtRecv.Height = 1450         ' It also means they're probably about to type         If txtSend.Enabled Then             txtSend.SetFocus         End If     Else         ' If the SIP is hidden, we need to expand txtRecv         txtRecv.Height = 2415     End If End Sub 

Notice that when the SIP becomes visible, we make the assumption that the user will likely want to begin typing, so set the application focus to txtSend for them.

You also need to do some clean up of the File object if it has been created. Just as you did in Chapter 3, you handle this by calling ReleaseFileObject from frmMain's OKClick event just before ending the application:

 Private Sub Form_OKClick()     ' Clean up our File object     ReleaseFileObject     ' End app     App.End End Sub 

ReleaseFileObject simply sets the File member variable to Nothing:

 Public Sub ReleaseFileObject()     Set m_FileObject = Nothing End Sub 

Team-Fly    
Top
 


eMbedded Visual BasicR. WindowsR CE and Pocket PC Mobile Applications
eMbedded Visual BasicR. WindowsR CE and Pocket PC Mobile Applications
ISBN: N/A
EAN: N/A
Year: 2001
Pages: 108

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