Windows 8 Metro Style– Bits of Binding

Data binding is a useful tool in putting an application together and I’ve spent quite a long time with various frameworks that support different aspects of binding. In the last few years, that has mainly revolved around the binding support that you find in XAML based applications for WPF, Silverlight and Silverlight for Windows Phone 7.

Recently I put together that simple example of a music application and I thought I’d revisit that with a view to taking a look at how that might be data-bound for both a XAML based app and a HTML based app. I don’t claim that this is complete or definitive or even correct – I’m just some guy on the internet.

Also, I’m not going to put together exactly the same app as in that previous video but it’ll be along the same lines.

I’m going to take the view that a user wants to be presented with a list of music albums and then selects an album to play a track. That’s a slightly false scenario because a lot of users would want to browse by artist and genre but it’s just an example.

The other thing that I’d say about what I do below is that it pays no real heed to performance. The way I’m working is probably a bad idea as it’s going to load a lot of data up front. There are other ways of doing this – I’m just exploring binding in this post.

Step 1 – Get a Set of Album Data

Let’s say that I want to get hold of a set of album data and display it in a control such as a ListView. This involves talking to the Windows Storage system and using the Music Library and so I’m going to set up a simple, blank project using both HTML/JS and XAML/C# and I’m going to set up the capabilities to allow me access to the music library.

image

XAML/C#

And then I can add a little UI so in the XAML world I might define a ListView in my BlankPage.xaml as;

  <Grid Background="{StaticResource ApplicationPageBackgroundBrush}">
    <ListView ItemsSource="{Binding Albums}" />
  </Grid>

and then I might initially write a class like the one below ( note – you can argue about whether I should really do a fire-and-forget call to PopulateAsync inside of that constructor, I’m with you – it feels a little odd but I’ll go with it for now );

  using System;
  using System.Collections.Generic;
  using System.Threading.Tasks;
  using Windows.Storage;
  using Windows.Storage.BulkAccess;
  using Windows.Storage.Search;

  public class AlbumData
  {
    public FolderInformation FolderInformation { get; set; }
  }
  public class MusicData
  {
    public MusicData()
    {
      this.PopulateAsync();
    }
    public List<AlbumData> Albums
    {
      get
      {
        return (this.albums);
      }
      set
      {
        this.albums = value;
      }
    }
    List<AlbumData> albums;

    async void PopulateAsync()
    {
      StorageFolderQueryResult query = KnownFolders.MusicLibrary.CreateFolderQuery(
        CommonFolderQuery.GroupByAlbum);

      FileInformationFactory factory = new FileInformationFactory(query,
        Windows.Storage.FileProperties.ThumbnailMode.MusicView,
        250,
        Windows.Storage.FileProperties.ThumbnailOptions.ResizeThumbnail);

      IReadOnlyList<FolderInformation> folderInfo = await factory.GetFoldersAsync();

      this.Albums = new List<AlbumData>();

      foreach (FolderInformation entry in folderInfo)
      {
        this.Albums.Add(new AlbumData()
        {
          FolderInformation = entry
        });
      }
    }
  }

And inside of my BlankPage.xaml.cs I could go and set up the DataContext on the Page itself which the ListView will locate because it has been told to find its ItemsSource via binding and that means searching up the hierarchy until it finds a DataContext that it can attempt to bind to;

    public sealed partial class BlankPage : Page
    {
        public BlankPage()
        {
            this.InitializeComponent();
        }
        protected override void OnNavigatedTo(NavigationEventArgs e)
        {
          this.DataContext = new MusicData();
        }
    }

Now when it comes to try and execute that code, it’s not going to work. There are a number of problems including;

  1. We bind to the Albums property but we then later re-assign it and there’s no way for the UI to know that we change it at a later point. We need INotifyPropertyChanged to fire an event when the property changes so that the UI control stands a chance.
  2. We dynamically build up a List<AlbumData> and there’s no way for the UI to know when items are being added to that changing list. We need INotifyCollectionChanged for the UI to understand that and sync those events.

In the XAML project templates for Visual studio, there’s a class called BindableBase which does the INotifyPropertyChanged work for us so I’ll use that here and, specifically, it provides a method SetProperty to do the work of changing the property value while firing a change notification (you should take a look at the CallerMemberNameAttribute that it uses to understand it better).

V2 of the class that takes that on board;

  using System;
  using System.Collections.Generic;
  using System.Collections.ObjectModel;
  using BlogPostCS.Common;
  using Windows.Storage;
  using Windows.Storage.BulkAccess;
  using Windows.Storage.Search;

  public class AlbumData
  {
    public FolderInformation FolderInformation { get; set; }
  }
  public class MusicData : BindableBase
  {
    public MusicData()
    {
      this.PopulateAsync();
    }
    public ObservableCollection<AlbumData> Albums
    {
      get
      {
        return (this.albums);
      }
      set
      {
        base.SetProperty(ref this.albums, value);
      }
    }
    ObservableCollection<AlbumData> albums;

    async void PopulateAsync()
    {
      StorageFolderQueryResult query = KnownFolders.MusicLibrary.CreateFolderQuery(
        CommonFolderQuery.GroupByAlbum);

      FileInformationFactory factory = new FileInformationFactory(query,
        Windows.Storage.FileProperties.ThumbnailMode.MusicView,
        250,
        Windows.Storage.FileProperties.ThumbnailOptions.ResizeThumbnail);

      IReadOnlyList<FolderInformation> folderInfo = await factory.GetFoldersAsync();

      this.Albums = new ObservableCollection<AlbumData>();

      foreach (FolderInformation entry in folderInfo)
      {
        this.Albums.Add(new AlbumData()
        {
          FolderInformation = entry
        });
      }
    }
  }

