Rebuilding the PDC 2010 Silverlight Application (Part 3)

The application hasn’t shown any data yet and so in this post I’ll start trying to get some data onto the screen. Specifically, I’ll look to get the tracks view and the sessions view built albeit with a default UI that will need a lot of styling at a later point.

Part 3 – Getting Data via WCF Data Services

Up until now, my data abilities are living in 2 projects;

image

and the exposed surface area is covered by this interface;

  public interface IPDCDataModel
  {
    IEnumerable<string> GetTracks();
  }

with an implementation hard-coded to return a bunch of strings;

 [Export(typeof(IPDCDataModel))]
  [PartCreationPolicy(CreationPolicy.NonShared)]
  public class PDCDataModel : IPDCDataModel
  {
    public IEnumerable<string> GetTracks()
    {
      return(
        from i in Enumerable.Range(1, 10)
        select string.Format("Dummy Track {0}", i));
    }
  }

Now, I’m going to choose to tie that PDCDataModel class pretty tightly to WCF Data Services rather than drop in some other level of abstraction and from that project I’ll directly reference the OData service for the PDC;

image

you could argue about this as it’s now very hard for me to test this layer in isolation.

Adding a Tracks View

Data Bits

I need to update my interface that sits between my views and the data model in order to be able to load up some data for the tracks;

  public interface IPDCDataModel
  {
    void LoadTracksAsync();
    event EventHandler<AsyncDataCompletedEventArgs<IEnumerable<Track>>> LoadTracksCompleted;
  }

these types live in a class library project that’s referenced from two places;

  1. the XAP project that builds my “data model”
  2. the XAP project that builds my out-of-browser views.

and so represent the encapsulation of my data model.

I also added a Track data type to that library;

  public class Track
  {
    public string Title { get; set; }
    public string Name { get; set; }
  }

along with a little event args class;

  public class AsyncDataCompletedEventArgs<T> : AsyncCompletedEventArgs
  {
    public AsyncDataCompletedEventArgs(Exception error)
      : base(error, false, null)
    {
    }
    public T Result { get; set; }
  }

with those in place I can look to build an implementation of this interface in my data model project;

  [Export(typeof(IPDCDataModel))]
  [PartCreationPolicy(CreationPolicy.Shared)]
  public class PDCDataModel : IPDCDataModel
  {
    public event EventHandler<AsyncDataCompletedEventArgs<IEnumerable<Track>>> LoadTracksCompleted;

    // NB: this class is effectively a Singleton, controlled by MEF
    public PDCDataModel()
    {
      this.proxy = new ODataProxy.ScheduleModel(new Uri(uri, UriKind.Absolute));
      this.proxy.HttpStack = HttpStack.ClientHttp;
    }
    public void LoadTracksAsync()
    {
      if (this.tracks == null)
      {
        this.proxy.BeginExecute<ODataProxy.Track>(
          this.proxy.Tracks.RequestUri,
          iar =>
          {
            Exception ex = null;
            
            try
            {
              this.tracks =
                this.proxy.EndExecute<ODataProxy.Track>(iar).Select(
                  track => new Track()
                  {
                    Name = track.Name,
                    Title = track.Title
                  }).ToList();
            }
            catch (Exception e)
            {
              ex = e;
            }
            FireTracksLoaded(ex);
          }, null);
      }
      else
      {
        FireTracksLoaded();
      }
    }
    void FireTracksLoaded(Exception ex = null)
    {
      var handlers = this.LoadTracksCompleted;

      if (handlers != null)
      {
        AsyncDataCompletedEventArgs<IEnumerable<Track>> args =
          new AsyncDataCompletedEventArgs<IEnumerable<Track>>(ex)
            {
              Result = this.tracks
            };

        handlers(this, args);
      }
    }
    List<Track> tracks;
    ODataProxy.ScheduleModel proxy;   
    const string uri = "http://odata.microsoftpdc.com/ODataSchedule.svc";
  }

and so this is fairly simple. I’ve chosen not to pass the types that the proxy owns (e.g. ODataProxy.Track) outside of this layer as it doesn’t feel right to me and those types won’t always suit my needs as we’ll see when it comes to looking at session data.

