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:

PrivateSub ShuffleListBox_DrawItem(ByVal sender AsObject, _

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:

Finally, regardless of what we drew, we’ll need to draw the focus rectangle (and dotted line around any selections:

e.DrawFocusRectangle()

EndSub

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:

PrivateSub LinkBtn_Click(ByVal sender As System.Object, _

ByVal e As System.EventArgs) Handles LinkBtn.Click

Dim numToLink AsInteger = ShuffleListBox.SelectedItems.Count

Then, we need to iterate through each of those and link them.:

For index AsInteger = 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:

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()

EndSub

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:

PrivateSub UnlinkBtn_Click(ByVal sender As System.Object, _

ByVal e As System.EventArgs) Handles UnlinkBtn.Click

ForEach item As xmlMediaEntry In ShuffleListBox.SelectedItems

If item.PrevLinkedElement IsNotNothing _

OrElse item.NextLinkedElement IsNotNothingThen

FlushLinkSetFromBeginningToEnd(item)

EndIf

Next

ShuffleListBox.Invalidate()

ShuffleListBox.Update()

FileChanged = True

ResetMenus()

ResetButtons()

EndSub

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):

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:

PrivateSub MoveEntry(ByVal direction AsInteger)

If direction <> 1 AndAlso direction <> -1 ThenReturn

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:

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:

PrivateSub Shuffle_Click(ByVal sender As System.Object, _

ByVal e As System.EventArgs) Handles ShuffleBtn.Click

Dim newList AsNew List(Of xmlMediaEntry)

Dim numberOfSongs AsInteger = 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 AsInteger = 0 To numberOfSongs - 1

Dim mediaItem As xmlMediaEntry = ShuffleListBox.Items(index)

If mediaItem.NextLinkedElement IsNotNothingThen

Dim placeToInsert AsInteger = FindInsertionLocation(newList)

Dim count = 0

Do

Insert(placeToInsert + count, mediaItem, newList)

count += 1

mediaItem = mediaItem.NextLinkedElement

LoopWhile mediaItem IsNotNothing

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)

EndIf

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()

ForEach m In newList

ShuffleListBox.Items.Add(m)

Next

' This is a playlist change, so react accordingly:

FileChanged = True

ResetMenus()

ResetButtons()

EndSub

(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:

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.

Now, in EmitXML, we’ll add the following code at the end of the method:

Dim hiddenLinkInfo = <?xmlversion="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:

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:

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 AsString = 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.Ifwe can’t find a particular song, we just skip it and work with whatever we’ve got.

(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

EndTry

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):

PrivateSub Link(ByVal linkList As List(OfInteger))

Dim numToLink AsInteger = linkList.Count

For index AsInteger = 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)

EndIf

Next

ShuffleListBox.Invalidate()

ShuffleListBox.Update()

EndSub

And that’s it!As usual, the full code can be found on my Temple of VB site.