-
This one’s going to be long, but for those of you who’ve felt the first 3 in this series were too easy I promise this one’s tougher J.
Let’s say you want to list all the customers from a table in a ComboBox, and update the UI based on which one is selected. To do this we’ll need to bring back two fields from the database – the customer’s name and the customer’s ID. When a customer is selected we want the ComboBox’s SelectedValue property to equal the customer’s ID.
Here’s some quick code that gets us up and running (using Northwind and LINQ to SQL):
Dim db As New NorthwindDataContext
Dim query = From row In db.Customers _
Select row.CompanyName, row.CustomerID
ComboBox1.DataSource = query.ToList()
ComboBox1.DisplayMember = "CompanyName"
ComboBox1.ValueMember = "CustomerID"
But now let’s say your boss looks at the app and says “I want you to add an ‘All Customers’ option as the first item in the list.” How would you do that? You’d need to insert an anonymous type into an existing sequence of anonymous types, which is tricky given that you can never actually use the type’s name.
Thankfully there’s a trick that uses generic parameter inference that allows us to do this (it relies on the fact that the compiler will share (or “unify”) the definition of multiple anonymous types when they have the same number of members, in the same order, of the same type, with the same names, and the same mutability):
Dim db As New NorthwindDataContext
Dim query = From row In db.Customers _
Select row.CompanyName, row.CustomerID
Dim allOption = New With {.CompanyName = "All Customers", _
.CustomerID = "-1"}
ComboBox1.DataSource = AddOptionForAll(query, allOption).ToList()
ComboBox1.DisplayMember = "CompanyName"
ComboBox1.ValueMember = "CustomerID"
...
Function AddOptionForAll(Of T)(ByVal sequence As IEnumerable(Of T), _
ByVal allOption As T) As IEnumerable(Of T)
'wrap individual element in an array and then union the two sequences
Return (New T() {allOption}).Union(sequence)
End Function
As long as we have an anonymous type that’s compatible with the anonymous type that the query generated, the compiler will determine that sequence and allOption are actually the same type and this should work fine.
Ok so let’s get to the bug – well in this case spotting it is pretty simple (it doesn’t compile J), but fixing it is tricky (though I’ve already given two pretty big hints). Here’s the text of the compiler error (it’s on the line that calls AddOptionForAll):
Data type(s) of the type parameter(s) in method 'Public Function AddOptionForAll(Of T)(sequence As System.Collections.Generic.IEnumerable(Of T), allOption As T) As System.Collections.Generic.IEnumerable(Of T)' cannot be inferred from these arguments because they do not convert to the same type. Specifying the data type(s) explicitly might correct this error.
What’s wrong and how do we fix it?
.
.
.
.
.
Answer: We actually told the compiler to generate two different anonymous type definitions, and thus it can’t unify them because the types really are different.
From earlier: “…the compiler will share (or “unify”) the definition of multiple anonymous types when they have the same number of members, in the same order, with the same names, and the same mutability”
The anonymous type that the query generates will have ReadOnly properties, whereas the anonymous type that we generated (allOption) will have Read/Write properties. The fix is to allOption immutable so that its structure will match the result of the query:
Dim allOption = New With {Key .CompanyName = "All Customers", _
Key .CustomerID = "-1"}
The “Key” modifier tells the compiler to make those properties ReadOnly and to override Equals and GetHashCode such that they only consider “Key” properties when deciding if two instances of the same anonymous type are equal. (the other hint was in the title J). For the query above, the compiler automatically inserts the “Key” modifier - i.e. what we wrote is exactly equivalent to this:
Dim query = From row In db.Customers _
Select New With {Key row.CompanyName, Key row.CustomerID}
So with a simple fix to the allOption line we’re now using the same anonymous type definition and everything works fine.
VB’s anonymous type syntax is very flexible and provides three different options: immutable, fully mutable, or partially mutable (i.e. some fields are ReadOnly while others are not). Even for a simple scenario like adding an option to a list, the better you understand how things work under the covers the easier it’ll be to debug problems later J.
-
What’s wrong with the following WPF code?
Class Window1
Sub OK_Click(sender As Object, e As RoutedEventArgs) Handles OK.Click
MsgBox("Button Clicked")
End Sub
End Class
<Window x:Class="Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1" Height="300" Width="300">
<Grid>
<Button Name="OK" Click="OK_Click">OK</Button>
</Grid>
</Window>
.
.
.
.
.
Answer: The message box will be displayed twice! To understand why, open up the project in Reflector, and in the System_Windows_Mark_IComponentConnector_Connect method you’ll see these two lines:
Me.OK = DirectCast(target, Button)
AddHandler Me.OK.Click, New RoutedEventHandler(AddressOf Me.OK_Click)
Ok so we’re adding one handler here, but click the link to navigate to “OK”. You’ll see this is actually a property of type Button (that’s what the compiler does for WithEvents/Handles – the full story of how WithEvents works is its own blog entry…). In the Setter you’ll see this code:
If (Not Me._OK Is Nothing) Then
AddHandler Me._OK.Click, handler
End If
So now we can clearly see there’s two AddHandler calls and thus the event gets handled multiple times by the same method (which is perfectly legal). One of them came from Handles OKButton.Click, and the other came from Click="OK_Click".
The key thing to realize is that if you wire up the eventhandler in XAML, don’t wire it up in code using Handles (and vice-versa). Thankfully in this scenario it’s obvious that there’s a problem because there’s a message box, but if the eventhandler was doing something more subtle (such as incrementing a counter or modifying some other state), this can lead to hard-to-diagnose bugs.
-
In this post, I’ll continue on with coding the new playlist shuffler. If you haven’t read part 1 yet, I highly recommend it so that this post will make more sense. J
Code for the controls (continued)
The Title TextBox
When the title changes, we’ll want to indicate that the playlist has changed, and we’ll want to cache the change and update the menus. This is pretty simple:
Private Sub edtTitle_TextChanged(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles edtTitle.TextChanged
playlistTitle = edtTitle.Text
FileChanged = True
ResetMenus()
End Sub
The ListBox
The listbox has only two events that we need to handle. The first is just handling whenever the selection changes – we’ll have to update the buttons in such a case:
Private Sub ListBox1_SelectedIndexChanged(ByVal sender As Object, _
ByVal e As System.EventArgs) Handles ShuffleListBox.SelectedIndexChanged
ResetButtons()
End Sub
The other event is the DrawItem event, which is called whenever a listbox element needs to be draw. Normally, this event is handled by the base class, but for this application, we’ll handle it so that we can decorate items which are linked. (We set this up by setting the DrawMode property of the listbox to OwnerDrawFixed.) Now, it even gets called when the listbox is empty, in order to draw the focus rectangle or whatever else should go into an empty list. Drawing isn’t too difficult. First, I’ll let it draw the background as usual:
Private Sub ShuffleListBox_DrawItem(ByVal sender As Object, _
ByVal e As System.Windows.Forms.DrawItemEventArgs) _
Handles ShuffleListBox.DrawItem
e.DrawBackground()
Then, assuming that the index is non-zero and we actually have some text, we’ll check to see if the item is linked. Anything linked with be written in bold, with the header node written in green, the tail node written in red, and anything in-between written in blue. The default, however, will be black text:
If e.Index >= 0 Then
Dim myBrush As Brush = Brushes.Black
which we will use if there are no links involved:
If elem.NextLinkedElement Is Nothing AndAlso elem.PrevLinkedElement Is Nothing Then
e.Graphics.DrawString(ShuffleListBox.Items(e.Index).ToString(), _
e.Font, myBrush, e.Bounds, StringFormat.GenericDefault)
but if there are links, we’ll have to use a bold font:
Else
Dim linkFont As Font = New Font(e.Font.Name, e.Font.Size, _
FontStyle.Bold, e.Font.Unit)
and set the colors appropriately:
If elem.NextLinkedElement IsNot Nothing _
AndAlso elem.PrevLinkedElement Is Nothing Then
myBrush = Brushes.Green ' Starting items are green
ElseIf elem.NextLinkedElement IsNot Nothing _
AndAlso elem.PrevLinkedElement IsNot Nothing Then
myBrush = Brushes.Blue ' Middle items are blue
ElseIf elem.NextLinkedElement Is Nothing _
AndAlso elem.PrevLinkedElement IsNot Nothing Then
myBrush = Brushes.Red ' End items are red
End If
e.Graphics.DrawString(ShuffleListBox.Items(e.Index).ToString(), _
linkFont, myBrush, e.Bounds, StringFormat.GenericDefault)
End If
End If
Finally, regardless of what we drew, we’ll need to draw the focus rectangle (and dotted line around any selections:
e.DrawFocusRectangle()
End Sub
The Link and Unlink buttons
I’ve been chattering a lot about linked files, but without walking through the process of actually linking them, it may be hard to see how they work in practice. When Link is chosen, we’ll take all of the selected files, move them together, and point them at each other as a doubly-linked list. We can then use the existence of these links to change the behavior the program when drawing the items (as we have already done), shuffling them, or even saving them (more on that later).
To link the files, first we need to know how many:
Private Sub LinkBtn_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles LinkBtn.Click
Dim numToLink As Integer = ShuffleListBox.SelectedItems.Count
Then, we need to iterate through each of those and link them.:
For index As Integer = 0 To numToLink - 1
Dim elem As xmlMediaEntry = ShuffleListBox.SelectedItems(index)
elem.NextLinkedElement = If(index <> numToLink - 1,_
CType(ShuffleListBox.SelectedItems(index + 1), xmlMediaEntry), _
Nothing)
elem.PrevLinkedElement = If(index <> 0, _
CType(ShuffleListBox.SelectedItems(index - 1), _
xmlMediaEntry), Nothing)
Note that I am using the If() ternary function above, which takes three arguments. The first is the condition to test, the second is code to run if the condition is true, and the third is code to run if the condition is false. In this case, I’m checking to see if we are at the first or last of the selected items, and linking accordingly. Since header nodes shouldn’t have a “previous” and tail nodes shouldn’t have a “next”, I set these to Nothing under those conditions – otherwise, they are set to reference the adjacent node in the appropriate direction.
Now, I want to physically move the linked tracks together so that they will actually play together. I’ll get the first one where it is, and move the others up right behind it. I do this by removing them from the listbox and adding them back in at the right spot, and then remind the listbox that they should still be selected:
If index <> 0 Then
ShuffleListBox.Items.RemoveAt(ShuffleListBox.SelectedIndices(index))
ShuffleListBox.Items.Insert(ShuffleListBox.SelectedIndices(index - 1) + 1, elem)
ShuffleListBox.SetSelected(ShuffleListBox.SelectedIndices(index - 1) + 1, True)
End If
Next
Be careful here; we are making two assumptions in all of this. First, we will be assuming that the list of selected indices is ordered from lowest to highest, and also that the list of selected objects is in the same order as the selected indices (although we could work around the latter point). These are safe assumptions to make, as near as I can tell. Second, we need to remember that the contents of SelectedIndices are indices into the Items() collection – that is, the contents of the 0th index of the SelectedIndices might refer to the 4th entry of Items() – that can get confusing.
Anyway, once we’ve moved them together and linked them, we need to tell the listbox to repaint so that it redraws the linked items appropriately. To do this, we invalidate the control, and then tell it to update the invalidated regions:
ShuffleListBox.Invalidate()
ShuffleListBox.Update()
Finally, we need to update the buttons and menus, because we have changed the playlist and we are in a condition where we could unlink now:
FileChanged = True
ResetMenus()
ResetButtons()
End Sub
Unlink is sort of the opposite of Link, as you might expect, but there is a catch – we might not have the entire chain in the selection. This means that we’ll have to navigate back to the head node for a chain before navigating through and unlinking. We’ll use a helper function to do this; the unlink handler simply checks each selected item to see if it is part of a chain and calls the helper function if so, then invalidates/repaints/updates:
Private Sub UnlinkBtn_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles UnlinkBtn.Click
For Each item As xmlMediaEntry In ShuffleListBox.SelectedItems
If item.PrevLinkedElement IsNot Nothing _
OrElse item.NextLinkedElement IsNot Nothing Then
FlushLinkSetFromBeginningToEnd(item)
End If
Next
ShuffleListBox.Invalidate()
ShuffleListBox.Update()
FileChanged = True
ResetMenus()
ResetButtons()
End Sub
Note that FlushLinkSetFromBeginning to End will clean out the whole linked chain for that item, so even if other members of that chain were part of the selection, it won’t be called a second time when those members are hit in the loop – they’ll already have had their links cleaned out. To prove that, here’s the code, which is identical to the link-crawling code we all learned in college (if we’re that old yet J):
Private Sub FlushLinkSetFromBeginningToEnd(ByVal elem As xmlMediaEntry)
Dim current As xmlMediaEntry = elem
' Rewind to the head of the linkage
While current.PrevLinkedElement IsNot Nothing
current = current.PrevLinkedElement
End While
' Now destroy the links in this chain, front to back:
Do While current.NextLinkedElement IsNot Nothing
current.PrevLinkedElement = Nothing
current = current.NextLinkedElement
current.PrevLinkedElement.NextLinkedElement = Nothing
Loop
current.PrevLinkedElement = Nothing
End Sub
Move Up and Move Down buttons
After moving the items during the Link, this code will look pretty tame. We’ll create a helper function to do the removal in either direction. We’ll verify that the helper function is told to move up or down exactly one, and return without doing anything otherwise:
Private Sub MoveEntry(ByVal direction As Integer)
If direction <> 1 AndAlso direction <> -1 Then Return
Now we’ll get the item and see if it’s linked. Moving a linked item will put the links out-of-sync with the playlist order, so we will flush the links out, since the assumption is that it was a bad chain to begin with:
Dim index As Integer = ShuffleListBox.SelectedIndices(0)
Dim item As xmlMediaEntry = ShuffleListBox.Items(index)
If item.PrevLinkedElement IsNot Nothing OrElse item.NextLinkedElement IsNot Nothing Then
FlushLinkSetFromBeginningToEnd(item)
End If
Then we remove and insert the item appropriately, keep it selected, and do the usual updating:
ShuffleListBox.Items.RemoveAt(index)
ShuffleListBox.Items.Insert(index + direction, item)
ShuffleListBox.SetSelected(index + direction, True)
ShuffleListBox.Invalidate()
ShuffleListBox.Update()
FileChanged = True
ResetMenus()
ResetButtons()
End Sub
We can then call this helper function from the event handlers, specifying -1 for “up” and +1 for “down”:
Private Sub MoveUpBtn_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MoveUpBtn.Click
MoveEntry(-1) ' Move in the negative direction
End Sub
Private Sub MoveDownBtn_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MoveDownBtn.Click
MoveEntry(1) ' Move in the positive direction
End Sub
The Shuffle Button
If you’ve read the posts that I did on my earlier attempts to shuffle a playlist, then this should be pretty familiar territory – we’ll create a new list, do an insertion from the first list to a random place in the second list (bringing along linked items), and then point the playlist to the new list. The big difference in this case is that we won’t be removing items from the first list until we’re all done. Why? Because the listbox (i.e., the original list) is an active control, and we don’t want it to have it flashing at the user during the shuffle.
First,we’ll create the storage list into which we will insert, and get the number of songs to move:
Private Sub Shuffle_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles ShuffleBtn.Click
Dim newList As New List(Of xmlMediaEntry)
Dim numberOfSongs As Integer = ShuffleListBox.Items.Count
Next, we walk through the original list and start moving things to random locations in the new list. There are two cases: the item is a header node for a linked set, or it is not. If it is a header node, we’ll move all of the associated files (middle and tail nodes) together with it, and increment the loop counter appropriately past all of those:
For index As Integer = 0 To numberOfSongs - 1
Dim mediaItem As xmlMediaEntry = ShuffleListBox.Items(index)
If mediaItem.NextLinkedElement IsNot Nothing Then
Dim placeToInsert As Integer = FindInsertionLocation(newList)
Dim count = 0
Do
Insert(placeToInsert + count, mediaItem, newList)
count += 1
mediaItem = mediaItem.NextLinkedElement
Loop While mediaItem IsNot Nothing
index += count
(FindInsertionLocation() and Insert() are helper functions; we’ll get to those in a second.) In the normal unlinked case, we just insert the file to a random location in the new list:
Else
Insert(mediaItem, newList)
End If
Next
(Again, bear with me – I’ll describe Insert() in a bit.) The new list is ready to go now, so we’ll flush all of the old items all at once using Clear(), and then reinsert the items in the new order.
ShuffleListBox.Items.Clear()
For Each m In newList
ShuffleListBox.Items.Add(m)
Next
' This is a playlist change, so react accordingly:
FileChanged = True
ResetMenus()
ResetButtons()
End Sub
(You could also simply remove and insert items to random location in the listbox without creating a second list – that would save some memory, but not enough to counter the elegance of operating beginning-to-end with minimal flickering in the listbox. If you are interested in that sort of randomization, check out the shuffle routine in my Euchre game series of posts.)
Now, to our helper functions: FindInsertionPoint needs to find a location in the new list that isn’t in the middle of a set of linked songs. We’ll do this in a simple way: generatea number, and if it’s in the middle of a list, then just rewind and use the location before the chain of songs. Of course, if the list is empty, we’d insert at 0. This is fundamentally identical to the version of this function from my earlier posts, so I won’t go into too much detail:
Private Function FindInsertionLocation(ByVal newList As List(Of xmlMediaEntry)) As Integer
If newList.Count = 0 Then
Return 0
Else
Dim placeToInsert As Integer = _
Math.Truncate(Microsoft.VisualBasic.Rnd() * (newList.Count + 1))
If placeToInsert = newList.Count Then Return placeToInsert ' Insert at end
Dim mediaItem As xmlMediaEntry = newList.Item(placeToInsert)
Dim itemEntry As xmlMediaEntry = mediaItem.PrevLinkedElement
If itemEntry IsNot Nothing Then
While itemEntry IsNot Nothing
itemEntry = itemEntry.PrevLinkedElement
placeToInsert -= 1
End While
Return If(placeToInsert >= 0, placeToInsert, 0)
End If
Return placeToInsert
End If
End Function
Insert() just wraps a normal insertion by checking for the condition where the index is at the end of the list, so that Add is used instead in that case:
Private Sub Insert(ByVal place As Integer, ByVal mediaItem As xmlMediaEntry, _
ByVal newList As List(Of xmlMediaEntry))
If place = newList.Count Then
newList.Add(mediaItem)
Else
newList.Insert(place, mediaItem)
End If
End Sub
And its overload (used in the simple file case) just calls FindInsertionLocation to get an index, then calls into its sibling:
Private Sub Insert(ByVal mediaItem As xmlMediaEntry, ByVal newList As List(Of xmlMediaEntry))
Dim place As Integer = FindInsertionLocation(newList)
Insert(place, mediaItem, newList)
End Sub
Persisting Linkage Information from Session to Session
We’re almost done now. Using what we’ve got, we can load in a playlist, link music items, shuffle it, and save it out. The linkage information, however, won’t persist – the next time we load in the playlist, it will be gone. In order to preserve this information so that the PC or Zune won’t stomp on it, we’ll avoid using metadata and instead we’ll persist a second hidden file which keeps this information handy. The information will be stored as XML in this format:
<body>
<mlink>
<media …/>
<media…/>
<media…/>
</mlink>
<mlink>
<media …/>
<media…/>
<media…/>
</mlink>
</body>
So, each <mlink> tag set will contains an arc of linked songs in sequential order.
We’ll save this information to a hidden file with a name identical to that of the playlist, but with the suffix “.links” added to it. After loading the link file after the playlist file, we’ll use this information to relink the files in the listbox. If Zune has changed the corresponding playlist (i.e., if the user has removed, added, or rearranged songs from within Zune), that’s OK; we’ll ignore any songs that we can’t identify.
First, let’s concentrate on saving the information. We’ll create a trivial helper function to create the hidden file’s name:
Private Function GetHiddenFileName() As String
Return FilePath & ".links"
End Function
Now, in EmitXML, we’ll add the following code at the end of the method:
Dim hiddenLinkInfo = <?xml version="1.0"?>
<body>
<%= GetLinkInfo() %>
</body>
This is similar to the other emit code – I create a schema for my XML document, but defer to a helper function to fill in the actual XML elements. Next, I need to determine if a previous version of the hidden file already exists. If it does, I’ll need to un-hide and delete it:
Dim hiddenPath As String = GetHiddenFileName()
If My.Computer.FileSystem.FileExists(hiddenPath) Then
File.SetAttributes(hiddenPath, FileAttributes.Normal)
My.Computer.FileSystem.DeleteFile(hiddenPath, FileIO.UIOption.OnlyErrorDialogs, FileIO.RecycleOption.DeletePermanently)
End If
Finally, I’ll save it and hide it:
' Save it and hide it.
hiddenLinkInfo.Save(hiddenPath)
File.SetAttributes(hiddenPath, File.GetAttributes(hiddenPath) Or FileAttributes.Hidden)
Next, I’ll create the helper function. To show how nicely this all nests, I’ll use one helper function for the <mlink> tags, and then one for the media items within those tags. Here’s GetLinkInfo:
Private Function GetLinkInfo() As IEnumerable(Of XElement)
Dim linkInfos As New List(Of XElement)
For Each m As xmlMediaEntry In ShuffleListBox.Items
If m.NextLinkedElement IsNot Nothing And m.PrevLinkedElement Is Nothing Then
Dim linkInfo = <mlink><%= GetLinkSet(m) %></mlink>
linkInfos.Add(linkInfo)
End If
Next
Return linkInfos
End Function
As you can see, the code iterates through the listbox and finds the next header node for a linked arc of songs. It generates a <mlink> tag for the set, and then uses yet another helper function to get eh media items, called GetLinkSet, which does a standard link crawl and returns the XELements in a list:
Private Function GetLinkSet(ByVal m As xmlMediaEntry) As IEnumerable(Of XElement)
Dim linkSet As New List(Of XElement)
Do
linkSet.Add(m.xmlElem)
m = m.NextLinkedElement
Loop While m IsNot Nothing
Return linkSet
End Function
I really like this solution; it is extremely elegant! But now we have to load it all back in when opening a playlist, so we add the following code to Open() right before the FileChanged = False line:
Try
Dim hiddenPath As String = GetHiddenFileName()
Dim xmlLinks As XElement = XElement.Load(hiddenPath)
Dim linkSets = From links In xmlLinks...<mlink> _
Select links
That opens the hidden file and gets the all of the <mlink> elemets. Next, for each of the <mlink> elements, we’ll retrieve the media elements and see if they exist in the listbox that we just populated earlier in Open(). If they do, we can throw them into a list to link up again as if the user had selected them. If we can’t find a particular song, we just skip it and work with whatever we’ve got.
For Each linkSet In linkSets
Dim linkedFiles = From media In linkSet.<media> _
Select media
Dim listOfElements As New List(Of Integer)
For Each m In linkedFiles
Dim entry As Integer = ShuffleListBox.FindStringExact(xmlMediaEntry.CreateMediaString(m))
If entry <> -1 Then
listOfElements.Add(entry)
End If
Next
Link(listOfElements)
Next
(Link() is a helper function which I’ll describe in a second.) Note that we are using the publically shared method of getting a file’s identifier from its title and artist. That should hopefully be enough to identify the song uniquely – if not, we can change CreateMediaString to return some other value, bearing in mind that whatever we choose will show up in the listbox. (Or, we can slowly step through the listbox and look for an exact match on all of a song’s attributes, but that’s not much fun…)
If we can’t open the hidden file, it might not exist, so we’ll just ignore that condition:
Catch ex As Exception
End Try
The final thing we need to do is define the Link() helper function. It’s very similar to the code we wrote to link selected items, except of course the items are not selected, and forcing a temporary multi-selection just for the purposes of linking would be an ugly hack. The biggest difference is that the items are drawn from a list of indices that we created in Open, we don’t have a SelectedItems list to help us, and we don’t need to set any file state – that’s done by the “Open” command. Otherwise, it’s very similar, and if we worked hard enough at it, we could probably combine the two functions (I’m feeling lazy):
Private Sub Link(ByVal linkList As List(Of Integer))
Dim numToLink As Integer = linkList.Count
For index As Integer = 0 To numToLink - 1
Dim elem As xmlMediaEntry = ShuffleListBox.Items(linkList(index))
elem.NextLinkedElement = If(index <> numToLink - 1, _
CType(ShuffleListBox.Items(linkList(index + 1)), xmlMediaEntry), _
Nothing)
elem.PrevLinkedElement = If(index <> 0, _
CType(ShuffleListBox.Items(linkList(index - 1)), xmlMediaEntry), _
Nothing)
If index <> 0 Then
ShuffleListBox.Items.RemoveAt(linkList(index))
ShuffleListBox.Items.Insert(linkList(index - 1) + 1, elem)
End If
Next
ShuffleListBox.Invalidate()
ShuffleListBox.Update()
End Sub
And that’s it! As usual, the full code can be found on my Temple of VB site.
‘Til next time,
--Matt--*
-
Way back in October 2007, I wrote up a few posts (here and here) on my experiments with the Windows Media Player object model. The problem I was trying to solve was that, when I had a playlist set to “shuffle,” WMP would break up songs that should always play together (for example, Jackson Browne’s “The Load-Out” and “Stay”, or Pink Floyd’s “Brain Damage” and “Total Eclipse”). In those posts, I worked around this by building my own shuffler which would emit a new randomized playlist which preserved those links, and then playing that playlist. A few months later, after I bought my Zune 80, I updated the program to copy the playlist to a ZPL format, since the format was essentially identical to WPL.
Although this was a good investigation into the WMP object model, I’ve never been happy with the program. First of all, for the program to work, I had to add metadata to the actual music tracks, and anything could come along and blow that metadata away. Second, the tracks had to already be adjacent in the parent playlist for it to work at all. Third, I had to bring up the WMP just to shuffle a playlist, which is pretty heavyweight. Finally, since I mostly care about Zune playlists these days and not WMP at all, it seemed the height of hackery to bring up the WMP to generate something for Zune (since the Zune object model is not public).
So, last week, after seeing my metadata blown away again when I carelessly updated my tracks after upgrading to Windows 7, I decided to work on a more permanent solution. Why bring up the WMP at all (or Zune, for that matter), when the playlists themselves are just XML, and the track just entries into those?
After some experiments with parsing the XML by following the parent/child chain, which seemed kludge to me, I came across a post by Avner in which he discusses porting iTunes playlists to Zune, leveraging LinQ to help with the data parsing. I realized right away that I could do something very similar in this case, and came up with what I think is a pretty elegant solution.
So here’s the plan: the application will:
(1) Load in a playlist that the user chooses
(2) Allow the user to rearrange the playlist
(3) Allow the user to link certain tracks together (and unlink them)
(4) Shuffle the playlist on command, preserving the linkages
(5) Allow the user to change the title of the playlist
(6) Allow the user to cave the playlist (or a copy of it)
(7) And, most important, persist the linkage data between sessions in such a way that the information won’t get blown away.
Let’s get started!
The form
Create a Windows Form Application (about 430 x 300) containing the following:
(1) A ListBox, about 290 x 180 and positioned on the left side, with the SelectionMode property set to “MultiExtended” and the DrawMode set to “OwnerDrawFixed.”
(2) Above that, a Label and EditBox, with the Label text set to “&Title”.
(3) To the right of those, five Buttons going down the right side, labeled “Link”, “Unlink”, “Move Up”, “Move Down”, and “Shuffle”.
(4) A MenuStrip. Select it, and on the right side of the strip, click the tiny right-pointing triangle icon and choose “Insert Standard Items” from the resulting popup. Now, go back and remove all of the items except File (Open, Close, Save, Save As, Exit) and Help (About). The Edit and Tools menu can be completely removed; we won’t be using those.
(5) An OpenFileDialog and a SaveFileDialog. The Filter property of both should be set to “Zune playlists|*.zpl” (without the quotes).
In my example, I also disabled the Maximize box in the form, added an icon and title to the form and application, etc., but those aren’t strictly necessary for the exercise.
The Zune playlist
The format of a Zune playlist (version 2.0) looks like this (memorize this, it will be useful later):
<smil>
<head>
<guid> (some guid)</guid>
<meta name="creatorId" content="(some guid)" />
<meta name="Subtitle" />
<meta name="ContentPartnerName" />
<meta name="ContentPartnerNameType" />
<meta name="ContentPartnerListID" />
<meta name="ItemCount" content="(number of tracks)" />
<meta name="TotalDuration" content=" (total duration) " />
<meta name="AverageRating" content="(whatever the average rating is)" />
<meta name="Generator" content="Zune -- 4.0.740.0" />
<title> (some title) </title>
</head>
<body>
<seq>
<media src="(file path of the track on disk)" serviceId="{BF0A0E00-0100-11DB-89CA-0019B92A3933}" albumTitle="(album title)" albumArtist="(artist)" trackTitle="(name of the track)" trackArtist="(track artist)" duration="(track duration)" />
<media … /> (etc… one for each track)
</seq>
</body>
</smil>
Code for the controls
The rest of this post will be organized based on the controls, starting from the top.
The Form
The only code associated with the form itself is the event handler for Load, a few member variables, and a helper class. Let’s take those in reverse order:
The helper class, xmlMediaEntry
We’ll be reading the Zune playlist using the XDocument class, and then we’ll be using LinQ to retrieve individual <media>…</media> tag pairs representing each track. These will be handed to us as XElements, and as we will want to add each of the tracks to the listbox, we’ll want to wrap these in an object that will expose the proper information to the ListBox – namely, a human-readable string. We will also want to have a way to link a track to another track, and we’ll be using a doubly-linked list to do this. So:
Class xmlMediaEntry
Public xmlElem As XElement
Public PrevLinkedElement As xmlMediaEntry = Nothing
Public NextLinkedElement As xmlMediaEntry = Nothing
That code allows us to cache the XElement and also point to other elements of this class backwards and forward. The header node for a chain of linked songs will have PrevLinkedElement remaining as Nothing; the tail node will have NextLinkedElement remain as Nothing; songs in the middle of the chain will have both of those set to some other node.
The following constructor allows us to initialize the object with the XElement to be cached.
Sub New(ByVal xelem As XElement)
xmlElem = xelem
End Sub
This code, exposed publically as a shared method, allows us to create a canonical (and readable) way to refer to a track in an XElement. To use one of my previous examples, the resulting value might be “The Load-Out (Jackson Browne)”:
Public Shared Function CreateMediaString(ByVal elem As XElement) As String
Return elem.Attribute("trackTitle").Value & _
" (" & elem.Attribute("trackArtist").Value & ")"
End Function
I can then leverage that function to override the ToString for the xmlMediaEntry:
Public Overrides Function ToString() As String
Return CreateMediaString(xmlElem)
End Function
End Class
And that’s all I need in that class. Once I add an instance of it to the ListBox, it will show up with nice readable text, and I’ll be able to navigate between it and any other linked songs (if any).
Member Variables
I’ll need a few variables to keep track of the state of the file, and these should all be self-explanatory:
Private FileLoaded As Boolean = False ' Do we have a file loaded into the listbox?
Private FileChanged As Boolean = False ' Has the playlist changed since it was last loaded/saved?
Private FilePath As String ' Where was the file last loaded from/saved to?
Private FileDirectory As String ' What directory was the file last loaded from/saved to?
We’ll also need to keep track of some of the playlist’s metadata. Each playlist has a GUID associated with it (if you don’t know what a GUID is, don’t worry about it – think of it as a unique identifier); if we do a “Save As” to a different file, we’ll want a different GUID. The user might also change the title of the playlist, and we should cache our creatorId format since it might be unique to the user’s situation:
Private playlistGuid As String ' What is the GUID of this playlist?
Private playlistTitle As String ' What is the title of this playlist?
Private playlistCreatorId As XElement ' What is the creator information of this playlist?
Events
The Form only has two events to handle that we care about – the Load event, and the FormClosing event. For the Load event, we’ll want to make sure that our menu items and buttons are enabled appropriately, and that we appropriately generate random numbers later on:
Private Sub VBShuffleForm_Load(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles MyBase.Load
Microsoft.VisualBasic.Randomize()
ResetButtons()
ResetMenus()
End Sub
We will be using those helper functions quite a lot, so it’s worth going over them. For ResetMenus, the “Open” commands will always be available, so we’ll apply a little overkill and ensure that. “Save As” and “Close” will be available whenever a file is loaded (otherwise, we’d have nothing to save or close!), and “Save” will be available if a file is loaded and has been changed since the last time that it was saved or loaded:
Private Sub ResetMenus()
OpenToolStripMenuItem.Enabled = True
CloseToolStripMenuItem.Enabled = FileLoaded
SaveAsToolStripMenuItem.Enabled = FileLoaded
SaveToolStripMenuItem.Enabled = FileLoaded AndAlso FileChanged
edtTitle.Enabled = FileLoaded
End Sub
Somewhat more complicated is ResetButtons. Each button reacts to one of three situations – nothing is selected in the listbox, exactly one thing is selected in the listbox, or multiple things are selected in the listbox. The breakdown is as follows:
· “Link” is only available if multiple things are selected. You can’t link if nothing is selected, and you can’t link a file to itself. It’s also OK to link files that are already linked, since you might be adding more files to the chain of songs.
· “Unlink” is available if at least one item in the selection has a link. You don’t have to select an entire link set to unlink it; it’s sufficient just to select one of the members of the link chain. If the selection contains elements from multiple link chains, then all of the associated chains will be unlinked. We can examine the “PrevLinkedElement” and “NextLinkedElement” values to determine if an item has a link.
· “Move Up” or “Move Down” moves one song, so they are only available if exactly one song is selected.
· “Shuffle” is available whenever a file is loaded.
So:
Private Sub ResetButtons()
If ShuffleListBox.SelectedIndices.Count = 0 Then
LinkBtn.Enabled = False
UnlinkBtn.Enabled = False
MoveDownBtn.Enabled = False
MoveUpBtn.Enabled = False
ElseIf ShuffleListBox.SelectedIndices.Count = 1 Then
LinkBtn.Enabled = False
MoveDownBtn.Enabled = (ShuffleListBox.SelectedIndices(0) <> _
ShuffleListBox.Items.Count() - 1)
MoveUpBtn.Enabled = (ShuffleListBox.SelectedIndices(0) <> 0)
UnlinkBtn.Enabled = (CType(ShuffleListBox.SelectedItems(0), _
xmlMediaEntry).PrevLinkedElement IsNot Nothing OrElse _
CType(ShuffleListBox.SelectedItems(0), xmlMediaEntry).NextLinkedElement _
IsNot Nothing)
Else
LinkBtn.Enabled = True
MoveDownBtn.Enabled = False
MoveUpBtn.Enabled = False
Dim enableBn As Boolean = True
For Each item In ShuffleListBox.SelectedItems
If CType(item, xmlMediaEntry).PrevLinkedElement Is Nothing AndAlso _
CType(item, xmlMediaEntry).NextLinkedElement Is Nothing Then
enableBn = False
Exit For
End If
Next
UnlinkBtn.Enabled = enableBn
End If
ShuffleBtn.Enabled = FileLoaded
End Sub
The FormClosing event handler needs to prompt the user if there are unsaved changes before closing the application. (Note that this is the event generated from the clicking the “X” on the form’s title bar – not the “Close” menu item! We’ll implement that other one later.) The user will have an opportunity to save his/her work before closing (if applicable), or else canceling the Close operation altogether.
First, we’ll define the code which determines if it’s OK to close:
Private Function OKToUnload() As Boolean
If Not FileLoaded OrElse Not FileChanged Then
Return True
End If
Dim result As MsgBoxResult = _
MsgBox("Do you want to save your changes?", _
MsgBoxStyle.YesNoCancel, "Save changes?")
If result = MsgBoxResult.Cancel Then
Return False
Else
If result = MsgBoxResult.Yes Then
Return SaveAs()
End If
End If
Return True
End Function
If there is no file loaded, or if the file is loaded but hasn’t changed, then it’s of course OK to proceed, and we return True. Otherwise, we pop up a MessageBox prompting the user if they want to save their changes. If the user chooses “Cancel,” then we simply return False to indicate that it is not OK to proceed. If the user chooses “Yes,” then we call SaveAs() (which we’ll define later) and return whatever the results are from that call. That just leaves “No,” which means the user doesn’t want to save changes, so we return True indicating that it’s OK to proceed. (The strings for the message box should be stored in the resources so that they can be localized, of course, but that makes the blog less readable. J)
Now, we can use that helper function in the event handler for Close:
Private Sub VBShuffleForm_FormClosing(ByVal sender As Object, _
ByVal e As System.Windows.Forms.FormClosingEventArgs) Handles Me.FormClosing
If Not OKToUnload() Then
e.Cancel = True
End If
End Sub
In other words, if it’s not OK to close the application, then we set e.Cancel to True indicating that the event should be canceled.
The Menu Strip
Now, we start getting into the meat of the code. Let’s go through these one by one:
FileàOpen
Open is pretty trivial to implement, as you can see:
Private Sub OpenToolStripMenuItem_Click(ByVal sender As Object, _
ByVal e As System.EventArgs) Handles OpenToolStripMenuItem.Click
Open()
End Sub
OK, I’m kidding. I moved all of the handling for this into a helper function, in case I ever wanted to open the file in some other way than the menu click. Here’s the actual implementation:
Private Function Open() As Boolean
Try
If Not OKToUnload() Then
Return False ' User canceled; just return.
End If
As you can see, we first leverage the “OKToUnload” helper function to make sure that the user doesn’t lose any work in progress. We’ll also wrap the functionality in a Try construct in case something goes wrong with the opening of the file.
Next, we’ll get the name of the playlist to be opened. If we’ve recently loaded from (or saved to) a directory, we’ll start out there; otherwise, we’ll default to the Music directory as our initial location with no particular file in mind to load:
If String.IsNullOrEmpty(FileDirectory) Then
ShuffleOpenFileDialog.InitialDirectory = My.Computer.FileSystem.SpecialDirectories.MyMusic
Else
ShuffleOpenFileDialog.InitialDirectory = FileDirectory
End If
ShuffleOpenFileDialog.FileName = ""
Dim result As DialogResult = ShuffleOpenFileDialog.ShowDialog
If result = Windows.Forms.DialogResult.Cancel Then
Return False ' User canceled; just return
End If
And we’ll need to cache that information for later usage. (I cache the directory separately, so that I can easily use it to initialize the Save dialog’s initial location later on):
FilePath = ShuffleOpenFileDialog.FileName
Dim fileData As FileInfo = My.Computer.FileSystem.GetFileInfo(FilePath)
FileDirectory = fileData.DirectoryName
Next, we’ll close any open playlist. We’ll use a helper function to do this:
ClosePlaylist()
Where ClosePlaylist() is defined to just flush out our caches, and the listbox:
Private Sub ClosePlaylist()
' Get rid of all items and clear the member variables.
ShuffleListBox.Items.Clear()
FileLoaded = False
FileChanged = False
edtTitle.Text = ""
playlistGuid = ""
playlistTitle = ""
playlistCreatorId = Nothing
End Sub
Finally, we get to the part where we load the file! As I mentioned before, we’ll be using XElements to store the items, and reading in the file in such a case is really easy:
Dim xmlElements As XElement = XElement.Load(FilePath)
If you’re debugging this, take a moment to look at the result in the watch window. You’ll see that this is a fairly deep structure that is difficult to plumb, and so digging though it to find the media items would be quite a chore. Fortunately, LinQ to XML is here to help us, and we can use it liberally. First, let’s get and cache the GUID, so that we can spit it out again if we save back to the same file:
Dim guids = From guidEntry In xmlElements...<guid> _
Select guidEntry
playlistGuid = guids(0).Value
Looks trivial, doesn’t it? But we just saved ourselves about twenty lines of code that we would have had to write to navigate the XElement structure. Basically, I told link to search for the GUID element, nested somewhere under the root of the document (hence the “…” before the <guid> tag) and return me a list (actually an IEnumeration) of all such tags. I happen to know that there’s only one in this schema, so I’ll cache the first (and only) one returned to me.
I can do something similar for the title:
Dim titles = From titleEntry In xmlElements...<title> _
Select titleEntry.Value
playlistTitle = titles(0)
edtTitle.Text = playlistTitle
But creatorId is tricker – it’s not a tag itself, but an attribute on another tag called “<meta>.” There are several other <meta> tags which could be in any order, so I can’t simply grad the first and be done with it. Fortunately, I can use “Where” and the “@” qualifiers to narrow down the field to just the one that I want:
' Get the creatorId metatag
Dim cIds = From metaEntry In xmlElements...<meta> _
Where metaEntry.@name = "creatorId" _
Select metaEntry
playlistCreatorId = cIds(0)
But it’s the media entries that we’re most interested in. We can retrieve those similar to the way we got the title and guid information:, but this time we’ll save the results to a list that we can iterate over. Once we have that list, we will create an xmlMediaEntry for each XElement in that list, and throw it into the ListBox:
Dim mediaEntries = From media In xmlElements...<media> _
Select media
Dim mediaList As List(Of XElement) = mediaEntries.ToList
For Each m In mediaList
ShuffleListBox.Items.Add(New xmlMediaEntry(m))
Next
The files are loaded, so we can update our state appropriately and return a “success:”
FileChanged = False
FileLoaded = True
ResetButtons()
ResetMenus()
Return True
Of course, if something went wrong, we need to tell the user, and return a failure code:
Catch ex As Exception
MsgBox("Error opening file: " & ex.Message, MsgBoxStyle.OkOnly)
Return False ' Oops, problems loading the playlist file.
End Try
End Function
FileàSave and FileàSaveAs
Here’s the other half of the file work. As with Open, we’ll do all of the work in a helper function, which we’ll then be able to leverage in other conditions:
Private Sub SaveToolStripMenuItem_Click(ByVal sender As Object, _
ByVal e As System.EventArgs) Handles SaveToolStripMenuItem.Click
Save()
End Sub
This, in turn, will call another function to emit the XML, after which it will clear the “FileChanged” flag and reset the menus, catching any errors that happen:
Private Function Save() As Boolean
Try
EmitXML()
FileChanged = False
ResetMenus()
Return True
Catch ex As Exception
MsgBox("Error saving file: " & ex.Message, MsgBoxStyle.OkOnly)
Return False
End Try
End Function
EmitXML, as you might guess, is where all of the work happens. But the work itself is pretty simple. Rather than opening a file and spitting in XML tags line by line, we’ll leverage an XML literal to build up the document and then we’ll save it wholesale to the file:
Private Sub EmitXML()
Dim playlist = <?xml version="1.0"?>
<?zpl version="2.0"?><smil>
<head>
<author/>
<guid><%= playlistGuid %></guid>
<%= playlistCreatorId %>
<meta name="Subtitle"/>
<meta name="ContentPartnerName"/>
<meta name="ContentPartnerNameType"/>
<meta name="ContentPartnerListID"/>
<meta name="ItemCount" content="754"/>
<meta name="TotalDuration" content="191685"/>
<meta name="AverageRating" content="7"/>
<meta name="Generator" content="Zune -- 4.0.740.0"/>
<title><%= playlistTitle %></title>
</head>
<body>
<seq>
<%= GetMediaElements() %>
</seq>
</body>
</smil>
playlist.Save(FilePath)
End Sub
How cool is that? The playlistGuid, playlistCreatorId, and playlistTitle will automatically be replaced by whatever we cached, and the media will be generated within the context of the XML by a helper function called GetMediaElements(), the form of which is very simple:
Private Function GetMediaElements() As IEnumerable(Of XElement)
Dim media As New List(Of XElement)
For Each m As xmlMediaEntry In ShuffleListBox.Items
media.Add(m.xmlElem)
Next
Return media
End Function
In other words, we just iterate through the listbox, pull the XElement out of each item, and add it to a list which gets passed back to the XML. That’s it; that’s all there is to it.
Now, we can leverage this for the Save As command; first the event handler:
Private Sub SaveAsToolStripMenuItem_Click(ByVal sender As Object, _
ByVal e As System.EventArgs) Handles SaveAsToolStripMenuItem.Click
SaveAs()
End Sub
And then the helper function. First, we get the name of the file to save, setting the initial directory to what we cached earlier, and defaulting the filename to the name of the title (instead of the original filename, reasoning that, if the user was saving to the same file, they would have used “Save” and not “SaveAs”):
Private Function SaveAs() As Boolean
ShuffleSaveFileDialog.FileName = playlistTitle
ShuffleSaveFileDialog.InitialDirectory = FileDirectory
Dim result As DialogResult = ShuffleSaveFileDialog.ShowDialog()
If result = Windows.Forms.DialogResult.Cancel Then
Return False
End If
We should supply a new GUID if the filename is going to change:
If FilePath <> ShuffleSaveFileDialog.FileName Then
playlistGuid = "{" & System.Guid.NewGuid.ToString() & "}"
End If
Then we cache the name and directory:
FilePath = ShuffleSaveFileDialog.FileName
Dim fileData As FileInfo = My.Computer.FileSystem.GetFileInfo(FilePath)
FileDirectory = fileData.DirectoryName
And having done that, we can just call Save() to do the rest of the work:
Return Save()
End Function
That’s it for the file work – for now. We’ll revisit Open and Save later when we persist the linkage info.
FileàClose and File->Exit
In the case of Close, we’re closing the playlist, not the application; for Exit, we close the application as well. The code for this is pretty trivial, as we have already written both of the helper functions involved:
Private Sub CloseToolStripMenuItem_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles CloseToolStripMenuItem.Click
If OKToUnload() Then
ClosePlaylist()
End If
End Sub
Private Sub ExitToolStripMenuItem_Click(ByVal sender As Object, _
ByVal e As System.EventArgs) Handles ExitToolStripMenuItem.Click
If OKToUnload() Then
Close()' This is Form.Close, of course.
End If
End Sub
HelpàAbout
This is pretty trivial to handle, and I’ve described it before in earlier posts, but here goes: right-click on the project, choose “Add New Item,” and add an About Dialog. Then, right-click the project again and choose “Properties.” In the resulting properties document, select the Application tab (if it isn’t already selected) and click the “Assembly Information…” button. Fill in the information however you prefer. Then, we can hook up the dialog to the command pretty easily as follows:
Private Sub AboutToolStripMenuItem_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles AboutToolStripMenuItem.Click
Dim dlg As New VBSmartShuffleAbout
dlg.ShowDialog()
End Sub
That’s all I’m going to cover in this post… it’s getting too long, and I’ve got nine more pages to go! I’ll post the next set in a separate post, and we’ll wrap this up then.
‘ Til next time,
--Matt--*
-
In yesterday’s post we saw that the use of the wrong comparison operator with Nothing in an If block can lead to surprising results. Let’s look at a slightly different case today:
Dim x As Integer = Nothing
If x = Nothing Then
MsgBox("A true statement - we land here")
Else
MsgBox("The statement is a lie - we land here")
End If
What gets printed? If the code is intended to print “A true statement…”, is there actually a bug in this code?
.
.
.
.
.
Answer: The code is correct according to its “specification” (I’m using the term loosely here J), so from a technical point of view there is no bug here. However, from a code readability and maintenance point of view, I’d argue that there is a bug.
The Nothing literal in VB means “default value for this type”, and for an Integer that would be 0. So the code above is exactly equivalent to this:
Dim x As Integer = 0
If x = 0 Then
MsgBox("A true statement - we land here")
Else
MsgBox("The statement is a lie - we land here")
End If
Now that’s way more readable than the version above, and dramatically reduces the potential for confusion. In general*, if you ever find yourself writing “= Nothing” in a conditional expression, you should either be changing the = to “Is” (as we saw yesterday), or you should change Nothing to the actual default value if possible.
The next post will have a real bug, I promise J
*Yes, there may be cases such as structure/generics/overloaded operators where you actually would want to say “= Nothing” – my point is that if you’re going to do this make sure you know what you’re doing, and insert a comment so that those reading your code also know what you’re doing J.
-
Microsoft has this neat mailing list called “Spot the Bug” where developers can send interesting snippets of code that look correct but actually have subtle bugs in them. The puzzles are a lot of fun and I’ve always thought it’d be a fun thing to try here on the team blog. Over the past year or so I’ve been keeping a list of interesting bug reports and emails where people have been tripped up by some of VB’s hidden subtleties (though admittedly many of these could apply to C# as well).
The format’s gonna be a bit of an experiment – let me know if you prefer to see the answer in the same post or posted a day later. Alright let’s get started…
Spot the Bug:
The following code is intended to print “Yes” – will it?
Dim x As Integer? = Nothing
If x = Nothing Then
MsgBox("Yes: x contains null")
Else
MsgBox("No: x does not contain null, and has a real value")
End If
.
.
.
.
.
Answer: No! We’re using the equality operator to compare a nullable integer with Nothing – this results in null propagation, i.e. the result of the expression is Nothing. An If-statement attempts to convert its condition to a Boolean, so basically we’re left with this:
If Nothing Then
Else
'Obviously we'll always land here
End If
This can be really surprising when working with nullables, so we’ve decided to add a warning for this case in VB10:

The warning explains the fix – use “Is” instead of = to avoid the null propagation and get the semantics you expect:
Dim x As Integer? = Nothing
If x Is Nothing Then
MsgBox("Yes: x contains null")
Else
MsgBox("No: x does not contain null, and has a real value")
End If
Check back for a similar case but with a bit of a twist tomorrow...
-
We’ve started two new “How Do I” videos series on the Visual Basic Developer Center. More to come, but here’s the latest!

-
There were a lot of Microsoft announcements that happened yesterday! Many of these happened during Steve Ballmer’s keynote at the SharePoint Developers Conference (Video, Twitter). Here is the list:
MSDN subscribers can download Beta 2 now! We will be opening up Beta 2 for everyone to download starting this Wednesday.
In honor of VS 2010 Beta 2, we have a lot of great content prepared. The following MSDN page aggregates our VB 2010 content.
We hope you will enjoy trying out the next release! We definitely look forward to hearing your feedback. Here are some places where you can contact us:
Thanks!
Lisa
-
Let me count the ways: xcopy, ClickOnce, Windows Installer (.msi), Windows Installer XML, Group Policy, Active Directory, Systems Management Service or System Center Configuration Manager, login scripts, CD, USB drive, and many more third-party software options.
Despite the many options, one common requirement is that applications typically have prerequisites to install before the application can run. Prerequisites can include the .NET Framework, Visual Basic PowerPacks, SQL Server Express, Windows Installer, runtime libraries, or maybe even a custom prerequisite.
There are three concepts in deployment related to prerequisites.
- Nesting.
- Merging.
- Chaining or bootstrapping.
Nesting is the process of embedding a Windows Installer file (.msi) within another .msi file. However, the How to create a nested .msi package article has an important disclaimer.

The drawbacks of creating a nested MSI installation are listed in the same article.
- Nested installations cannot share components.
- An administrative installation cannot contain a nested installation.
- Patching and upgrading will not work with nested installations.
- The installer will not correctly cost a nested installation.
- Integrated ProgressBars cannot be used with nested installations.
- Resources that are to be advertised cannot be installed by the nested installation.
- A package that performs a nested installation of an application should also uninstall the nested application when the parent product is uninstalled.
For these reasons, nesting is no longer supported.
Merging includes shared code, files, resources, registry entries, and setup logic to applications as a single compound file. Prerequisites available as a merge module (.msm) form can be added to a .msi file. For example, if a prerequisite is available as an .msm, you can add it to a Setup project in Visual Studio as shown in How to: Create or Add a Merge Module Project. However, merge modules cannot be serviced by the same owner as the .msi file, so it is difficult to fix issues in the merge module. Tao of the Windows Installer, Part 4 lists two cautionary notes:
- Do not consume merge modules of vendors who do not promise to fix their merge modules promptly when bugs arrive
- Be prepared to handle the heat when bugs are found in your merge module causing issues for others’ products that have consumed your merge module and you get to put out the flame
For these reasons, using merge modules is not recommended.
Chaining or bootstrapping is the process of checking for and installing missing prerequisites, including installing the application that is going to be used to install the rest of the prerequisites and application. You can use Visual Studio to generate a chainer/bootstrapper that is called Setup.exe. This program checks for and installs missing prerequisites before installing the application.
If you have created a Visual Basic Windows application by using Visual Studio, the default selections in the Prerequisites Dialog Box are Windows Installer and the .NET Framework. To learn more about how to install prerequisites in Visual Studio, see the following topics.
How to: Install Prerequisites in Windows Installer Deployment
How to: Install Prerequisites with a ClickOnce Application
You can create custom prerequisites and add them to the Prerequisites Dialog Box in Visual Studio. Use the Bootstrapper Manifest Generator to create a product.xml file that describes the prerequisite and a package.xml files that includes locale-specific error messages. Then, copy the bootstrapper package to \Program Files\Microsoft SDKs\Windows\v6.0A\Bootstrapper\Packages folder. For more information, see Creating Bootstrapper Packages.
You may want to use other tools besides Visual Studio to generate a bootstrapper. For example, you can also use MSBuild and the GenerateBootstrapper Task to create a bootstrapper on a build computer. Alternatively, you can use the Windows Installer XML Toolset to generate .msi files and package prerequisites. For more information, see http://wix.sourceforge.net/.
Nesting and merging create a single file to deploy an application and its prerequisites, but these methods are not supported and not recommended. Chaining or bootstrapping is an effective way of checking for and installing prerequisites on end-user computers so that your applications run successfully. For more questions about chaining and bootstrapping, search for answers or post new questions in the ClickOnce and Setup & Deployment forum.
Happy deployment!
Mary Lee, Programming Writer.
-
In my last post, I talked about the hidden costs that can occur whenever you call out to methods, particularly in loops. In looking at my examples, reader KG2V commented that another thing that folks need to be aware of is avoiding the assumption that the world (or, in this case, a list) is a static thing. It’s a good point and it deserves some attention, particularly given that different languages will react in different ways to change.
To illustrate this point, let’s assume that we have a form with a ListBox on it, and that we also have a button on the form that, when clicked, runs the following VB code:
' Bad -- don't do this
Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
For i As Integer = 0 To ListBox1.Items.Count - 1
ListBox1.Items.RemoveAt(i)
Next
End Sub
Ostensibly, we want to remove all of the items from the ListBox. (Yes, normally we’d use Clear() to do that, but stay with me for a moment; I’ll switch to more “real” cases in a few paragraphs.) But if we run this code, we’ll get an exception. Why? Because whenever we remove an object, all of the indices of the objects after it will decrease by 1. Let’s assume that there are three objects in the list - A, B, and C – so that Count=3:
· When i = 0, we’ll remove the object at index 0, which is A. B now moves to index 0, and C now moves to index 1.
· When i = 1, we’ll remove the object at index 1, which is C. There is nothing after C, so nothing else changes.
· When i =2, we’ll remove the object at index 2, which is… nothing. There’s only one object left, which is B, and it’s at index 0. So, we get an out-of-bounds exception.
In VB (as was pointed out to me in the comments section of my last point), the value of Count is only assessed once, right when the loop is encountered, so we will always loop through (in this case) three times.
But is the lack of reevaluation of Count the only issue? Maybe not. Let’s try the same method in C#, in which Count is evaluated on every iteration:
// Bad -- don't do this.
private void button1_Click(object sender, EventArgs e)
{
int i;
for (i = 0; i < listBox1.Items.Count; i++)
{
listBox1.Items.RemoveAt(i);
}
}
· When i = 0, we’ll remove the object at index 0, which is A. B now moves to index 0, and C now moves to index 1.
o Now, we’ll loop back to the top. The value of i becomes 1, and the value of Count becomes 2. The stop condition is not met, so we keep going.
· When i = 1, we’ll remove the object at index 1, which is C. There is nothing after C, so nothing else changes.
o Again, we loop back to the top. The value of i becomes 2, and the value of Count becomes 1. The stop condition is met, and we exit the loop.
So, although no exception is thrown, the method also fails in C# -- B is left behind. So, the problem isn’t whether or not Count is evaluated; it’s that the elements in the list are changing indices on each deletion.
Sometimes doing things backwards is good
This is why, in the case where multiple removals need to be made on an indexed list, a smart programmer (all of whom are yawning right now as they read this) will work backwards. That is, they start with the largest index and work down towards zero. For example, imagine that we want to remove the 4th, 7th, and 9th element from a list. We don’t want to do it this way:
' Bad -- don't do this
ListBox1.Items.RemoveAt(4)
ListBox1.Items.RemoveAt(7)
ListBox1.Items.RemoveAt(9)
Once we’ve removed the 4th item, then the element at 7 will have moved to 6 and the element at 9 will have moved to 8. After we’ve removed the new 7th item, the element at 8 will move to 7, and so on. We’d either be removing the wrong elements or we’d crash. It’s far better to do them in reverse order:
ListBox1.Items.RemoveAt(9)
ListBox1.Items.RemoveAt(7)
ListBox1.Items.RemoveAt(4)
None of the indices up front will be affected by changes that are occurring behind them in the list – their indices won’t change, and this will work fine.
This issue comes up when you are (for example) deleting items from a listbox that supports multiselection. You basically have two ways to get the selected items in a listbox: you can use the SelectedIndices member function, which returns a collection of indices, or you can use SelectedItems, which returns a collection of selected items. Given either collection, there’s no way to say “remove all of these.” There is a “Remove” command on the resulting collections, but if you use it, you’ll just be removing the item from *that* collection and not from the ListBox, which has its own collection of items – essentially, you’ll just deselect the items.
So, what do you do? At first, it looks like you have two options; you could try removing them from the master list based on item, or based on object. Let’s look at removal based on the item:
' Bad -- don't do this
For Each index As Integer In ListBox1.SelectedIndices
ListBox1.Items.RemoveAt(index)
Next
This won’t work. The selected indices are in numerical order (no matter in which order you selected them), so you’ll be sliding indices on each deletion and will subsequently delete the wrong things. You won’t crash if you overreach on an index, because the For…Each loop will check for Nothing when it does a MoveNext internally and will exit gracefully, but this is actually worse since it may take longer before you notice a problem!
This won’t work either, and will potentially crash:
' Bad -- don't do this
For index As Integer = 0 To ListBox1.SelectedIndices.Count - 1
ListBox1.Items.RemoveAt(ListBox1.SelectedIndices(index))
Next
Again, the indices for the downstream objects will decrease, you’ll delete the wrong things, and you’ll likely crash.
You can, however, iterate down instead, as long as you are confident that the selected indices are in numerical order (as they will be in this case):
' This works:
For index As Integer = ListBox1.SelectedIndices.Count - 1 To 0 Step -1
ListBox1.Items.RemoveAt(ListBox1.SelectedIndices(index))
Next
Having found something that works via iteration, let’s now consider the enumeration angle instead. In general, I prefer to enumerate lists whenever possible instead of iterating through them. This is not one of those times, though. Consider this code, which will fail:
' Bad -- don't do this
For Each item In ListBox1.SelectedItems
ListBox1.Items.Remove(item)
Next
“Well, c’mon, why doesn’t this work?” I hear you cry. “I don’t care about indices in this case, I’m not even using them; I’m just removing specific items.” True, but if you run this code, you will indeed get an exception essentially stating that the collection has been modified and the list of selected items is no longer valid for enumeration. We can’t work around this by assigning the collection of selected items to another variable and enumerating on that, since that just creates a reference to the same collection. In order for it to work, you’ve have to get the references out of the collection itself so that the ListBox is no longer part of the equation on the removal loop’s assessment.
' Really, this is just silly -- don't ever do this
Dim selItems As New Collection
For Each item In ListBox1.SelectedItems
selItems.Add(item)
Next
For Each item In selItems
ListBox1.Items.Remove(item)
Next
Functionally, this will work; however, you shouldn’t do it for two reasons:
· First of all, it’s stupid – you have to loop through twice. Occasionally, I do need to clone lists, like when creating an editable list from a read-only list – I’ve done that in a previous blogpost on this site, in fact. But my motives are not so noble here; I’m using this for no other reason than allowing enumeration to work.
· Second, each call to RemoveItem has a hidden cost – namely, the list has got to go find that item. In this case, you can bet that there’s good hashing going on that doesn’t make that as painful as it might be, but do you really want to count on that, given the better numeric alternative above?
No, just there’s really no good way to do removal by enumeration that I’ve ever found. And since each object is totally clueless about whether it itself is selected or not, you can’t even enumerate through the collection yourself and delete it if it’s marked as selected.
Of course, I’ve just talked about deletions, since they are more interesting than additions due to the crash potential. However, additions can also mess you up in a loop due to similar index changes, and I’ve actually run into cases where a list may have both insertions and deletions done, so that I wasn’t even able to leverage the change in list size as a clue to figure out what had happened to the data. The bottom line is, whenever doing an operation within a loop, you need to understand whether or not that operation will collide with your loop conditions and assumptions. Generally, you control this in your code, but in multithreaded environments, where changes to an existing store of data might be made from a different thread “simultaneously” with your thread, you really need to understand how thread-safe your code is with respect to data changes. (But that is a story for another time.)
‘Til next time,
--Matt--*
-
(Note: there was a grievous error in this post based on a bad assumption on my art regarding VB. Not feeling the need to hide my ignorance :-), I have instead made a number of edits in this post to bring it back to some semblance of reality.)
One thing that gets me annoyed with myself is realizing that the product or service I’ve just bought has some hidden costs that I didn’t anticipate. It might be as complicated as realizing that my plane ticket has all sorts of byzantine surcharges and luggage costs, or as simple as making budgeting for a new killer gaming PC and forgetting about the sales tax – it seems that there are always extra costs out there that will come back to bite you. Being (as I have said in past postings to this blog) an inherently lazy person, and being fully caught up in our “I must have the sparkly thing right now” culture, I’m apt to overlook the fine print in any purchase agreement unless I really force myself to slow down and pay attention to the details.
One of the times I really have to pay attention is when I’m reviewing code here at Microsoft. As a member of the release team that makes the final decisions as to what last-minute fixes are safe enough to be applied to an imminent release, it’s my job to take a look at the proposed fix and make sure that it doesn’t introduce more problems than it purports to solve – i.e., that it doesn’t add hidden costs.
On the rare occasion that I find a problem in someone’s code, it’s usually something that’s obviously dysfunctional – a case where a variable isn’t initialized properly, or where a different team has simultaneously made a change that will render this change dangerous. But some of the problems are more subtle, and might not be obvious if only minimal testing and review has been done. Consider the following code (very similar to a case I reviewed here at work the other day):
Public MyList As New List(Of String)
Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
Dim s As New System.Text.StringBuilder
For i As Integer = 0 To MyList.Count - 1
s.Append(MyList(i))
Next
End Sub
Edit: This is an error in my blog post – I hadn’t realized that VB, unlike many other languages, only gets the value of the stop condition once (thanks to Phil Wells for pointing this out). To make my point, I would have to be using code like this:
Dim s As New System.Text.StringBuilder
Dim i As Integer = 0
Do While i < MyList.Count() - 1
s.Append(MyList(i))
i += 1
Loop
The method Count() might still have a hidden cost, but in the For Loop case, I’m at least only hitting it once. The fact that VB only analyzes the stop condition once leads to a different issue to be aware of that I’ll cover in my next blog.
Seems pretty simple, yes? When a button is clicked, the code iterates through the strings in a list and appends them to a StringBuilder, creating (for whatever reason) one long string containing all of the contents. And, to cursory appearances, it will function correctly.
However, the code has hidden costs. You see, written this way, the value MyList.Count is going to be reevaluated on every iteration of the “For” loop, even though the value itself isn’t changing. Furthermore, my indexing into MyList, which is always with reference to the beginning of the list, will also have a non-zero impact. This impacts the performance of the code and will make my customers unhappy over the sloth-like operation of my application.
In this case, the cost isn’t really high, as Count() is a property wrapping something that’s stored as a private integer member, and index referencing also has tricks to speed it up, but there’s still a cost and it can add up. It’s even worse if the Count() call has to be calculated! Consider this really awful code where I’ve created my own “List” type:
Public Class MyLameObject
' For some unknown reason, I'm creating my own linked list instead of using a List(Of) generic.
' Please don't do this for real!
Public data As String
Public NextMLO As MyLameObject
Public Sub Insert(ByVal mlo As MyLameObject)
If NextMLO IsNot Nothing Then
NextMLO = mlo
Else
mlo.NextMLO = NextMLO.NextMLO
NextMLO.NextMLO = mlo
End If
End Sub
Public Function Count() As Integer
Dim ct As Integer = 0
Dim current As MyLameObject = Me
Do While current IsNot Nothing
ct += 1
current = current.NextMLO
Loop
Return ct
End Function
Public Function Item(ByVal index As Integer) As MyLameObject
If index >= Count() OrElse index < 0 Then Return Nothing
Dim current As MyLameObject = Me
For i As Integer = 0 To index - 1
current = current.NextMLO
Next
Return current
End Function
End Class
Public MyLameObjectHeader As New MyLameObject
Public MyLameObjectHeader As New MyLameObject
Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) _
Handles Button1.Click
Dim s As New System.Text.StringBuilder
For i As Integer = 0 To MyLameObjectHeader.Count - 1
s.Append(MyLameObjectHeader.Item(i))
Next
End Sub
Perhaps as a school assignment, I’ve created a singly-linked list which contains objects of type “MyLameObject” (instead of using optimized collections that we provide). I’ve also created a member function called “Count” which returns the number of objects in my list (starting from the current node) and another member function called “Item” which returns me a reference to a specific object in the list, as indexed from the current node. The code will compile and it will “work.” But, oh! It has so many hidden costs, and if the definition of MyLameObject exists in an assembly for which I don’t have the source code, I might not even realize it!
First, let’s look at button click handler. It’s calling Count() on every single iteration, and there will be a number of iterations equal to the value returned from Count(). Count(), in turn, walks the entire list every time it’s called. So, that means that if my list contains 1000 items, that means that I’ll be referencing a MyLameObject one million times! (This is what they refer to as an “O(n2) problem” in computer science classes.)
But wait, it gets worse: the code for Item() calls Count() also on each iteration to verify that the index is not out of bounds – that brings us up to a count of 1001000 MyLameObject references.
Edit: As noted above, that isn’t true for VB, though it would be for many other languages
And, of course, Item() itself needs to walk the list to actual get to the desired item, on every iteration – that adds 500500 more object hits (using the n(n+1)/2 formula) , for a grand total of 1501500 1500500 object hits. And all that just to concatenate 1000 strings! That, I fear, is going to slow down my code’s performance somewhat.
Now, of course I could add code to get track of values for Count(), and that would reduce my cost to a third of what it was. I could use some clever hashing algorithm to more easily get to the items while still being able to use method calls. I could enumerate the list myself instead of relying on “Count” or on “Item,” and that would really cut my costs down dramatically. Or, best of all, I could avoid reinventing the wheel and using the classes that VB.NET already supplies for me, since we do our best to optimize them.
But that’s not the point that I’m trying to make here. The point is, whenever you call a function, you are at its mercy performance-wise – you are stuck with whatever hidden costs it contains. You might not even be able to determine what these hidden costs are if you don’t own the code you’re calling, except by experimentation. And if you call that function iteratively, you’re just compounding the problem.
Now, you can’t avoid making calls into code you don’t own – at some level, we all use libraries or assemblies that are handed to us. But you can be smart about how you use them. Consider *this* code:
Dim s As New System.Text.StringBuilder
Dim count As Integer = MyList.Count
For i As Integer = 0 To count - 1
s.Append(MyList(i))
Next
Now, I’m only calling Count() once, so I minimize the impact of its cost! Or,
Edit: As above, this doesn’t actually make a difference in VB. Of course, you still have the cost of “Count(),” whatever it may be. In this case, it’s just a property read; if it’s some home-grown collection or data repository, Count() (or it’s analog) could be a lot more costly.
Consider this even better example that you can use on collections that support IEnumerable (as all System.Collections types do):
Dim s As New System.Text.StringBuilder
For Each data As String In MyList
s.Append(data)
Next
Yeah! Now I’m firing on all cylinders! Now I don’t have to worry about indexing costs either – I’m just walking the list, collecting my strings in the fastest way possible. Really, the only cost I have to deal with is the cost of actually appending the data (which is in fact why I’m using the highly-optimized StringBuilder class instead of just concatenating strings using a much more costly s &= data line).
So, rule of thumb: whenever calling any method, expect hidden costs (magnified if you’re in a loop) and work to minimize them. Call the methods outside of the loop if they are providing data that will remain the same throughout the loop, such as a count, and avoid them altogether if there’s an alternative way to iterate through the data that doesn’t implicitly involve resetting your position. Your customers will appreciate the time savings!
‘Til next time,
--Matt--*
-
Thanks to the SQL Server Team we just released a free SQL Server 2008 Express How-To-Guide Series on msdev.com. During the next weeks we’ll also integrating parts of the series into the Visual Basic Developer Center, C# Developer Center, the Beginner Developer Learning Center, etc.
One video of the series: How do I query data in a database?
Given the fact that we don’t have unlimited resources (yeah, I know… that was the biggest surprise for me as well when I started working at Microsoft. “Where are all the bags with money standing around in each corner? What? There are none? D’oh!”) we tried to cover a pretty comprehensive area starting with basics like “How do I create a database?” over “How do I query data in a database?” to more advanced features like “How do I import data from another source (Excel, Access, XML, MySQL, ...) into a SQL Server Database?”.
Please let me know how you like the selection we made and if there are other topics you’d like to see*.
Enjoy!
Daniel
P.S.: We’ll rearrange the videos soon to have a nice walkthrough including “Topics you should know before watching this video” and “Next videos you might want to watch”.
* I don’t want to raise wrong expectations in that I’m promising other videos to show up on request but depending on the topic/amount of people requesting content we might extend the series.
-
While helping some Windows API folks with some sample code this week, I stumbled upon...uh...I mean “carefully researched” an issue that you might find handy.
You may be aware that the RaiseEvent statement automatically checks to verify if an event handler exists before raising the event. If the event is Nothing, then there’s no event handler and RaiseEvent terminates. If the event is not Nothing, then RaiseEvent triggers the event.
However, what if you want to follow a different code path if the event is Nothing? Unfortunately, the RaiseEvent statement doesn’t return a value that could indicate whether the event was fired. So, what’s a developer to do?
The answer? When the VB compiler sets up your event, it creates a private variable for the event in the form of <event name>Event. So, if I set up an event named TriggerMe, the VB compiler creates an event variable named TriggerMeEvent. You can test this variable for Nothing. It’s as simple as that! (Note: You won't see this variable using Intellisense, but you can find it when debugging or through reflection.)
Here’s a quick example:
Public Event SolutionFound As EventHandler
Private Sub OnSolutionFound()
If SolutionFoundEvent IsNot Nothing Then
RaiseEvent SolutionFound(Me, New EventArgs())
Else
' No event handler has been set.
MsgBox("There is no event handler. That makes me sad.")
End If
End Sub
-
The other day I got an email from a developer asking an interesting question about VB10’s Auto-implemented Properties feature.
“I am, however, somewhat confused regarding the properties feature you mentioned - i.e. turning 11 lines of get/set/backing variable into one line. Wouldn't this be exactly the same as the existing functionality of creating a public variable? I see no benefit for using a property in this case, except perhaps for the sake of what some would consider best practice.”
On the surface there doesn’t seem to be a big difference between this:
Public Property Name() As String
And this:
Public Name() As String
Clearly both will expose an object’s state to the outside world, and can be read/modified using the exact same syntax. Which then begs the question, why would I ever use a property if I have no special logic in the Getter/Setter?
Let’s consider some reasons:
1. Fields can’t be used in Interfaces

You can’t enforce the existence of a field in an object’s public contract through an interface. For properties though it works fine.
2. Validation
While your application currently may not require any validation logic to set a particular value, changing business requirements may require inserting this logic later. At that point changing a field to a property is a breaking change for consumers of your API. (For example if someone was inspecting your class via reflection).
3. Binary Serialization
Changing a field to a property is a breaking change if you’re using binary serialization. Incidentally, this is one of the reasons VB10’s auto-implemented properties have a “bindable” backing field (i.e. you can express the name of the backing field in code) – that way, if you change an auto-implemented property to an expanded property, you can still maintain serialization compatibility by keeping the backing field name the same (in C# you’re forced to change it because it generates backing fields with unbindable names).
4. A lot of the .NET databinding infrastructure binds to properties but not fields
I’ve heard arguments on both sides as to whether or not that’s a good thing, but the reality is that’s the way it works right now.
5. Exposing a public field is an FxCop violation
For many of the reasons listed above :)
One argument I’ve heard for using fields over properties is that “fields are faster”, but for trivial properties that’s actually not true, as the CLR’s Just-In-Time (JIT) compiler will inline the property access and generate code that’s as efficient as accessing a field directly.
For these reasons and more, in most cases you’re better off using a property rather than exposing a public field. Thankfully VB10 will make this a lot easier :)
-
In my previous post, we fixed up a C# GPS library to provide more support, and wrote all of the UI for a VB GPS application, all based on (but modified from) a Mobile GPS sample in the Windows Mobile 6.0 SDK. In this post, we’ll finish up the app by enabling a bunch of cool functionality not exposed in the original sample.
The C# sample uses a helper function called UpdateData – I will too (as noted above), except that the code is going to deviate significantly. We’ll start out by checking is the GPS is actually running, just in case we called outside of an event:
Protected Sub UpdateData(ByVal sender As Object, ByVal args As System.EventArgs)
If vbgps.Opened Then
First, we’ll update the screen with whatever cached device data we have:
If device IsNot Nothing Then
Me.Text = "VBGPS: " & device.FriendlyName.ToString
Me.StatusLabel.Text = device.DeviceState.ToString
End If
This will change the title bar to include the name of the GPS device in use (in case you have multiple devices) and will set the StatusLabel to whatever the state of the GPS is (e.g., “On”).
All other interesting information comes from the cached position object:
If position IsNot Nothing Then
We’ll start with the altitude. There are two ways of calculating altitude – sea level (self-explanatory), and ellipsoid (which calculates altitude against a “perfect” ellipsoid representation of the otherwise pear-shaped earth). We’re going to go with ellipsoid. We’ll need to check if the information for it is reported as being valid, and if so, assigned the value to the appropriate label. The value is returned to us in meters, and we’ll also show it in feet by multiplying the result by 3.281 feet/meters. Again, we’ll format the results so that we only have two decimal places:
If position.EllipsoidAltitudeValid Then
Me.AltLabel.Text = CDbl(position.EllipsoidAltitude).ToString("####0.00") _
& " m (" & CDbl(position.EllipsoidAltitude * 3.281).ToString("####0.00") _
& "ft)"
End If
Next is velocity. The speed value will be in meters per second; We’ll show it in kph and mph. I’ll also show the heading (if valid), and separate the two by an “@” sign (e.g., “88.00 kph (54.69 mph) @ 45.23°”:
If position.SpeedValid Then
Me.VelLabel.Text = CDbl(position.Speed * 1.852).ToString("###0.00") _
& " kph (" & CDbl(position.Speed * 1.151).ToString("###0.00") & " mph)"
If position.HeadingValid Then
Me.VelLabel.Text = Me.VelLabel.Text & " @ " _
& position.Heading.ToString("##0.00") & "°"
End If
End If
Latitude and longitude are next. We’ll be showing them in both DMS and DM format, using the methods we wrote in C#. The only interesting thing here is converting negative values to W or S indicators:
If position.LatitudeValid Then
Me.LatLabel.Text = position.LatitudeInDegreesMinutesSeconds.DMSString() _
& " (" & position.LatitudeInDegreesMinutesSeconds.DMString & ")"
If position.Latitude < 0 Then
Me.LatLabel.Text = Me.LatLabel.Text & " S"
Else
Me.LatLabel.Text = Me.LatLabel.Text & " N"
End If
End If
If position.LongitudeValid Then
Me.LongLabel.Text = position.LongitudeInDegreesMinutesSeconds.DMSString() _
& " (" & position.LongitudeInDegreesMinutesSeconds.DMString & ")"
If position.Longitude < 0 Then
Me.LongLabel.Text = Me.LongLabel.Text & " W"
Else
Me.LongLabel.Text = Me.LongLabel.Text & " E"
End If
End If
Satellites are a bit more complicated, and if you do a straight port of the C# sample, you’ll get runtime errors. This is because the C# code references values which may be NULL even if the values are valid – my VB code checks to see if the values are Nothing before referencing them. (A value of Nothing is equivalent to zero satellites in this case, since the C# library uses the existence and size of the satellite array to generate a value.)
There are three satellite components that we need to use – the number of satellites used to create a positioning solution, the number of them in view, and the total count of satellites – the resulting string should look something like (for example) “4/7 (8)”, depending on the actual number of satellites involved.
If position.SatellitesInSolutionValid AndAlso _
position.SatellitesInViewValid AndAlso _
position.SatelliteCountValid Then
Dim SatSol As Satellite() = position.GetSatellitesInSolution()
Dim SatView As Satellite() = position.GetSatellitesInView()
Dim SatStr As String
If SatSol IsNot Nothing Then
SatStr = SatSol.Length & "/"
Else
SatStr = "0/"
End If
If SatView IsNot Nothing Then
SatStr += SatView.Length & " ("
Else
SatStr += "0 ("
End If
Me.SatLabel.Text = SatStr & position.SatelliteCount & ")"
End If
You can actually get more information on each satellite (such as its location in orbit, etc.) simply by exploring the methods and fields on the satellite object, but for now this will do fine for our project.
Now that we’ve shown the information as to where we are, we need to tell the user how to get to where they are going. Creating maps and routes is just a bit beyond the scope of this blog, but we can certainly indicate direction and distance. Let’s assume the existence of a helper function called UpdateTargetInfo at this point – we’ll define it later:
UpdateTargetInfo()
End If
End If
End Sub
Now, you’ll recall that we added fields to allow the user to specify the target point. We’ll want to translate the users entries into actual lat/long coordinates, and it would also be really cool if we could update the distance and direction in real-time. There are six fields and two combo boxes that are involved with the target, so let’s create a handler to deal with all of their change events (TextChanged for the text boxes, SelectedIndexChanged for the combo boxes):
Private Sub Target_TextChanged(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles _
LatDegTB.TextChanged, LatMinTB.TextChanged, LatSecTB.TextChanged, _
LongDegTB.TextChanged, LongMinTB.TextChanged, LongSecTB.TextChanged, _
LatPosCB.SelectedIndexChanged, LongPosCB.SelectedIndexChanged
I’m going to use a helper function to collect the data, because collecting latitude data is identical to collecting longitude data, so we might as well treat them generically. The helper method will be called “CacheCoordinate” and we’ll define it later. As arguments, we’ll pass through the relevant controls and caches, and then update the distance and direction afterwards:
CacheCoordinate(LatDegTB, LatMinTB, LatSecTB, LatPosCB, TargetLatitude)
CacheCoordinate(LongDegTB, LongMinTB, LongSecTB, LongPosCB, TargetLongitude)
UpdateTargetInfo()
Now, the user could enter all sorts of errors accidentally into the text fields, any of which could cause an exception to be thrown. I could check and recheck for text and oddly-formed numbers being inserted into those fields, but for this exercise, I’m going to go with the moral equivalent of duct tape and simply use a Try block around the code we just wrote. If there’s an error, I’ll simply tell the user that their entered target information is invalid, and so the distance and direction cannot be determined:
Try
CacheCoordinate(LatDegTB, LatMinTB, LatSecTB, LatPosCB, TargetLatitude)
CacheCoordinate(LongDegTB, LongMinTB, LongSecTB, LongPosCB, TargetLongitude)
UpdateTargetInfo()
Catch ex As Exception
Me.DistLabel.Text = "Invalid target."
Me.DirLabel.Text = "Invalid target."
TargetLatitude = Nothing
TargetLongitude = Nothing
End Try
End Sub
Now, we’ll start to parse together the data, starting with latitude. The combo box for it has two values – “N” or “E” (at the 0 position) and “S” or “W”(at the 1 position) – so a simple check on the index will tell us if we’ll be using positive or negative coordinates:
Private Sub CacheCoordinate(ByVal DegField As TextBox, ByVal MinField As TextBox, _
ByVal SecField As TextBox, ByVal PosField As ComboBox, _
ByRef cache As DegreesMinutesSeconds)
Dim isPositive As Boolean ' South and west are negative values
isPositive = (PosField.SelectedIndex = 0)
Note that the cache needs to be passed in as ByRef, since we’ll be allocating new memory around it!
So, a user might want to enter data in three ways: degrees with a decimal portion; degrees and minutes, with minutes having a decimal portion; degrees, minutes, and seconds, with seconds having a decimal portion. To help the user, we’ll hide all subsequent fields if a decimal point is used in one of the earlier fields. That is, if the user enters “47.” in the degrees field, then we know that he/she won’t need the minutes or seconds fields, so we’ll just hide them:
If DegField.Text.Contains(".") Then
MinField.Hide()
SecField.Hide()
Next, we’ll get the degrees value and makes sure that it is between 0 and 180 degrees:
Dim ddeg As Double = CDbl(DegField.Text)
If ddeg > 180 OrElse ddeg < 0 Then
Throw New Exception()
End If
Now, if the coordinate is south or west, we’ll have to multiply by -1 to indicate that:
If Not isPositive Then
ddeg *= -1.0
End If
And then finally cache the coordinate:
cache = New DegreesMinutesSeconds(ddeg)
Now, if the degrees field didn’t have a decimal, we’ll need to remember to show the minutes field in case it had been hidden earlier:
Else
MinField.Show()
We also know that degrees will be a whole number, so let’s cache that away:
Dim deg As UInteger = CUInt(DegField.Text)
The next case is the degree-minutes case – if the minutes field has a decimal, we’ll know that seconds won’t be needed, so we’ll hide it:
If MinField.Text.Contains(".") Then
SecField.Hide()
We then validate both the degrees (0…180) and the minutes (0…60), and then cache the target using the appropriate DegreesMinutesSeconds constructor that we created earlier:
Dim dmin As Double = CDbl(MinField.Text)
If deg > 180 OrElse deg < 0 _
OrElse dmin > 60 OrElse dmin < 0 Then
Throw New Exception()
End If
cache = New DegreesMinutesSeconds(isPositive, deg, dmin)
If we’ve gotten this far without finding a decimal place, then we know that we’re using all of the fields (degrees, minutes, and seconds), so we should verify that seconds is showing, do the validation, and cache the target:
SecField.Show()
Dim min As UInteger = CUInt(MinField.Text)
Dim dsec As Double = CDbl(SecField.Text)
If deg > 180 OrElse deg < 0 _
OrElse min > 180 OrElse min < 0 _
OrElse dsec > 60 OrElse dsec < 0 Then
Throw New Exception()
End If
' Cache this data for later use
cache = New DegreesMinutesSeconds(isPositive, deg, min, dsec)
End If
End If
End Sub
It’s time for us to write this UpdateTargetInfo() code. That code will be responsible for calculating both the distance and direction from our current position to our desired position. We’ll be using the Math library to do these calculations. The functions in the math library use radians, not degrees, but we can whip up a quick translation between the two:
Private Function DegToRad(ByVal deg As Double) As Double
Return (deg * Math.PI) / 180
End Function
Private Function RadToDeg(ByVal rad As Double) As Double
Return (rad * 180) / Math.PI
End Function
To calculate distance, we’ll be using the Haversine formula that we all learned in high-school trigonometry. I refreshed my memory on this formula (and the direction formula) at http://www.movable-type.co.uk/scripts/latlong.html. These formulae do calculations based on a great-circle, which (if you’ll recall) is actually the path to follow for the shortest distance between two points on a sphere. (Thus, if your destination is on the same line of latitude as you, the direction you want to go will not actually be due west or east unless you are at the equator.)
Assuming our current position is valid, we’ll go ahead & get out starting point, as well as define the radius of the earth:
Private Sub UpdateTargetInfo()
If position IsNot Nothing AndAlso position.LatitudeValid AndAlso _
position.LongitudeValid AndAlso _
TargetLatitude IsNot Nothing AndAlso TargetLongitude IsNot Nothing Then
Dim radius As Double = 6371 ' kilometers
Dim rlat1 As Double = DegToRad(position.Latitude)
Dim rlat2 As Double = DegToRad(TargetLatitude.ToDecimalDegrees)
Dim rlong1 As Double = DegToRad(position.Longitude)
Dim rlong2 As Double = DegToRad(TargetLongitude.ToDecimalDegrees)
We’ll also get the difference in latitude and longitude handy:
Dim rdeltaLat As Double = rlat2 - rlat1
Dim rdeltaLong As Double = rlong2 - rlong1
Now, we can calculate distance based on Haversine:
Dim a As Double = Math.Pow(Math.Sin(rdeltaLat / 2), 2) + _
Math.Cos(rlat1) * Math.Cos(rlat2) * Math.Pow(Math.Sin(rdeltaLong / 2), 2)
Dim c As Double = 2.0 * Math.Atan2(Math.Sqrt(a), Math.Sqrt(1 - a))
Dim distance As Double = radius * c
[Edit 8/7/09: Fixed typo in calculation of a -- used rlong2 instead of rlat2 accidentally. Will repost on Temple of VB site.]
The distance is described in kilometers at this point. If we’re under one kilometer, it’d be easier for the user if we switch to meters instead – and of course, we’d like to show miles and feet as well, all formatted nicely using ToString formatting:
If distance > 1.0 Then
Me.DistLabel.Text = distance.ToString("####0.00") & " km (" & CDbl(distance * 0.621).ToString("####0.00") & "mi)"
Else
Me.DistLabel.Text = (distance * 1000.0).ToString("####0.00") & " m (" & CDbl(distance * 3280.84).ToString("####0.00") & "ft)"
End If
Next is direction:
Dim theta As Double
theta = Math.Atan2(Math.Sin(rdeltaLong) * Math.Cos(rlat2), _
Math.Cos(rlat1) * Math.Sin(rlat2) - _
Math.Sin(rlat1) * Math.Cos(rlat2) * Math.Cos(rdeltaLong))
Dim degTheta As Double = (RadToDeg(theta) + 360.0) Mod 360
Me.DirLabel.Text = degTheta.ToString("##0.00") & "°"
And of course if the position information isn’t valid, then we need to tell the user:
Else
Me.DistLabel.Text = "No GPS data available."
Me.DirLabel.Text = "No GPS data available."
End If
End Sub
And that’s it! Debugging it is a bit tricky, since you have to take it outside to try it out (I was basically running in and out of my house to try out new ideas I’d just coded), but it’s fully functional and would be useful for (for instance) geocaching or surveying. Going forward, I’ll probably add more functional to mark areas that I was interested in persisting – once I get some more free time. J The current version’s code is available at my Temple of VB site – it also includes a setup project for CABbing up the file, as well as a console application to test out the direction/distance functionality. Enjoy!
‘Til next time,
--Matt--*