I’ve also chosen to change the PartCreationPolicy on this data model to make it a singleton via MEF and I have it holding onto data like the List<Track> because that data isn’t going to change so I might as well cache it here in the data model.

View Bits

With that built, I can go and revisit my original TracksView that sits on top of this and alter it to actually display something;

<UserControl
  x:Class="PDCTutorial.Views.Views.TracksView"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
  xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
  xmlns:tk="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Toolkit"
  xmlns:defv="clr-namespace:PDCTutorial.DeferredViewContracts;assembly=PDCTutorial.DeferredViewContracts"
  mc:Ignorable="d"
  d:DesignHeight="300"
  d:DesignWidth="400">
  <Grid
    x:Name="LayoutRoot">
    <tk:BusyIndicator
      IsBusy="{Binding IsBusy}">
      <ListBox
        ItemsSource="{Binding Tracks}">
        <ListBox.ItemTemplate>
          <DataTemplate>
            <defv:DeferredView
              ViewName="TrackView" />
          </DataTemplate>
        </ListBox.ItemTemplate>
      </ListBox>
    </tk:BusyIndicator>
  </Grid>
</UserControl>

and this imports a ViewModel that now needs building up a little to actually do something;

  [Export("TracksViewModel", typeof(object))]
  public class TracksViewModel : PropertyChangedNotification, 
    IPartImportsSatisfiedNotification
  {
    public TracksViewModel()
    {
    }
    [Import]
    public IPDCDataModel DataModel 
    { 
      get; 
      set; 
    }
    public bool IsBusy
    {
      get
      {
        return (_IsBusy);
      }
      set
      {
        _IsBusy = value;
        RaisePropertyChanged("IsBusy");
      }
    }
    bool _IsBusy;

    public IEnumerable<TrackViewModel> Tracks
    {
      get
      {
        return (_Tracks);
      }
      set
      {
        _Tracks = value;
        RaisePropertyChanged("Tracks");
      }
    }
    IEnumerable<TrackViewModel> _Tracks;      

    public void OnImportsSatisfied()
    {
      this.IsBusy = true;

      this.DataModel.LoadTracksCompleted += OnLoadTracksCompleted;
      this.DataModel.LoadTracksAsync();
    }
    void OnLoadTracksCompleted(object sender, 
      AsyncDataCompletedEventArgs<IEnumerable<Track>> e)
    {
      Deployment.Current.Dispatcher.BeginInvoke(() =>
        {
          this.DataModel.LoadTracksCompleted -= OnLoadTracksCompleted;

          this.IsBusy = false;

          if (e.Error != null)
          {
            MessageBoxChildWindow.ShowError(e.Error);
          }
          else
          {
            this.Tracks = e.Result.Select(
              track => new TrackViewModel()
              {
                Track = track
              });
          }
        });
    }
  }

and it brings in another deferred view, the TrackView which is as below;

<UserControl
  x:Class="PDCTutorial.Views.Views.TrackView"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
  xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
  mc:Ignorable="d"
  d:DesignHeight="300"
  d:DesignWidth="400">
  <Button
    Content="{Binding Track.Name}"
    Command="{Binding SwitchTracksCommand}">
    <ToolTipService.ToolTip>
      <ToolTip>
        <TextBlock
          Width="192"
          Height="48"
          Text="{Binding Track.Title}" />
      </ToolTip>
    </ToolTipService.ToolTip>
  </Button>
</UserControl>

and those instances will end up bound to the instances of the TrackViewModel which were handed out by the Tracks property of the TracksViewModel. The TrackViewModel is very simple;

using System.Windows.Input;
  using Microsoft.Expression.Interactivity.Core;
  using PDCTutorial.DataModelContracts;
  using PDCTutorial.Utility;

  public class TrackViewModel : PropertyChangedNotification
  {
    public TrackViewModel()
    {
      this.SwitchTracksCommand = new ActionCommand(OnSwitchTracks);
    }
    public Track Track
    {
      get
      {
        return (_Track);
      }
      set
      {
        _Track = value;
        RaisePropertyChanged("Track");
      }
    }
    Track _Track;

    public ICommand SwitchTracksCommand
    {
      get
      {
        return (_SwitchTracksCommand);
      }
      set
      {
        _SwitchTracksCommand = value;
        RaisePropertyChanged("SwitchTracksCommand");
      }
    }
    ICommand _SwitchTracksCommand; 

    public void OnSwitchTracks()
    {      
      // TODO: We need to now switch tracks!
    }
  }

