There are three utility classes in the FileSwapper: RegistrySettings, MP3Util, and KeywordUtil. All of them use shared methods to provide helper functions.
The first class, RegistrySettings, wraps access to the Windows registry. It allows the application to store and retrieve machine-specific information. You could replace this class with code that reads and writes settings in an application configuration file, but the drawback would be that multiple users couldn't load the same client application file from a network (as they would end up sharing the same configuration file).
The RegistrySettings class provides five settings as public variables and two methods. The Load() method retrieves the values from the specified key and configures the public variables. The Save() method stores the current values in the appropriate locations. The RegistrySettings class also hard-codes several pieces of information, including the first-run defaults (which are used if no preexisting registry information is found), and the path used for storing registry settings (HKEY_LOCAL_MACHINE\Software\FileSwapper\Settings). This information could also be drawn from an application configuration file.
Public Class RegistrySettings Public SharePath As String Public ShareMP3Only As Boolean Public MaxUploadThreads As Integer Public MaxDownloadThreads As Integer Public Port As Integer Public Sub Load() Dim Key As RegistryKey Key = Microsoft.Win32.Registry.LocalMachine.CreateSubKey( _ "Software\FilesSwapper\Settings") SharePath = Key.GetValue("SharePath", Application.StartupPath) Port = CType(Key.GetValue("LocalPort", "8000"), Integer) ShareMP3Only = CType(Key.GetValue("OnlyShareMP3", "True"), Boolean) MaxUploadThreads = CType(Key.GetValue("MaxUploadThreads", "2"), Integer) MaxDownloadThreads = CType(Key.GetValue("MaxDownloadThreads", "2"), _ Integer) End Sub Public Sub Save() Dim Key As RegistryKey Key = Microsoft.Win32.Registry.LocalMachine.CreateSubKey( _ "Software\FilesSwapper\Settings") Key.SetValue("SharePath", SharePath) Key.SetValue("LocalPort", Port.ToString()) Key.SetValue("OnlyShareMP3", ShareMP3Only.ToString()) Key.SetValue("MaxUploadThreads", MaxUploadThreads.ToString()) Key.SetValue("MaxDownloadThreads", MaxDownloadThreads.ToString()) End Sub End Class
Tip |
Instead of including a Load( ) and Save( ) method, you could create property procedures for the RegistrySettings class that perform this work. Then, whenever you set a property, the value will be committed, and whenever you access a value, it will be retrieved from the registry. This adds additional overhead, but it's minor. |
The MP3Util class provides the functionality for retrieving MP3 tag data from a file. The class provides two shared functions. The first, GetMP3Keywords(), opens a file, looks for the 128-byte ID3v2 tag that should be found at the end of the file, and verifies that it starts with the word "TAG". If so, individual values for the artist, album, and song title are retrieved using the second method, GetTagData(), which converts the binary data to a string using ASCII encoding information. All the retrieved data is delimited with spaces and combined into along string using a StringBuilder. This string is then parsed into a list of keywords.
Public Class MP3Util Public Shared Function GetMP3Keywords(ByVal filename As String) As String() Dim fs As New FileStream(filename, FileMode.Open) ' Read the MP3 tag. fs.Seek(0 - 128, SeekOrigin.End) Dim Tag(2) As Byte fs.Read(Tag, 0, 3) If Encoding.ASCII.GetString(Tag).Trim() = "TAG" Then Dim KeywordString As New StringBuilder() ' Title. KeywordString.Append(GetTagData(fs, 30)) ' Artist. KeywordString.Append(" ") KeywordString.Append(GetTagData(fs, 30)) ' Album. KeywordString.Append(" ") KeywordString.Append(GetTagData(fs, 30)) ' Year. KeywordString.Append(" ") KeywordString.Append(GetTagData(fs, 4)) fs.Close() Return KeywordUtil.ParseKeywords(KeywordString.ToString()) Else fs.Close() Dim EmptyArray() As String = {} Return EmptyArray End If End Function Public Shared Function GetTagData(ByVal stream As Stream, _ ByVal length As Integer) As String Dim Bytes(length - 1) As Byte stream.Read(Bytes, 0, length) Dim TagData As String = Encoding.ASCII.GetString(Bytes) ' Trim nulls. Dim TrimChars() As Char = {" ", vbNullChar} TagData = TagData.Trim(TrimChars) Return TagData End Function End Class
Note |
The GetTagData( ) includes a very important final step, which removes all null characters from the string.Without this step, the string will contain embedded nulls. If you try to submit this data to the discovery web service, the proxy class will throw an exception, because it won't be able to format the strings into a SOAP message. |
The final utility class is KeywordUtil. It includes a single shared method— ParseKeywords()—that takes a string which contains a list of keywords, and splits it into words wherever a space, comma, or period is found. This step is performed using the built-in String.Split() method. Thus, if you index an MP3 file that has the artist "Claude Debussy," the keyword list will include two entries: "Claude" and "Debussy". This allows a peer to search with both or only one of these terms.
At the same time that ParseKeywords() splits the keyword list, it also removes extraneous strings, such as noise words ("the", "for", "and", and so on). You may want to add additional noise words to improve its indexing. In addition, strings that include only a delimiter are removed (for example, a string containing a single blank space). This is necessary because the String.Split() method doesn't deal well with multiple spaces in a row. To make the processing logic easy, keywords are added into an ArrayList on the fly and converted into a strongly typed string array when the process is complete.
Public Class KeywordUtil Private Shared NoiseWords() As String = {"the", "for", "and", "or"} Public Shared Function ParseKeywords(ByVal keywordString As String) _ As String() ' Split the list of words into an array. Dim Keywords() As String Dim Delimeters() As Char = {" ", ",", "."} Keywords = keywordString.Split(Delimeters) ' Add each valid word into an ArrayList. Dim FilteredWords As New ArrayList() Dim Word As String For Each Word In Keywords If Word.Trim() <> "" And Word.Length > 1 Then If Array.IndexOf(NoiseWords, Word.ToLower()) = -1 Then FilteredWords.Add(Word) End If End If Next ' Convert the ArrayList into a normal string array. Return FilteredWords.ToArray(GetType(String)) End Function End Class
The FileSwapper is a highly asynchronous application that provides real-time status information for many tasks. In several places in code, a user-interface operation needs to be marshaled to the user-interface thread in order to prevent potential errors. This is usually the case when updating one of the three main ListView controls in the FileSwapper: the upload status display, the download status display, and the search-result listing.
For the first two cases, there's a direct mapping between threads and ListView items. For example, every concurrent upload requires exactly one ListViewItem to display ongoing status information. To simplify the task of creating and updating the ListViewItem, FileSwapper includes a wrapper class called ListViewItemWrapper. ListViewItemWrapper performs two tasks. When it's first instantiated, it creates and adds a ListViewItem on the correct thread using the private AddListViewItem() procedure. Second, when a user calls the ChangeStatus() method, it updates the status column of a ListViewItem on the correct thread using the private RefreshListViewItem() procedure. In order to use these subroutines with the Control.Invoke() method, they cannot take any parameters. Thus, the information required to create or update the ListViewItem must be stored in temporary private variables, such as RowName and RowStatus.
Here's the complete code for the ListViewItemWrapper:
Public Class ListViewItemWrapper Private ListView As ListView Private ListViewItem As ListViewItem ' These variables are used to store temporary information required when a call ' is marshaled to the user-interface thread. Private RowName As String Private RowStatus As String Public Sub New(ByVal listView As ListView, ByVal rowName As String, _ ByVal rowStatus As String) Me.ListView = listView Me.RowName = rowName Me.RowStatus = rowStatus ' Marshal the operation to the user-interface thread. listView.Invoke(New MethodInvoker(AddressOf AddListViewItem)) End Sub ' This code executes on the user-interface thread. Private Sub AddListViewItem() ' Create new ListView item. ListViewItem = New ListViewItem(RowName) ListViewItem.SubItems.Add(RowStatus) ListView.Items.Add(ListViewItem) End Sub Public Sub ChangeStatus(ByVal rowStatus As String) Me.RowStatus = rowStatus ' Marshal the operation to the user-interface thread. ListView.Invoke(New MethodInvoker(AddressOf RefreshListViewItem)) End Sub ' This code executes on the user-interface thread. Private Sub RefreshListViewItem() ListViewItem.SubItems(1).Text = RowStatus End Sub End Class
The ListViewItemWrapper is a necessity in our peer-to-peer application, because the downloading and uploading operations won't be performed on the main application threads. However, you'll find that this class is useful in many Windows applications. Any time you need to create a highly asynchronous interface, it makes sense to use this control wrapper design pattern.