The Search class is the first of three custom-threaded objects used by FileSwapper. As part of any search, FileSwapper attempts to contact each peer with a network ping (the equivalent of asking "are you there?"). FileSwapper measures the time it takes for a response and any errors that occur, and then displays this information in the search results. This allows the user to decide where to send a download request, depending on which peer is fastest.
The drawback of this approach is that pinging each peer could take a long time, especially if some peers are unreachable. This in itself isn't a problem, provided the user has some way to cancel a long-running search and start a new one. To implement this approach, the Search class uses custom threading code.
Threading the Search class may seem easy, but it runs into the classic userinterface problem. In order to display the results in the ListView, the user-interface code must be marshaled to the main application thread using the Control.Invoke() method. This isn't difficult, but it is an added complication.
The Search class needs to track several pieces of information:
The thread it's using to execute the search.
Its current state (searching, not searching).
The ListView where it should write search results.
The SearchResults it retrieves.
The ping times it calculates.
Here's a basic skeleton that shows the private variables used by the Search class:
Public Class Search ' The thread in which the search is executed. Private SearchThread As System.Threading.Thread ' The ListView in which results must be displayed. Private ListView As ListView Private Keywords() As String ' The current state. Private _Searching As Boolean = False Public ReadOnly Property Searching() As Boolean Get Return _Searching End Get End Property ' The search results and ping times. Private SearchResults() As SharedFile Private PingTimes As New Hashtable() Public Function GetSearchResults() As SharedFile() If _Searching = False Then Return SearchResults Else Return Nothing End If End Function Public Sub New(ByVal linkedControl As ListView) ListView = linkedControl End Sub ' (Other code omitted.) End Class
The Search class code uses a thread-wrapping pattern that allows it to manage all the intricate threading details. Essentially, the Search class tracks the thread it's using and performs thread management so the rest of the application doesn't need to. The Search class provides methods such as StartSearch(), which creates and launches the thread, and Abort(), which stops the thread. This is a pattern we'll use again for the file download and upload objects.
Public Sub StartSearch(ByVal keywordString As String) If _Searching Then Throw New ApplicationException("Cancel current search first.") Else _Searching = True SearchResults = Nothing ' Parse the keywords using the same logic used when indexing files. Keywords = KeywordUtil.ParseKeywords(keywordString) ' Create the search thread, which will run the private Search() method. SearchThread = New Threading.Thread(AddressOf Search) SearchThread.Start() End If End Sub Public Sub Abort() If _Searching Then SearchThread.Abort() _Searching = False End If End Sub
The actual searching code is contained in the private Search() method. The search results are downloaded using the shared App.SearchForFile() method, which passes the request to the discovery web service. The individual peers are pinged using a private PingRecipients() method, which makes use of a separate component. This component isn't shown here, because it requires raw socket code that's quite lengthy.
Private Sub Search() SearchResults = App.SearchForFile(Me.Keywords) _Searching = False PingRecipients() Try ListView.Invoke(New MethodInvoker(AddressOf UpdateInterface)) Catch ' An error could occur here if the search is canceled and the ' class is destroyed before the invoke finishes. End Try End Sub Private Sub PingRecipients() PingTimes.Clear() Dim File As SharedFile For Each File In SearchResults Dim PingTime As Integer = PingUtility.Pinger.GetPingTime(File.Peer.IP) If PingTime = -1 Then PingTimes.Add(File.Guid, "Error") Else PingTimes.Add(File.Guid, PingTime.ToString() & " ms") End If Next End Sub
The PingUtility uses the Internet Control Message Protocol (ICMP). As you saw in Chapter 8, not all networks allow ping requests. If a ping attempt fails, the peer's ping time will show an error, but the peer may still be reachable for a file transfer.
When the results have been retrieved and the ping times compiled, the final results are written to the ListView and the call is marshaled to the correct thread using the Control.Invoke() method.
Private Sub UpdateInterface() ListView.Items.Clear() If SearchResults.Length = 0 Then MessageBox.Show("No matches found.", "Error", MessageBoxButtons.OK, _ MessageBoxIcon.Information) Else Dim File As SharedFile For Each File In SearchResults Dim Item As ListViewItem = ListView.Items.Add(File.FileName) Item.SubItems.Add(PingTimes(File.Guid).ToString()) Item.SubItems.Add(File.FileCreated) Item.SubItems.Add(File.Peer.IP) Item.SubItems.Add(File.Peer.Port) Item.SubItems.Add(File.Guid.ToString()) Item.SubItems.Add(File.Peer.Guid.ToString()) ' Store the SharedFile object for easy access later. Item.Tag = File Next End If End Sub
Note that the matching SharedFile object is embedded in each ListViewItem, so that it can be retrieved easily if the user chooses to download the file. This saves you from the work of creating a custom ListViewItem or parsing the text information in the ListViewItem to determine the appropriate settings.
Only one search can run at a time, because the App object provides a single Search variable. When the user clicks the Search button on the SwapperClient form, the current search is aborted immediately, regardless of its state, and a new search is launched based on the current keywords.
Private Sub cmdSearch_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles cmdSearch.Click If App.SearchThread.Searching Then App.SearchThread.Abort() End If App.SearchThread.StartSearch(txtKeywords.Text) End Sub
Figure 9-6 shows sample search results for a query with the single word "Debussy".