and there’s some work there to actually decide what to do in the OnSwitchTracks method.

Adding a Sessions View

In displaying the session list I want to be able to display data such as;

  • Session Title
  • Session Code
  • Presenter Name
  • Presenter Picture
  • Session Picture
  • Session Abstract
  • What format of content is downloadable (powerpoint? high def MP4? low def MP4?)

and this isn’t available from a single collection of data hanging off the OData feeds for the PDC. In fact, I found some of the navigation across feeds a little odd for the OData service.

As an example I can navigate to this session http://odata.microsoftpdc.com/ODataSchedule.svc/Sessions(guid'1b08b109-c959-4470-961b-ebe8840eeb84') but then if I navigate to the Presenters via http://odata.microsoftpdc.com/ODataSchedule.svc/Sessions(guid'1b08b109-c959-4470-961b-ebe8840eeb84')/Presenters then I end up with a GUID as an ID.

There is no navigation to take me to that presenter. To navigate to that presenter I have to take this GUID and go to the top level collection called Speakers with the GUID, i.e. http://odata.microsoftpdc.com/ODataSchedule.svc/Speakers(guid'c3fc690b-6e6b-43a7-8775-1b10585923cd').

That all feels a little “weird” to me, I’d expect to be able to navigate directly.

What this ultimately means is that my sessions view needs to read data from the Session and Speaker collections in order to be able to display what it wants to display.

When it comes to loading session data, there’s a trade-off to be made between doing one big download of all the session data or demand-loading the data for a particular track and then caching it for subsequent use.

I went for the latter and so load a track’s worth of data at a time and then keep it.

Data Bits

I added new methods to my data model interface to support the idea of loading up sessions asynchronously;

  public interface IPDCDataModel
  {
    void LoadTracksAsync();
    event EventHandler<AsyncDataCompletedEventArgs<IEnumerable<Track>>> LoadTracksCompleted;

    void LoadSessionsForTrackAsync(Track track);
    event EventHandler<AsyncDataCompletedEventArgs<IEnumerable<Session>>> LoadSessionsForTrackCompleted;
  }

and added classes surfaced from my data model to represent a Session;

  public class Session
  {
    public string Name { get; set; }
    public string TrackName { get; set; }
    public string Code { get; set; }
    public string ThumbnailUrl { get; set; }
    public string Description { get; set; }
    public string HashTag { get; set; }
    public Speaker Speaker { get; set; }
    public DownloadableContent Content { get; set; }
  }

which has a Speaker;

 public class Speaker
  {
    static Speaker()
    {
      _unknownSpeaker = new Speaker()
      {
        Name = "Unknown",
        ImageUrl = null
      };
    }
    public string Name { get; set; }
    public string ImageUrl { get; set; }

    public static Speaker Unknown
    {
      get
      {
        return (_unknownSpeaker);
      }
    }
    static Speaker _unknownSpeaker;
  }

and some DownloadableContent;

 public class DownloadableContent
  {
    public string PowerPointUrl { get; set; }
    public string LowVideoUrl { get; set; }
    public string HighVideoUrl { get; set; }

    public bool HasPowerPoint
    {
      get
      {
        return (!string.IsNullOrEmpty(this.PowerPointUrl));
      }
    }
    public bool HasLowVideo
    {
      get
      {
        return (!string.IsNullOrEmpty(this.LowVideoUrl));
      }
    }
    public bool HasHighVideo
    {
      get
      {
        return (!string.IsNullOrEmpty(this.HighVideoUrl));
      }
    }
  }