And if I run that code then the ListView will display a view of the albums albeit with nothing interesting in that view because we don’t have a decent ItemTemplate in place but I can return to that.

HTML/JS

Following similar steps in the HTML/JS world, I can go and set up a ListView control by doing;

  <div data-win-control="WinJS.UI.ListView"
    data-win-bind="winControl.itemDataSource: albums"
    data-win-otions="{ layout: { type : WinJS.UI.ListLayout } }">
  </div>

And then I might try and set up the data context in my default.js file;

            var promise = WinJS.UI.processAll();

            promise.done(
                function ()
                {
                    var dataContext = new Music.MusicData();
                    return (WinJS.Binding.processAll(document.body, dataContext));
                }
            );

and write a music data class similar to the one that I wrote in C# and, in a similar way, I might not quite get it working on my first attempt;

(function ()
{
    "use strict";

    var albumData = WinJS.Class.define(
        function ()
        {
        },
        {
            folderInformation: null
        }
    );
    var musicData = WinJS.Class.define(
        function ()
        {
            this.populateAsync();
        },
        {
            albums: null,
            populateAsync: function ()
            {
                var wRT = {
                    stg: Windows.Storage,
                    blk: Windows.Storage.BulkAccess,
                    sch: Windows.Storage.Search
                };

                var that = this;

                var query = wRT.stg.KnownFolders.musicLibrary.createFolderQuery(
                    wRT.sch.CommonFolderQuery.groupByAlbum);

                var factory = new wRT.blk.FileInformationFactory(
                    query,
                    wRT.stg.FileProperties.ThumbnailMode.musicView,
                    250,
                    wRT.stg.FileProperties.ThumbnailOptions.resizeThumbnail);

                var promise = factory.getFoldersAsync();

                promise.done(
                        function (folders)
                        {
                            that.albums = [];

                            folders.forEach(
                                function (folder)
                                {
                                    var albumInfo = new albumData();
                                    albumInfo.folderInformation = folder;
                                    that.albums.push(albumInfo);
                                }
                            );
                        }
                    );

            }
        }
    );

    WinJS.Namespace.define("Music",
        {
            MusicData: musicData,
            AlbumData: albumData
        }
    );
})();

This one causes me a whole host of problems and, in fact, runtime exceptions. The first of those comes because I am trying to bind my ListView property called itemDataSource to my MusicData.albums property and ListView.itemDataSource actually expects to be provided with an IListDataSource and I’m simply trying to provide it with an array.

Note also that I might not even provide it with an array. There looks to be a race condition between databinding trying to resolve the property albums and the asynchronous code that tries to set that property to a value. Because there’s no change notification on the property albums it’s possible/likely that the value null is returned for the albums property before it later gets set to a new array instance.

If you take a look at IListDataSource then you’ll notice it has a method called createListBinding and that returns an IListBinding and optionally takes an IListNotificationHandler that can be used to receive change notifications around the list. It feels like a very similar thing to ObservableCollection in the .NET world.

For an array based data-source like I have, I can use a WinJS.Binding.List to give me that IListDataSource although for real-world purposes a StorageDataSource might make more sense but I’ll leave that outside of the scope of this post.

Now, what about my albums property that doesn’t support change notification? How do I set up that property in order to do change notification? What’s the equivalent of INotifyPropertyChanged?

I think it’s represented by 2 of the methods over here – i.e. bind and unbind in the sense that if I want to observe a property foo of an object someObject then I can call;

someObject.bind(“foo”, function() { });

and my function will get called (as per the caveats in that doc link) when the value changes. When I am bored with the change notifications, I can call unbind in a similar way to take them away.

This is similar to INotifyPropertyChanged in the .NET world.

That observableMixin comes with another function notify and the three work together in order to make me an observable property. I can mix this into my class definition using WinJS.Class.mix giving me;

(function ()
{
    "use strict";

    var albumData = WinJS.Class.mix(
        WinJS.Class.define(
            function ()
            {
            },
            {
                folderInformation:
                {
                    get: function ()
                    {
                        return (this._folderInformation);
                    },
                    set: function (value)
                    {
                        var oldValue = this._folderInformation;
                        this._folderInformation = value;
                        this.notify("folderInformation", value, oldValue);
                    }
                },
                _folderInformation: null
            }
        ),
        WinJS.Binding.observableMixin
    );

    var musicData = WinJS.Class.mix(
        WinJS.Class.define(
            function ()
            {
                this.albums = new WinJS.Binding.List([], { binding: false });
                this.populateAsync();
            },
            {
                albums:
                {
                    get: function ()
                    {
                        return (this._albums);
                    },
                    set: function (value)
                    {
                        var oldValue = this._albums;
                        this._albums = value;
                        this.notify("albums", value, oldValue);
                    }
                },
                _albums: null,
                populateAsync: function ()
                {
                    var wRT = {
                        stg: Windows.Storage,
                        blk: Windows.Storage.BulkAccess,
                        sch: Windows.Storage.Search
                    };
                    var that = this;

                    var query = wRT.stg.KnownFolders.musicLibrary.createFolderQuery(
                        wRT.sch.CommonFolderQuery.groupByAlbum);

                    var factory = new wRT.blk.FileInformationFactory(
                        query,
                        wRT.stg.FileProperties.ThumbnailMode.musicView,
                        250,
                        wRT.stg.FileProperties.ThumbnailOptions.resizeThumbnail);

                    var promise = factory.getFoldersAsync();
                    promise.done(
                        function (folders)
                        {
                            folders.forEach(
                                function (folder)
                                {
                                    var albumInfo = new albumData();
                                    albumInfo.folderInformation = folder;
                                    that.albums.push(albumInfo);
                                }
                            );
                        }
                    );
                }
            }
        ),
        WinJS.Binding.observableMixin
    );

    WinJS.Namespace.define("Music",
        {
            MusicData: musicData,
            AlbumData: albumData
        }
    );
})();

