Linking Zune media items with LinQ, Part 2 (Matt Gertz)

Linking Zune media items with LinQ, Part 2 (Matt Gertz)

  • Comments 2

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--*

Leave a Comment
  • Please add 4 and 8 and type the answer here:
  • Post
  • it is good !

    welcome to the web www.jersey21.com

    and the blog of www.jersey21.com/bolg

  • I was looking for information about how long a large old xml full address ... thanks

Page 1 of 1 (2 items)