and then added the implementation of the new LoadSessionsForTrackAsync and the corresponding event to my existing IPDCDataModel implementation;

 [Export(typeof(IPDCDataModel))]
  [PartCreationPolicy(CreationPolicy.Shared)]
  public class PDCDataModel : IPDCDataModel
  {
    public event EventHandler<AsyncDataCompletedEventArgs<IEnumerable<Track>>> LoadTracksCompleted;
    public event EventHandler<AsyncDataCompletedEventArgs<IEnumerable<Session>>> LoadSessionsForTrackCompleted;

    // NB: this class is effectively a Singleton, controlled by MEF
    public PDCDataModel()
    {
      this.proxy = new ODataProxy.ScheduleModel(new Uri(ODATA_SERVICE_URI, UriKind.Absolute));
      this.proxy.HttpStack = HttpStack.ClientHttp;
    }
    public void LoadTracksAsync()
    {
      if (this.tracks == null)
      {
        this.proxy.BeginExecute<ODataProxy.Track>(
          this.proxy.Tracks.RequestUri,
          iar =>
          {
            Exception ex = null;
            
            try
            {
              this.tracks =
                this.proxy.EndExecute<ODataProxy.Track>(iar).Select(
                  track => new Track()
                  {
                    Name = track.Name,
                    Title = track.Title
                  }).ToList();
            }
            catch (Exception e)
            {
              ex = e;
            }
            FireTracksLoaded(ex);
          }, null);
      }
      else
      {
        FireTracksLoaded();
      }
    }
    void FireTracksLoaded(Exception ex = null)
    {
      var handlers = this.LoadTracksCompleted;

      if (handlers != null)
      {
        AsyncDataCompletedEventArgs<IEnumerable<Track>> args =
          new AsyncDataCompletedEventArgs<IEnumerable<Track>>(ex)
            {
              Result = this.tracks
            };

        handlers(this, args);
      }
    }

    public void LoadSessionsForTrackAsync(Track track)
    {
      if (this.sessions == null)
      {
        this.sessions = new List<Session>();        
      }

      var filteredSessions =
        this.sessions.Where(s => s.TrackName == track.Name);

      if (filteredSessions.Count() == 0)
      {
        List<DataServiceRequest> requests = new List<DataServiceRequest>();

        if (this.speakers == null)
        {
          this.speakers = new Dictionary<Guid, Speaker>();

          DataServiceRequest<ODataProxy.Speaker> speakerRequest =
            new DataServiceRequest<ODataProxy.Speaker>(proxy.Speakers.RequestUri);

          requests.Add(speakerRequest);
        }

        DataServiceQuery<ODataProxy.Session> query =
          (DataServiceQuery<ODataProxy.Session>)
          (
            from s in this.proxy.Sessions.Expand("Presenters").Expand("DownloadableContent")
            where s.TrackId == track.Name
            select s
          );

        DataServiceRequest<ODataProxy.Session> sessionRequest =
          new DataServiceRequest<ODataProxy.Session>(query.RequestUri);

        requests.Add(sessionRequest);

        this.proxy.BeginExecuteBatch(iar =>
          {
            Exception ex = null;

            try
            {
              DataServiceResponse responses = this.proxy.EndExecuteBatch(iar);

              if ((responses.BatchStatusCode >= 200) &&
                   (responses.BatchStatusCode < 300))
              {
                foreach (QueryOperationResponse resp in responses)
                {
                  ex = resp.Error;

                  if (ex != null)
                  {
                    throw ex;
                  }
                  if (resp.Query.ElementType == typeof(ODataProxy.Speaker))
                  {
                    AddSpeakersFromResponse(resp);
                  }
                  if (resp.Query.ElementType == typeof(ODataProxy.Session))
                  {
                    AddSessionsFromResponse(resp);
                  }
                }  
              }
              else
              {
                throw new DataServiceQueryException("Bad response code to query");
              }
            }
            catch (Exception e)
            {
              ex = e;
            }
            FireSessionsForTrackLoaded(track, ex);
          },
          null,
          requests.ToArray());
      }
      else
      {
        FireSessionsForTrackLoaded(track);
      }
    }
    void AddSpeakersFromResponse(QueryOperationResponse response)
    {
      foreach (ODataProxy.Speaker speaker in response)
      {
        speakers[speaker.Id] = new Speaker()
        {
          Name = speaker.FullName,
          ImageUrl = speaker.PhotoUrl ?? Speaker.Unknown.ImageUrl
        };
      }
    }
    void AddSessionsFromResponse(QueryOperationResponse response)
    {
      this.sessions.AddRange(
        ((IEnumerable<ODataProxy.Session>)response).Select(
          session => new Session()
          {
            Name = session.ShortTitle,
            Description = session.ShortDescription,
            ThumbnailUrl = session.ThumbnailUrl,
            HashTag = session.TwitterHashtag,
            TrackName = session.TrackId,
            Code = session.Code,
            Speaker = DetermineSpeaker(session),
            Content = DetermineSessionContent(session)
          }));
    }
    DownloadableContent DetermineSessionContent(ODataProxy.Session session)
    {
      DownloadableContent content = new DownloadableContent();

      foreach (ODataProxy.Content sourceContent in session.DownloadableContent)
      {
        if (!string.IsNullOrEmpty(sourceContent.Url))
        {
          if (sourceContent.Url.EndsWith(POWERPOINT_EXTENSION))
          {
            content.PowerPointUrl = sourceContent.Url;
          }
          else if (sourceContent.Url.EndsWith(MP4_EXTENSION))
          {
            if (sourceContent.Url.Contains(HIGH_VIDEO_SEARCH_STRING))
            {
              content.HighVideoUrl = sourceContent.Url;
            }
            if (sourceContent.Url.Contains(LOW_VIDEO_SEARCH_STRING))
            {
              content.LowVideoUrl = sourceContent.Url;
            }
          }
        }
      }
      return (content);
    }
    Speaker DetermineSpeaker(ODataProxy.Session session)
    {
      Speaker speaker = Speaker.Unknown;

      if ((session.Presenters != null) &&
        (session.Presenters.Count > 0) &&
        (this.speakers != null) &&
        (this.speakers.ContainsKey(session.Presenters[0].Id)))
      {
        speaker = speakers[session.Presenters[0].Id];
      }
      return (speaker);
    }
    void FireSessionsForTrackLoaded(Track track, Exception ex = null)
    {
      var handlers = this.LoadSessionsForTrackCompleted;

      if (handlers != null)
      {
        handlers(this, new AsyncDataCompletedEventArgs<IEnumerable<Session>>(ex)
        {
          Result = this.sessions.Where(s => s.TrackName == track.Name)
        });
      }
    }
    List<Session> sessions;
    Dictionary<Guid, Speaker> speakers;
    List<Track> tracks;
    ODataProxy.ScheduleModel proxy;   

    const string ODATA_SERVICE_URI = "http://odata.microsoftpdc.com/ODataSchedule.svc";
    const string POWERPOINT_EXTENSION = ".pptx";
    const string MP4_EXTENSION = ".mp4";
    const string HIGH_VIDEO_SEARCH_STRING = "MP4_High";
    const string LOW_VIDEO_SEARCH_STRING = "MP4_Low";
  }