Note that this is one way of doing this – there are others. They may involve less code. With small but important tweaks to my UI binding definition;

  <div     
    data-win-control="WinJS.UI.ListView"
    data-win-bind="winControl.itemDataSource: albums.dataSource WinJS.Binding.oneTime"
    data-win-options="{ layout: { type : WinJS.UI.ListLayout } }">
  </div>

I get to more or less the same place as my XAML project has got to.

Now, the sharp-eyed among you might have noticed that I just cheated.

How? In the data-win-bind above I try and bind the winControl.itemDataSource to my property called albums.dataSource. I went to some length to try and support change notification around my albums property but if you look carefully I then use the converter WinJS.Binding.oneTime which means “don’t bother with change notification”.

This is because, currently, the dataSource property of the WinJS.Binding.List doesn’t play nicely with change notification. Consequently, even though my albums property is trying to do the right thing, the sub-property dataSource won’t and so I’ve had to use WinJS.Binding.oneTime and you’ll also notice that my code above makes sure to set the value of albums to an empty WinJS.Binding.List before it really needs to such the value of the property never actually changes.

As far as I know, this behaviour with dataSource is a bug and so I’m just trying to work around that here in the preview.

The other thing you might have noticed is that when I construct my WinJS.Binding.List, I pass an option to it of;

{ binding: false }

What does that do? If this is set to true then the list will call WinJS.Binding.as() on each of the elements in the list but I don’t think I need the list to do that because my elements will (when I get to write them) contain values that do change notification themselves rather than needing WinJS.Binding.as() to do it for me. With that said, I don’t think there’s any actual harm that would be called by doing this because that routine is smart enough to spot that I have the bits in place for change notification.

As an aside, WinJS.Binding.oneTime is a converter. There are some more converters to come in a moment…

Step 2 – Display Albums

Ok, having started to get some data it might be nice to display something. I’ll make it simple and display the album’s title, artist name and the album cover and I’ll do this in a basic grid layout.

XAML/C#

For now, let’s put the ListView into the first column of a grid and display those basic properties – here’s the UI I came to for the moment;

<Page x:Class="BlogPostCS.BlankPage"
      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      xmlns:local="using:BlogPostCS"
      xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
      xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
      mc:Ignorable="d"
      xmlns:utils="using:BlogPostCS.Utilities">
  <Page.Resources>
    <utils:ImageConverter x:Key="imageConverter" />
  </Page.Resources>
  <Grid Background="{StaticResource ApplicationPageBackgroundBrush}">
    <Grid.ColumnDefinitions>
      <ColumnDefinition></ColumnDefinition>
      <ColumnDefinition></ColumnDefinition>
    </Grid.ColumnDefinitions>
    <ListView ItemsSource="{Binding Albums}">
      <ListView.ItemTemplate>
        <DataTemplate>
          <Grid>
            <Grid.ColumnDefinitions>
              <ColumnDefinition Width="Auto" />
              <ColumnDefinition Width="Auto" />
            </Grid.ColumnDefinitions>
            <Image Grid.Row="0"
                   Height="320"
                   Grid.RowSpan="2"
                   Source="{Binding Cover,Converter={StaticResource imageConverter}}" />
            <StackPanel Grid.Column="1">
              <TextBlock Text="{Binding Title}"
                         Style="{StaticResource HeaderTextStyle}" />
              <TextBlock Text="{Binding Artist}"
                         Style="{StaticResource SubheaderTextStyle}" />
            </StackPanel>
          </Grid>
        </DataTemplate>
      </ListView.ItemTemplate>
    </ListView>
  </Grid>
</Page>

Which display very simply as;

image

There’s a couple of “interesting” things that went on here. I am binding to properties Title, Artist and Cover but if you were following along you’d know that the actual data class for the item in the FlipView is an AlbumData which only had one property on it called FolderInformation which was of a WinRT type called FolderInformation.

Why didn’t I just bind into the properties on that type like FolderInformation.MusicProperties.Album? Because, as far as I know, that won’t work. What I did instead was this;

  public class AlbumData : BindableBase
  {
    public string Title
    {
      get
      {
        return (this.FolderInformation.MusicProperties.Album);
      }
    }
    public string Artist
    {
      get
      {
        return (this.FolderInformation.MusicProperties.Artist);
      }
    }
    public StorageItemThumbnail Cover
    {
      get
      {
        return (this.FolderInformation.Thumbnail);
      }
    }
    public FolderInformation FolderInformation
    {
      get
      {
        return (this.folderInformation);
      }
      set
      {
        this.folderInformation = value;
        base.OnPropertyChanged("Title");
        base.OnPropertyChanged("Artist");
        base.OnPropertyChanged("Cover");
      }
    }
    FolderInformation folderInformation;
  }

So I made accessors on my AlbumData which reach into the underlying FolderInformation instance and that works fine. Note that there’s no actual change notification happening here as the value FolderInformation is only ever set once at the moment by my code and that’s more or less at construction time (before binding gets hold of it anyway).

The other thing that’s going on is that my Cover property is using the StorageItemThumbnail which WinRT returns to me but I can’t naturally bind that to an Image.Source property in my UI so I wrote a converter as below;