View Bits

There’s then a sessions view to construct on top of these new data bits. I made sure that my MainView was importing a deferred view called SessionsView;

<Grid
    x:Name="LayoutRoot">
    <Grid.ColumnDefinitions>
      <ColumnDefinition
        Width="*" />
      <ColumnDefinition Width="3*"/>
    </Grid.ColumnDefinitions>
    <defv:DeferredView
      ViewName="TracksView" />
    <defv:DeferredView
      Grid.Column="2"
      ViewName="SessionsView" />
  </Grid>

and then implement that view;

<UserControl
  x:Class="PDCTutorial.Views.Views.SessionsView"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
  xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
  xmlns:tk="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Toolkit"
  xmlns:defv="clr-namespace:PDCTutorial.DeferredViewContracts;assembly=PDCTutorial.DeferredViewContracts"
  mc:Ignorable="d"
  d:DesignHeight="300"
  d:DesignWidth="400">
  <Grid
    x:Name="LayoutRoot"
    Background="White">
    <tk:BusyIndicator
      IsBusy="{Binding IsBusy}">
      <ListBox
        ItemsSource="{Binding Sessions}">       
        <ListBox.ItemTemplate>
          <DataTemplate>
            <defv:DeferredView
              ViewName="SessionView" />
          </DataTemplate>
        </ListBox.ItemTemplate>
      </ListBox>
    </tk:BusyIndicator>
  </Grid>
</UserControl>

making sure that it exports itself as “SessionsView” and brings in the right view model;

 [ExportView(ViewName = "SessionsView")]
  public partial class SessionsView : UserControl
  {
    public SessionsView()
    {
      InitializeComponent();
    }
    [Import("SessionsViewModel")]
    public object ViewModel
    {
      set
      {
        this.DataContext = value;
      }
    }
  }

and the view model for that view talks through to the new data functionality;

  [Export("SessionsViewModel", typeof(object))]
  public class SessionsViewModel : PropertyChangedNotification, 
    IPartImportsSatisfiedNotification
  {
    public SessionsViewModel()
    {
      this.Sessions = new ObservableCollection<SessionViewModel>();
    }

    public bool IsBusy
    {
      get
      {
        return (_IsBusy);
      }
      set
      {
        _IsBusy = value;
        RaisePropertyChanged("IsBusy");
      }
    }
    bool _IsBusy;

    [Import]
    public IPDCDataModel DataModel
    {
      get;
      set;
    }

    public ObservableCollection<SessionViewModel> Sessions
    {
      get
      {
        return (_Sessions);
      }
      set
      {
        _Sessions = value;
        RaisePropertyChanged("Sessions");
      }
    }
    ObservableCollection<SessionViewModel> _Sessions;

    public void OnImportsSatisfied()
    {
      // TODO. This needs replacing in a moment.
      this.IsBusy = true;

      this.DataModel.LoadSessionsForTrackCompleted += OnSessionsLoaded;
      this.DataModel.LoadSessionsForTrackAsync(new Track()
      {
        Name = "Client & Devices"
      });
    }
    void OnSessionsLoaded(object sender, AsyncDataCompletedEventArgs<IEnumerable<Session>> e)
    {
      Deployment.Current.Dispatcher.BeginInvoke(() =>
        {
          this.IsBusy = false;

          if (e.Error == null)
          {
            while (this.Sessions.Count > 0)
            {
              this.Sessions.RemoveAt(0);
            }
            foreach (Session session in e.Result)
            {
              this.Sessions.Add(new SessionViewModel()
              {
                Session = session
              });
            }
          }
          else
          {
            MessageBoxChildWindow.ShowError(e.Error);
          }
        });
    }
  }

and it is currently hard-coded to switch to the “Client & Devices” track on loading.

This relies on yet another deferred view, the “SessionView” and, for the moment, the view takes a everything-but-the-kitchen-sink approach;