class ImageConverter : IValueConverter
  {
    public object Convert(object value, Type targetType, object parameter, string language)
    {
      if (value.GetType() != typeof(StorageItemThumbnail))
      {
        throw new ArgumentException("Expected a thumbnail");
      }
      if (targetType != typeof(ImageSource))
      {
        throw new ArgumentException("What are you trying to convert to here?");
      }
      StorageItemThumbnail thumbnail = (StorageItemThumbnail)value;
      BitmapImage image = new BitmapImage();
      image.SetSource(thumbnail);
      return (image);
    }

    public object ConvertBack(object value, Type targetType, object parameter, string language)
    {
      throw new NotImplementedException();
    }
  }

And that lets me get away with the binding.

HTML/JS

What about achieving the same thing in my HTML/JS project? The UI that I came up with is;

<body>
  <div id="albumTemplate" data-win-control="WinJS.Binding.Template">
    <div class="albumContainer">
      <div class="albumDetailsContainer">
        <h1 class="albumTitle" data-win-bind="textContent: folderInformation.musicProperties.album WinJS.Binding.oneTime">
        </h1>
        <h2 class="albumArtist" data-win-bind="textContent: folderInformation.musicProperties.artist WinJS.Binding.oneTime">
        </h2>
      </div>
      <img class="albumImage" data-win-bind="src: folderInformation.thumbnail Utility.imageConverter" />
    </div>
  </div>
  <div id="listView" data-win-control="WinJS.UI.ListView" data-win-bind="winControl.itemDataSource: albums.dataSource WinJS.Binding.oneTime"
    data-win-options="{ layout: { type : WinJS.UI.ListLayout }, itemTemplate : albumTemplate }">
  </div>
</body>

And the basic additions there are the itemTemplate addition to the ListView to make use of the template that I have called albumTemplate.

Unlike the XAML case, I’ve pretty much bound directly to the sub-properties of my data object’s property called folderInformation. That is, I’ve directly bound to those properties by reaching into folderInformation.musicProperties.album and folderInformation.musicProperties.artist and so on.

However, you might notice that I’ve again used WinJS.Binding.oneTime as a converter and that’s because as far as I know those WinRT objects that I’m binding to do not support change notification and, as it happens, I don’t need them to because I don’t expect the artist or album to change.

Beyond that, the binding on the img is interesting because it binds the src of the image to the folderInformation.thumbnail property which is of WinRT type StorageItemThumbnail.

There’s no natural binding to be made here and so I introduced a converter. In the WinJS world a converter like this is a function that takes the form;

function(sourceObject, sourceProperties, destinationObject, destinationProperties)

where sourceProperties and destinationProperties are arrays such that if the binding was something like;

“winControl.itemDataSource: myProperty.dataSource”

then sourceProperties = [ “winControl”, “itemDataSource” ] and destinationProperties = [ “myProperty”, “dataSource”].

There’s an easier way of creating a converter than to write a function that takes all these parameters. You can just call WinJS.Binding.converter() and pass a function that takes a single argument and performs the conversion on it.

WinJS.Binding.oneTime() is itself a converter and in my particular case to get the conversion that I wanted I did something similar to what that function does and wrote my own Utility.imageConverter as below;

(function ()
{
    function walkObjectProperties(root, properties)
    {
        for (i = 0, len = (properties.length - 1) ; i < len; i++)
        {
            root = root[properties[i]];
        }
        return (
            {
                obj : root,
                prop: properties[properties.length - 1]
            }
        );
    }
    function oneTimeConverter(converterFunction)
    {
        return (
            function (source, sourceProperties, dest, destProperties)
            {
                var s = walkObjectProperties(source, sourceProperties);
                var d = walkObjectProperties(dest, destProperties);

                d.obj[d.prop] = converterFunction(s.obj[s.prop]);

                return ({
                    cancel: function () { }
                });
            }
        );
    };

    WinJS.Namespace.define("Utility",
        {
            imageConverter: oneTimeConverter(
                function(thumbnail)
                {
                    return (URL.createObjectURL(thumbnail));
                }
            )
        }
    );
})();

With that in place, I “just” need a little styling on the UI in my CSS;

body {
    display: -ms-grid;
    -ms-grid-columns: 1fr 1fr;
    -ms-grid-rows: 1fr;
}
#listView {    
    -ms-grid-column: 1;    
    height: 100%;
}
.albumContainer {
    display: -ms-grid;
    -ms-grid-rows: auto;
    -ms-grid-columns: auto 1fr;
}
.albumDetailsContainer {
    display: -ms-box;
    -ms-box-orient: vertical;
    -ms-grid-column: 2;
}
.albumImage {   
    width: 320px;
    -ms-grid-row-span: 2;
}

To end up in the same place as my XAML based app;

image

Step 3 – Display Tracks

At the moment, my “data model” doesn’t contain the data to support track data and so it’s necessary to do a little work to actually get hold of that data. I created a new TrackData class which follows the pattern I used for my AlbumData class in that it wraps up a WinRT FileInformation class and then pulls properties from it;

  public class TrackData : BindableBase
  {
    public StorageItemThumbnail Cover
    {
      get
      {
        return (this.fileInformation.Thumbnail);
      }
    }
    public string Title
    {
      get
      {
        return (this.fileInformation.MusicProperties.Title);
      }
    }
    public uint TrackNumber
    {
      get
      {
        return (this.fileInformation.MusicProperties.TrackNumber);
      }
    }
    public TimeSpan Duration
    {
      get
      {
        return (this.fileInformation.MusicProperties.Duration);
      }
    }
    public uint BitrateKbps
    {
      get
      {
        return (this.fileInformation.MusicProperties.Bitrate / 1000);
      }
    }
    public FileInformation FileInformation
    {
      get
      {
        return (this.fileInformation);
      }
      set
      {
        this.fileInformation = value;
        base.OnPropertyChanged("Title");
        base.OnPropertyChanged("TrackNumber");
        base.OnPropertyChanged("Duration");
        base.OnPropertyChanged("BitrateKbps");
      }
    }
    FileInformation fileInformation;
  }