<UserControl
  x:Class="PDCTutorial.Views.Views.SessionView"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
  xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
  mc:Ignorable="d"
  d:DesignHeight="300"
  d:DesignWidth="400"
  xmlns:my="clr-namespace:PDCTutorial.Views.ViewModels"
  xmlns:sdk="http://schemas.microsoft.com/winfx/2006/xaml/presentation/sdk">
  <UserControl.Resources>
    <Style
      TargetType="TextBlock">
      <Setter
        Property="FontSize"
        Value="8" />
      <Setter
        Property="MaxWidth"
        Value="284" />
      <Setter
        Property="TextWrapping"
        Value="Wrap" />
    </Style>
  </UserControl.Resources>
  <Grid Margin="12">
    <Grid.ColumnDefinitions>
      <ColumnDefinition
        Width="Auto" />
      <ColumnDefinition
        Width="Auto" />
    </Grid.ColumnDefinitions>
    <Grid.RowDefinitions>
      <RowDefinition
        Height="Auto" />
      <RowDefinition
        Height="Auto" />
      <RowDefinition
        Height="Auto" />
      <RowDefinition
        Height="Auto" />
      <RowDefinition
        Height="Auto" />
      <RowDefinition
        Height="Auto" />
      <RowDefinition
        Height="Auto" />
      <RowDefinition
        Height="Auto" />
      <RowDefinition
        Height="Auto" />
      <RowDefinition
        Height="Auto" />
      <RowDefinition
        Height="Auto" />
      <RowDefinition
        Height="Auto" />
      <RowDefinition
        Height="Auto" />
      <RowDefinition
        Height="Auto" />
    </Grid.RowDefinitions>
    <sdk:Label
      Content="Code:"
      Grid.Column="0"
      Grid.Row="0"/>
    <TextBlock
      Grid.Column="1"
      Grid.Row="0"
      Text="{Binding Path=Session.Code}"/>
    <sdk:Label
      Content="Description:"
      Grid.Column="0"
      Grid.Row="1"/>
    <TextBlock
      Grid.Column="1"
      Grid.Row="1"
      Text="{Binding Path=Session.Description}"/>
    <sdk:Label
      Content="Hash Tag:"
      Grid.Column="0"
      Grid.Row="2"/>
    <TextBlock
      Grid.Column="1"
      Grid.Row="2"
      Text="{Binding Path=Session.HashTag}"/>
    <sdk:Label
      Content="Name:"
      Grid.Column="0"
      Grid.Row="3"/>
    <TextBlock
      Grid.Column="1"
      Grid.Row="3"
      Text="{Binding Path=Session.Name}"/>
    <sdk:Label
      Content="Thumbnail Url:"
      Grid.Column="0"
      Grid.Row="4"/>
    <Image
      Grid.Column="1"
      Grid.Row="4"
      Source="{Binding Path=Session.ThumbnailUrl}"
      Stretch="Fill"
      VerticalAlignment="Center"
      Width="48" />
    <sdk:Label
      Content="Track Name:"
      Grid.Column="0"
      Grid.Row="5"/>
    <TextBlock
      Grid.Column="1"
      Grid.Row="5"
      Text="{Binding Path=Session.TrackName}"/>
    <sdk:Label
      Content="Has High Video:"
      Grid.Column="0"
      Grid.Row="6"/>
    <CheckBox
      Content=""
      Grid.Column="1"
      Grid.Row="6"
      IsChecked="{Binding Path=Session.Content.HasHighVideo}"/>
    <sdk:Label
      Content="Has Low Video:"
      Grid.Column="0"
      Grid.Row="7"/>
    <CheckBox
      Content=""
      Grid.Column="1"
      Grid.Row="7"
      IsChecked="{Binding Path=Session.Content.HasLowVideo}"/>
    <sdk:Label
      Content="Has Power Point:"
      Grid.Column="0"
      Grid.Row="8"/>
    <CheckBox
      Content=""
      Grid.Column="1"
      Grid.Row="8"
      IsChecked="{Binding Path=Session.Content.HasPowerPoint}"/>
    <sdk:Label
      Content="High Video Url:"
      Grid.Column="0"
      Grid.Row="9"/>
    <TextBlock
      Grid.Column="1"
      Grid.Row="9"
      Text="{Binding Path=Session.Content.HighVideoUrl}"/>
    <sdk:Label
      Content="Low Video Url:"
      Grid.Column="0"
      Grid.Row="10"/>
    <TextBlock
      Grid.Column="1"
      Grid.Row="10"
      Text="{Binding Path=Session.Content.LowVideoUrl}"/>
    <sdk:Label
      Content="Power Point Url:"
      Grid.Column="0"
      Grid.Row="11"/>
    <TextBlock
      Grid.Column="1"
      Grid.Row="11"
      Text="{Binding Path=Session.Content.PowerPointUrl}"/>
    <sdk:Label
      Content="Image Url:"
      Grid.Column="0"
      Grid.Row="12"/>
    <Image
      Grid.Column="1"
      Grid.Row="12"
      Source="{Binding Path=Session.Speaker.ImageUrl}"
      Width="48" />
    <sdk:Label
      Content="Name:"
      Grid.Column="0"
      Grid.Row="13"/>
    <TextBlock
      Grid.Column="1"
      Grid.Row="13"
      Text="{Binding Path=Session.Speaker.Name}"/>
  </Grid>
</UserControl>

and, once again, it is underpinned by a SessionViewModel which is returned from the Sessions property on the SessionsViewModel;

  public class SessionViewModel : PropertyChangedNotification
  {
    public Session Session
    {
      get
      {
        return (_Session);
      }
      set
      {
        _Session = value;
        RaisePropertyChanged("Session");
      }
    }
    Session _Session;
  }

with all of that in place, I can now run up my application and get some data displayed albeit only for the “Client & Devices” track because that one is hard-coded right now.

image

Sure, it’s the UI from Hell but it’s moving along…

Wiring the Views Together

At the moment, my 2 views know nothing about each other and the Tracks view has no way of communicating a change of the selected track across to the Sessions view. This needs fixing.

Clearly, this would be easy to communicate through the underlying data model but I’ve chosen to have a simple “broadcast” messaging service present in the app whereby views can opt in to messages sent around by other views.

I added 2 more projects to my solution;

image

and defined interfaces which are built into a class library;

  public interface IMessageChannel<T>
  {
    void Broadcast(T t);
    void Subscribe(Action<T> t);
  }
  public interface IMessageService
  {
    IMessageChannel<T> GetChannel<T>(string channelName);
  }

and implementation which is built into a XAP;

  [Export(typeof(IMessageService))]
  [PartCreationPolicy(CreationPolicy.Shared)]
  public class SimpleMessageBroker : IMessageService
  {
    class SimpleMessageChannel<T> : IMessageChannel<T>
    {
      public void Broadcast(T message)
      {
        if (this.subscriptions != null)
        {
          foreach (var item in this.subscriptions)
          {
            item(message);
          }
        }
      }
      public void Subscribe(Action<T> subscription)
      {
        if (this.subscriptions == null)
        {
          this.subscriptions = new List<Action<T>>();
        }
        this.subscriptions.Add(subscription);
      }
      List<Action<T>> subscriptions;
    }
    public IMessageChannel<T> GetChannel<T>(string channelName)
    {
      if (this.channels == null)
      {
        this.channels = new Dictionary<string, Dictionary<Type, object>>();
      }
      if (!channels.ContainsKey(channelName))
      {
        this.channels[channelName] = new Dictionary<Type, object>();
      }
      if (!channels[channelName].ContainsKey(typeof(T)))
      {
        (this.channels[channelName])[typeof(T)] = new SimpleMessageChannel<T>();
      }
      return ((IMessageChannel<T>)(this.channels[channelName])[typeof(T)]);
    }
    Dictionary<string, Dictionary<Type, object>> channels;
  }

and then I made sure that my TrackViewModel had access to this IMessageService and so can send a message when the track selection changes.

Note – I found this “interesting” because my TrackView is bound to a TrackViewModel which is not created by MEF but is created directly by its parent view model and so it’s “not obvious” to import an IMessageService implementation. I thought of various solutions;

  1. Import an ExportFactory for the TrackViewModel.
  2. Call SatisyImports from the TrackViewModel.

but in the end I just imported the IMessageService onto the parent view model and passed it down to the TrackViewModel as it got created.

and I made sure that my TracksViewModel does an import on this IMessageService;

  [Export("TracksViewModel", typeof(object))]
  public class TracksViewModel : PropertyChangedNotification, 
    IPartImportsSatisfiedNotification
  {
	// Snip... 

    [Import]
    public IMessageService MessageService { get; set; }

and that it passes it down to the TrackViewModel which then uses it when the track selection changes;


  public class TrackViewModel : PropertyChangedNotification
  {
    // Snip...

    public void OnSwitchTracks()
    {
      MessageService.GetChannel<Track>("TrackChanged").Broadcast(this.Track);
    }
  }

and then on the receiving side the SessionsViewModel also imports the IMessageService and subscribes to the “TrackChanged” event and when it sees it, it loads the data;

  [Export("SessionsViewModel", typeof(object))]
  public class SessionsViewModel : PropertyChangedNotification, 
    IPartImportsSatisfiedNotification
  {
    // Snip...

    public void OnImportsSatisfied()
    {
      // TODO. This needs replacing in a moment.       
      this.MessageService.GetChannel<Track>("TrackChanged").Subscribe(OnTrackChanged);
      this.DataModel.LoadSessionsForTrackCompleted += OnSessionsLoaded;
    }
    void OnTrackChanged(Track track)
    {
      this.IsBusy = true;     
      this.DataModel.LoadSessionsForTrackAsync(track);
    }

and so with that in place I’ve got a UI that comes together from a bunch of pieces to display the session data across the various tracks;

image

What’s Still Missing

There’s a bit of work to do here yet including things like;

  1. Build some kind of downloading component
  2. Build the download view
  3. Make it look a lot better than it does right now

Where’s the Source?

Here’s the source code for download.

What’s Next?

Maybe this needs to look a little better before any more code gets added to it because, right now, it looks like it was hit with the ugly stick.

So, next up, we’ll add a little styling before moving on to build some more functionality.