and then I need to modify my AlbumData class so that it maintains some collection of TrackData – this is the delta;

public class AlbumData : BindableBase
  {
    public AlbumData()
    {
      this.Tracks = new ObservableCollection<TrackData>();
    }
    public ObservableCollection<TrackData> Tracks
    {
      get
      {
        return (this.tracks);
      }
      set
      {
        this.tracks = value;
        base.OnPropertyChanged("Tracks");
      }
    }
    ObservableCollection<TrackData> tracks;

And then I need code to ensure that this collection is populated. I already had code which requested albums from WinRT and enumerated them and I added to it a little to populate the Tracks collection;

this.Albums = new ObservableCollection<AlbumData>();

      foreach (FolderInformation albumEntry in albumFolder)
      {
        if (!string.IsNullOrEmpty(albumEntry.MusicProperties.Album))
        {
          AlbumData album = new AlbumData()
          {
            FolderInformation = albumEntry
          };
          this.Albums.Add(album);

          QueryOptions options = new QueryOptions(CommonFileQuery.OrderByMusicProperties, new string[] { ".mp3" });

          StorageFileQueryResult trackQuery = albumEntry.CreateFileQueryWithOptions(options);

          FileInformationFactory trackFactory = new FileInformationFactory(trackQuery, ThumbnailMode.MusicView);

          IReadOnlyList<FileInformation> tracks = await trackFactory.GetFilesAsync();

          foreach (FileInformation track in tracks)
          {
            album.Tracks.Add(new TrackData()
            {
              FileInformation = track
            });
          }
        }
      }

I also added to my MusicData class the notion of a SelectedAlbum;

  public class MusicData : BindableBase
  {
    public MusicData()
    {
      this.PopulateAsync();
    }
    public AlbumData SelectedAlbum
    {
      get
      {
        return (this.selectedAlbum);
      }
      set
      {
        base.SetProperty(ref this.selectedAlbum, value);
      }
    }
    AlbumData selectedAlbum;

And with that in place, I can wire up my UI by adding another ListView into a second grid column to display the SelectedAlbum.Tracks and make sure that the first ListView binds its SelectedItem to the SelectedAlbum property in a 2-way manner;

<Page x:Class="BlogPostCS.BlankPage"
      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      xmlns:local="using:BlogPostCS"
      xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
      xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
      mc:Ignorable="d"
      xmlns:utils="using:BlogPostCS.Utilities">
  <Page.Resources>
    <utils:ImageConverter x:Key="imageConverter" />
    <utils:StringFormatConverter x:Key="stringFormatConverter" />
  </Page.Resources>
  <Grid Background="{StaticResource ApplicationPageBackgroundBrush}">
    <Grid.ColumnDefinitions>
      <ColumnDefinition></ColumnDefinition>
      <ColumnDefinition></ColumnDefinition>
    </Grid.ColumnDefinitions>
    <ListView ItemsSource="{Binding Albums}"
              SelectedItem="{Binding SelectedAlbum,Mode=TwoWay}"
              SelectionMode="Single">
      <ListView.ItemTemplate>
        <DataTemplate>
          <Grid>
            <Grid.ColumnDefinitions>
              <ColumnDefinition Width="Auto" />
              <ColumnDefinition Width="Auto" />
            </Grid.ColumnDefinitions>
            <Image Grid.Row="0"
                   Height="320"
                   Grid.RowSpan="2"
                   Source="{Binding Cover,Converter={StaticResource imageConverter}}" />
            <StackPanel Grid.Column="1">
              <TextBlock Text="{Binding Title}"
                         Style="{StaticResource HeaderTextStyle}" />
              <TextBlock Text="{Binding Artist}"
                         Style="{StaticResource SubheaderTextStyle}" />
            </StackPanel>
          </Grid>
        </DataTemplate>
      </ListView.ItemTemplate>
    </ListView>
    <ListView Grid.Column="1"
              ItemsSource="{Binding SelectedAlbum.Tracks}">
      <ListView.ItemTemplate>
        <DataTemplate>
          <Grid Margin="6">
            <Grid.ColumnDefinitions>
              <ColumnDefinition Width="Auto" />
              <ColumnDefinition Width="*" />
            </Grid.ColumnDefinitions>
            <Grid Background="{StaticResource ListViewItemPlaceholderRectBrush}"
                    Width="48"
                    Height="48">
              <TextBlock Text="{Binding TrackNumber}"
                         VerticalAlignment="Top"
                         HorizontalAlignment="Left"
                         FontSize="24"
                         Foreground="White" />
            </Grid>
            <StackPanel Grid.Column="1"
                        VerticalAlignment="Top"
                        Margin="10,0,0,0">
              <TextBlock Text="{Binding Title}"
                         Style="{StaticResource SubheaderTextStyle}"
                         TextWrapping="NoWrap" />
              <TextBlock Text="{Binding BitrateKbps, Converter={StaticResource stringFormatConverter},
                          ConverterParameter='\{0\} kbps'}"
                         Style="{StaticResource CaptionTextStyle}" />
              <TextBlock Text="{Binding Duration, Converter={StaticResource stringFormatConverter}, 
                            ConverterParameter='\{0:mm\}m \{0:ss\}s'}"
                         Style="{StaticResource BodyTextStyle}"/>
            </StackPanel>
          </Grid>
        </DataTemplate>
      </ListView.ItemTemplate>
    </ListView>
  </Grid>
</Page>

If you were following along closely, you’d notice that another converter has crept in. I wanted to format the Duration and BitrateKbps in a particular way for display and the binding here lacks the StringFormat option that you might find in other XAML frameworks. So, I built a simple converter to do the part that I needed;

class StringFormatConverter : IValueConverter
  {
    public object Convert(object value, Type targetType, object parameter, string language)
    {
      string result = string.Empty;

      if (parameter == null)
      {
        throw new ArgumentException("Need a parameter to pass to String.Format");
      }
      if (targetType != typeof(string))
      {
        throw new ArgumentException("String.Format returns a string");
      }
      if (value != null)
      {
        result = string.Format((string)parameter, value);
      }
      return (result);
    }
    public object ConvertBack(object value, Type targetType, object parameter, string language)
    {
      throw new NotImplementedException();
    }
  } 

And that leaves me;

image

HTML/JS

In the JS project, I need to do similar work to make sure that I’ve got the right data classes populated. Firstly, I made a new trackData “class”;

    var trackData = WinJS.Class.mix(
        WinJS.Class.define(
            function ()
            {
            },
            {
                fileInformation:
                {
                    get: function ()
                    {
                        return (this._fileInformation);
                    },
                    set: function (value)
                    {
                        var oldValue = this._fileInformation;
                        this._fileInformation = value;
                        this.notify("fileInformation", value, oldValue);
                    }
                },
                _fileInformation: null
            }
        ),
        WinJS.Binding.observableMixin);

And modified my albumData class to include a list of those things;

    var albumData = WinJS.Class.mix(
        WinJS.Class.define(
            function ()
            {
                this.tracks = new WinJS.Binding.List([], { binding: false });
            },
            {
                folderInformation:
                {
                    get: function ()
                    {
                        return (this._folderInformation);
                    },
                    set: function (value)
                    {
                        var oldValue = this._folderInformation;
                        this._folderInformation = value;
                        this.notify("folderInformation", value, oldValue);
                    }
                },
                _folderInformation: null,
                tracks :
                {
                    get: function ()
                    {
                        return (this._tracks);
                    },
                    set: function (value)
                    {
                        var oldValue = this._tracks;
                        this._tracks = value;
                        this.notify("tracks", value, oldValue);
                    }
                },
                _tracks: null                    
            }
        ),
        WinJS.Binding.observableMixin
    );

And modified my code to populate a list of those things. I already had a loop which populated the album data and so I just added to that a little;

folders.forEach(
	function (folder)
        {
        	var albumInfo = new albumData();
                albumInfo.folderInformation = folder;

		if (folder.thumbnail)
		{
			that.albums.push(albumInfo);

			var trackQuery = folder.createFileQuery(wRT.sch.CommonFileQuery.orderByMusicProperties);
			var trackFactory = new wRT.blk.FileInformationFactory(trackQuery,
				wRT.stg.FileProperties.ThumbnailMode.musicView);

			trackFactory.getFilesAsync().done(
				function (tracks)
				{
					tracks.forEach(
						function (track)
						{
							var trackInfo = new trackData();
                                                        trackInfo.fileInformation = track;
                                                        albumInfo.tracks.push(trackInfo);
						}
					);
				}
			);
		}
	}
);

And I added to my musicData class a SelectedAlbum property;

selectedAlbum : 
                {
                    get: function ()
                    {
                        if (!this._selectedAlbum)
                        {
                            return ({
                                tracks : new WinJS.Binding.List([])
                            });
                        }
                        return (this._selectedAlbum);
                    },
                    set: function (value)
                    {
                        var oldValue = this._selectedAlbum;
                        this._selectedAlbum = value;
                        this.notify("selectedAlbum", value, oldValue);
                    }
                },
                _selectedAlbum : null,

Note that this is a little weird. It starts off life as null in the prototype but this causes me a problem because if I return null from here in response to binding then binding gets upset and so I, instead, return a dummy object with a dummy tracks property on it. I’ll come back to this in a moment because it’s actually a bit futile anyway for the moment.

And then it was off to work on the UI. I added a new ListView to show the tracks and set it up as below;

  <!-- Note - I'd really like to *bind* the oniteminvoked handler but I'm not sure I can -->
  <div id="listView" data-win-control="WinJS.UI.ListView" data-win-bind="winControl.itemDataSource: albums.dataSource WinJS.Binding.oneTime"
    data-win-options="{ layout: { type : WinJS.UI.ListLayout }, itemTemplate : albumTemplate, oniteminvoked: DefaultJS.albumInvoked }">
  </div>
  <!-- Note - this binding is redundant but there's code in default.js which patches it up -->
  <div id="trackView" data-win-control="WinJS.UI.ListView" data-win-bind="winControl.itemDataSource: selectedAlbum.tracks.dataSource WinJS.Binding.oneTime"
    data-win-options="{ layout: { type : WinJS.UI.ListLayout }, itemTemplate: trackTemplate }">
  </div>

There’s a couple of things in there I’m not 100% happy about;

  1. I’d like to bind the oniteminvoked handler of my listView to some function that lives on my data object (my musicData instance) but I don’t know that I can.
  2. I’d like to (and I attempt to) bind the trackView control so that its itemDataSource is the selectedAlbum.tracks.dataSource value. However, as I’ve previously mentioned I can’t do this in a change-notification enabled manner right now with a WinJS.Binding.List and so I go down the route of the WinJS.Binding.oneTime converter and it means that I need a little code on the other list view that is displaying albums to actually change the datacontext manually.

The need to add that code made me update my default.js file;

function () {
    "use strict";

    var app = WinJS.Application;
    var dataContext = new Music.MusicData();

    app.onactivated = function (eventObject) {
        if (eventObject.detail.kind === Windows.ApplicationModel.Activation.ActivationKind.launch)
        {

            var promise = WinJS.UI.processAll();

            promise.done(
                function ()
                {
                    return (WinJS.Binding.processAll(document.body, dataContext));
                }
            );

        }
    };

    WinJS.Namespace.define("DefaultJS",
        {
            albumInvoked: function (e)
            {
                var promise = e.detail.itemPromise;
                promise.done(
                    function (item)
                    {
                        dataContext.selectedAlbum = item.data;

                        // NB: I don't think I should have to do this. However...
                        // I've used WinJS.Binding.oneTime in the UI because
                        // the tracks property surfaces a WinJS.Binding.List and
                        // that can't handle change notification right now.
                        // Consequently, my binding in the UI is a bit redundant
                        // and this line of code patches up what I'd like to have
                        // happen when the previous line of code executes.
                        trackView.winControl.itemDataSource =
                            dataContext.selectedAlbum.tracks.dataSource;
                    }
                );
            }
        }
    );

    app.start();
})();

Then I had a UI template to add to my default.html file;

  <div id="trackTemplate" data-win-control="WinJS.Binding.Template">
    <div class="trackContainer">
      <div class="trackBox"></div>
      <h2 class="trackNumber" data-win-bind="textContent: fileInformation.musicProperties.trackNumber WinJS.Binding.oneTime">
      </h2>
      <div class="trackDetailsContainer">
        <div class="win-type-x-large" data-win-bind="textContent: fileInformation.musicProperties.title WinJS.Binding.oneTime">
        </div>
        <div class="trackBitrate">
          <span class="win-type-xx-small" data-win-bind="textContent: fileInformation.musicProperties.bitrate Utility.bitrateConverter">
          </span><span class="win-type-xx-small"> Kbps</span>
        </div>
        <div class="trackDuration" data-win-bind="textContent: fileInformation.musicProperties.duration Utility.durationConverter">
        </div>
      </div>
    </div>
  </div>

And there are a couple of new converters in that file to convert my duration value and my bitrate value so for completeness;

function convertDuration(duration)
    {
        var seconds = duration / 1000.0;
        var decimalMinutes = seconds / 60.0;
        var wholeMinutes = Math.floor(decimalMinutes);
        var seconds = Math.floor((decimalMinutes - wholeMinutes) * 60.0);
        return (wholeMinutes + "m " + seconds + "s");
    }
    function convertBitrate(bitrate)
    {
        return (Math.floor(bitrate / 1000));
    }

    WinJS.Namespace.define("Utility",
        {
            imageConverter: oneTimeConverter(
                function(thumbnail)
                {
                    return (URL.createObjectURL(thumbnail));
                }
            ),
            durationConverter: oneTimeConverter(convertDuration),
            bitrateConverter: oneTimeConverter(convertBitrate)
        }
    );

and with a few additions to my default.css file;

#trackView {    
    -ms-grid-column: 2;    
    height: 100%;
}
.trackContainer {
    display: -ms-grid;
    -ms-grid-rows: 48px 1fr;
    -ms-grid-columns: 60px 1fr;
}
.trackNumber {
    -ms-grid-row: 1;
    -ms-grid-column: 1;
}
.trackDetailsContainer {
    -ms-grid-row: 1;
    -ms-grid-row-span: 2;
    -ms-grid-column: 2;
    display: -ms-box;
    -ms-box-orient: vertical;
}
.trackBox {
    -ms-grid-row: 1;
    -ms-grid-column: 1;
    -ms-grid-column-align: stretch;
    -ms-grid-row-align: stretch;
    background-color: #292929;
    margin-right: 12px;
}

Then I have a UI that’s not dissimilar from the XAML one;

image

Step 4 – Select a Track and Play It

One way to get a selected track and have it play is to create a new property on my data model for the SelectedTrack in a similar manner to how I added SelectedAlbum and then somehow bind a media player to that track and have it play.

That is;

  public class MusicData : BindableBase
  {
    public MusicData()
    {
      this.PopulateAsync();
    }
    public TrackData SelectedTrack
    {
      get
      {
        return (this.selectedTrack);
      }
      set
      {
        base.SetProperty(ref this.selectedTrack, value);
      }
    }
    TrackData selectedTrack;

And then make sure that’s bound from the ListView which is displaying the tracks;

    <ListView Grid.Column="1"
              ItemsSource="{Binding SelectedAlbum.Tracks}"
              SelectedItem="{Binding SelectedTrack,Mode=TwoWay}">

so that the selection makes its way into the data. Then I need some media player to be bound to that SelectedTrack which causes a problem. I added another grid row and put the MediaPlayer into it.

My SelectedTrack is of type TrackData where I have a WinRT FileInformation instance which can give me a name and path and so on but if I want to use a MediaElement then it has a property called Source that I need to bind to. But that property wants a Uri.

I couldn’t come up with any way to magically turn a FileInformation object into a Uri and so I ended up writing my own attached property that looks like this;

    <MediaPlayer Grid.Row="1" AutoPlay="True">
      <utils:MediaPlayerStorageSource.StorageSource>
        <utils:FileInformationSource Source="{Binding SelectedTrack.FileInformation}" />
      </utils:MediaPlayerStorageSource.StorageSource>
    </MediaPlayer>

And that then plays the music file as the selection changes in the ListView without me having to write code to fire as that selection changes (although that code might be easier than writing attached properties J).

The code for the attached property looks like this;

namespace BlogPostCS.Utilities
{
  using System;
  using Windows.Storage.BulkAccess;
  using Windows.Storage.Streams;
  using Windows.UI.Xaml;
  using Windows.UI.Xaml.Controls;

  // TODO: Didn't spend too much time in this source file so might have messed it up.
  public class FileInformationSource : DependencyObject
  {
    public static DependencyProperty SourceProperty =
      DependencyProperty.Register("Source", typeof(FileInformation), typeof(FileInformationSource),
      new PropertyMetadata(null, OnStorageFileChanged));

    public FileInformationSource()
    {

    }
    public FileInformation Source
    {
      get
      {
        return ((FileInformation)base.GetValue(SourceProperty));
      }
      set
      {
        base.SetValue(SourceProperty, value);
      }
    }
    static async void OnStorageFileChanged(DependencyObject sender,
      DependencyPropertyChangedEventArgs args)
    {
      FileInformationSource that = (FileInformationSource)sender;
      that.StopMedia();
      if ((args.NewValue != null) && (that.mediaPlayer != null))
      {
        FileInformation file = (FileInformation)args.NewValue;
        IRandomAccessStream stream = await file.OpenReadAsync();
        that.mediaPlayer.SetSource(stream, file.ContentType);
      }
    }
    public void AttachToMediaPlayer(MediaPlayer player)
    {
      this.mediaPlayer = player;
    }
    public void DetachFromMediaPlayer()
    {
      StopMedia();
      this.mediaPlayer = null;
    }
    void StopMedia()
    {
      if (this.mediaPlayer != null)
      {
        if (this.mediaPlayer.CurrentState == Windows.UI.Xaml.Media.MediaElementState.Playing)
        {
          // TDB: whether this is really sensible.
          this.mediaPlayer.Stop();
        }
      }
    }
    MediaPlayer mediaPlayer;
  }
  public static class MediaPlayerStorageSource
  {
    public static DependencyProperty StorageSourceProperty =
      DependencyProperty.RegisterAttached(
        "StorageSource", typeof(FileInformationSource), typeof(MediaPlayer), new PropertyMetadata(null, OnSourceChanged));

    public static FileInformationSource GetStorageSource(MediaPlayer element)
    {
      return ((FileInformationSource)element.GetValue(StorageSourceProperty));
    }
    public static void SetStorageSource(MediaPlayer element, FileInformationSource value)
    {
      element.SetValue(StorageSourceProperty, value);
    }
    static void OnSourceChanged(DependencyObject source, DependencyPropertyChangedEventArgs args)
    {
      if (args.OldValue is FileInformationSource)
      {
        ((FileInformationSource)args.OldValue).DetachFromMediaPlayer();
      }
      if (args.NewValue is FileInformationSource)
      {
        ((FileInformationSource)args.NewValue).AttachToMediaPlayer((MediaPlayer)source);
      }
    }
  }
}

And that’s it. I’m doing no more with the XAML based project Smile

HTML/JS

To finish up on the HTML side I need to take similar action although it’s perhaps easier than what I did in the XAML world and especially around the “gap” between the <audio/> tag and the WinRT file that I want to play in it.

I added a selectedTrack property to my musicData class;

    var musicData = WinJS.Class.mix(
        WinJS.Class.define(
            function ()
            {
                this.albums = new WinJS.Binding.List([], { binding: false });
                this.populateAsync();
            },
            {
                selectedTrack : 
                {
                    get: function ()
                    {
                        return (this._selectedTrack);
                    },
                    set: function (value)
                    {
                        var oldValue = this._selectedAlbum;
                        this._selectedTrack = value;
                        this.notify("selectedTrack", value, oldValue);
                    }
                },
                _selectedTrack : null,

and added a little code to make sure that it gets set when the selected item in the tracksView changes;

    WinJS.Namespace.define("DefaultJS",
        {
            trackInvoked: function (e)
            {
                var promise = e.detail.itemPromise;
                promise.done(
                    function (item)
                    {
                        dataContext.selectedTrack = item.data;
                    }
                );
            },

And altered my UI to invoke that handler;

  <div id="trackView" data-win-control="WinJS.UI.ListView" data-win-bind="winControl.itemDataSource: selectedAlbum.tracks.dataSource WinJS.Binding.oneTime"
    data-win-options="{ layout: { type : WinJS.UI.ListLayout }, itemTemplate: trackTemplate, oniteminvoked: DefaultJS.trackInvoked }">
  </div>

And then added a new grid row to my layout and added an <audio/> tag into it with the src property data-bound but, once again, that needs a little conversion;

  <audio id="mediaPlayer" controls="controls" autoplay="autoplay" data-win-bind="src:selectedTrack.fileInformation Utility.fileInformationConverter">
  </audio>

Which is provided by;

function convertFileInformation(fileInformation)
    {
        var result = null;

        if (fileInformation)
        {
            result = URL.createObjectURL(fileInformation);
        }
        return (result);
    }

    WinJS.Namespace.define("Utility",
        {
            imageConverter: oneTimeConverter(
                function(thumbnail)
                {
                    return (URL.createObjectURL(thumbnail));
                }
            ),
            durationConverter: oneTimeConverter(convertDuration),
            bitrateConverter: oneTimeConverter(convertBitrate),
            fileInformationConverter: WinJS.Binding.converter(convertFileInformation)
        }
    );

And that’s it. This post went on way too long and I hadn’t quite realised what I was setting out on when I started writing it so the smart thing to do would be to stop Smile

Here’s the 2 projects for download if they are of use to you – note that there’s no best practise here or anything like that but, instead, just some experiments with some bits of code.

XAML/CS Project.

HTML/JS Project.