Rebuilding the PDC 2010 Silverlight Application (Part 6)

As it stands in that previous post, when the user selects to download a particular PowerPoint deck or a hi-def or lo-def video for a session, that information is only being stored in a view model ( the SessionViewModel ) and so it easily gets lost if the user performs an action such as switching to a different track.

Also, there’s the small issue of not actually downloading any content files yet Smile

Saving Session Selections

As an example if I select that I want Mark’s session here;

image

and then move to the “Cloud Services” track and back again to the “Client & Devices” track;

imageimage

then my selection is lost because it was only stored on a SessionViewModel which has since been destroyed and re-created.

I pondered for quite a while as to whether this selection status belonged on the underlying data model or whether it belonged elsewhere on perhaps another service of some kind.

It seems simple enough but at some point I’m going to have to pass a list of sessions to be downloaded to some kind of downloading component and I want to minimise the dependencies of that component whilst not going crazy and duplicating all my data in the app.

So, in the end I did go with the idea of including this status on the underlying data model such that my DownloadableContent class which is currently being used to store what kind of content is available additionally gets beefed up a little to store what kind of content the user has chosen to download.

I also beefed that class up to have some more capabilities such as being responsible for managing the local file paths for downloaded content;

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

    public bool UserWantsPowerPoint { get; set; }
    public bool UserWantsHighDefVideo { get; set; }
    public bool UserWantsLowDefVideo { get; set; }

    public bool UserWantsSomeContent
    {
      get
      {
        return (UserWantsPowerPoint ||
          UserWantsHighDefVideo ||
          UserWantsLowDefVideo);
      }
    }
    public int DownloadFileCount
    {
      get
      {
        int total = this.UserWantsPowerPoint ? 1 : 0;
        total += this.UserWantsHighDefVideo ? 1 : 0;
        total += this.UserWantsLowDefVideo ? 1 : 0;
        return (total);
      }
    }
    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));
      }
    }
    public string PowerPointLocalFile
    {
      get
      {
        return (System.IO.Path.Combine(PowerPointFolder,
          UrlUtility.MakeLocalFileName(this.PowerPointUrl)));
      }
    }
    public string LowVideoLocalFile
    {
      get
      {
        return (System.IO.Path.Combine(LowDefFolder,
          UrlUtility.MakeLocalFileName(this.LowVideoUrl)));
      }
    }
    public string HighVideoLocalFile
    {
      get
      {
        return (System.IO.Path.Combine(HighDefFolder,
          UrlUtility.MakeLocalFileName(this.HighVideoUrl)));
      }
    }
    static string PowerPointFolder
    {
      get
      {
        return (System.IO.Path.Combine(
          Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments),
          PDC_FOLDER));
      }
    }
    static string HighDefFolder
    {
      get
      {
        return (System.IO.Path.Combine(
          Environment.GetFolderPath(Environment.SpecialFolder.MyVideos),
          PDC_HIGH_DEF_FOLDER));
      }
    }
    static string LowDefFolder
    {
      get
      {
        return (System.IO.Path.Combine(
          Environment.GetFolderPath(Environment.SpecialFolder.MyVideos),
          PDC_LOW_DEF_FOLDER));
      }
    }
    public static void EnsureFolders()
    {
      string[] folders = 
      {
        PowerPointFolder,
        HighDefFolder,
        LowDefFolder 
      };
      foreach (var folder in folders)
      {
        if (!System.IO.Directory.Exists(folder))
        {
          System.IO.Directory.CreateDirectory(folder);
        }
      }
    }
    const string PDC_FOLDER = "PDC2010";
    const string PDC_HIGH_DEF_FOLDER = "PDC2010High";
    const string PDC_LOW_DEF_FOLDER = "PDC2010Low";
  }

and with a quick change or two to the SessionViewModel which ultimately puts this data onto the screen, my user’s selections are being stored in the underlying data.

So, that’s easy enough but I need to make sure that these selections are surfaced easily by the data model and so I added a little to its interface with this last method here added to the existing interface to my data model;

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

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

    IEnumerable<Session> GetSessionsForDownload();
  }

and implemented that;

    public IEnumerable<Session> GetSessionsForDownload()
    {
      return (this.sessions.Where(s => s.Content.UserWantsSomeContent));
    }

and that’s all good.

Launching the Download View

I need a new button on my UI to initiate the download process and that’s easy enough to add to my existing SessionsView;

          <Button
            Content="Download"
            Foreground="White"
            Command="{Binding DownloadCommand}">
          </Button>

where it will need styling but already fires a DownloadCommand that I’ve added to the underlying SessionsViewModel.

I don’t want to wire the SessionsViewModel so that it knows too much about how to start the downloading process and so I’ve chosen to simply have it use my already present messaging service to simply send a message. If some other piece of componentry knows how to receive and deal with that message then that’s great and, if not, it’s not the responsibility of this particular view model to worry about that. So, that command ultimately just causes a message to be sent;

     this.DownloadCommand = new ActionCommand(() =>
        {
          StartDownloading();
        });

with the StartDownloading method;

    void StartDownloading()
    {
      if (this.MessageService != null)
      {
        this.MessageService.GetChannel<bool>("startDownloadChannel").Broadcast(true);
      }
    }

note that the bool there is a little arbitrary but I need to send some type of message and bool seems good enough.

Componentry for Downloading

I need something to actually do the file downloading for me and so I added 2 new projects to the solution;

image

the Downloader project contains the implementation and builds into a new XAP file which will be downloaded on demand whereas the DownloaderContracts project provides interfaces (and supporting types) that can be referenced by my view models in other projects.

I won’t dump out all the source for this here but I’ll include a little class diagram;

image

and so I’ve got a FileDownloader which knows how to download a single file and fires events as it does so. Then I’ve got a SessionDownloader which (badly named) knows how to download all the sessions that the user has selected using the FileDownloader to actually get the work done on a per-file basis.

The SessionDownloader needs the IPDCDataModel in order to be able to GetSessionsForDownload() that reflects the user’s selections and so that will be injected by MEF and the SessionDownloader also needs to export itself via MEF so that other code can take dependencies on it and hence its functionality is abstracted behind ISessionDownloader and it exports itself as an ISessionDownloader.

Adding a new XAP file like this to my project means that I need to remember to update the App.xaml file in my MainShell project otherwise that XAP will never get loaded so I updated that file to include the newly added XAP and make sure that it only loads itself in the out-of-browser case;

<Application.ApplicationLifetimeObjects>
    <xm:AppXapConfiguration XapsDownloaded="OnXapsDownloaded">
      <xm:AppXapConfiguration.Xaps>
        <xm:XapConfiguration
          Name="ViewManagement"
          Source="PDCTutorial.DeferredViewManagement.xap"
          LoadContext="All"/>
        <xm:XapConfiguration
          Name="Messaging"
          Source="PDCTutorial.Messaging.xap"
          LoadContext="OutOfBrowser" />
        <xm:XapConfiguration
          Name="Downloader"
          Source="PDCTutorial.Downloader.xap"
          LoadContext="OutOfBrowser" />
        <xm:XapConfiguration
          Name="DataModel"
          Source="PDCTutorial.DataModel.xap" 
          LoadContext="OutOfBrowser"/>
        <xm:XapConfiguration
          Name="MainShell"
          Source="PDCTutorial.MainShell.xap" 
          LoadContext="All"/>
        <xm:XapConfiguration
          Name="Views"
          Source="PDCTutorial.Views.xap" 
          LoadContext="OutOfBrowser"/>
        <xm:XapConfiguration
          Name="InBrowserViews"
          Source="PDCTutorial.InBrowserViews.xap"
          LoadContext="InBrowser" />
      </xm:AppXapConfiguration.Xaps>
    </xm:AppXapConfiguration>
  </Application.ApplicationLifetimeObjects>

New View and ViewModel for Downloading

Now that I’ve got some componentry for performing my downloads hidden behind ISessionDownloader, I need a new view to present that functionality to the user and I need that view to be hidden by default and to spring into life when it is requested to do so via my little messaging channel named “startDownloadChannel” receiving a message.

The natural way to do this would have been to use a ChildWindow but the work that I’d done around my “deferred view management” was all based around UserControl and so it was a little painful for me to revisit all that at this point and start trying to make it work with some common ancestor of ChildWindow and UserControl and so I didn’t go down the route of using ChildWindow.

I created a new view called DownloadingView which is created the same way as all my other views in this app by just making a UserControl and then having it export itself and import its view model;

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

and then I wired that view into my MainView by adding it as a DeferredView with the intention being to have it Collapsed when it is not in use and switch it to Visible when necessary. This is my MainView.xaml with the new view added such that it will sit over the top of the other content;

<UserControl
  x:Class="PDCTutorial.Views.Views.MainView"
  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:defv="clr-namespace:PDCTutorial.DeferredViewContracts;assembly=PDCTutorial.DeferredViewContracts"
  mc:Ignorable="d"
  d:DesignHeight="300"
  d:DesignWidth="400">
  <Grid
    x:Name="LayoutRoot"
		Background="{StaticResource backgroundBrush}">
    <Grid.ColumnDefinitions>
      <ColumnDefinition
        Width="*" />
      <ColumnDefinition Width="3*"/>
    </Grid.ColumnDefinitions>
    <defv:DeferredView
      ViewName="TracksView" />
    <defv:DeferredView
      Grid.Column="2"
      ViewName="SessionsView" />
    <defv:DeferredView
      ViewName="DownloadingView"
      Grid.ColumnSpan="2"/>
  </Grid>
</UserControl>

I also added a new DownloadingViewModel to support this new view and made it dependent on my IMessageService so that it can “know” when the downloader is being activated and also on my ISessionDownloader so that it can actually drive the downloading process. Both of these are, naturally, injected by MEF.

In this snippet from that class, the set handler for the MessageService property tries to make sure that when the property is set, we sync up a handler to the startDownloadChannel such that when a message arrives on that channel we will change the ViewVisibilty property (which the view binds the visibility of its content to) and we also call the (not shown) StartDownload function to kick off the whole download process;

  [Export("DownloadingViewModel", typeof(object))]
  public class DownloadingViewModel : PropertyChangedNotification
  {
    public DownloadingViewModel()
    {
      this.ViewVisibility = Visibility.Collapsed;

      this.CancelCommand = new ActionCommand(() =>
        {
          this.SessionDownloader.StopDownload();
        });
    }
    public Visibility ViewVisibility
    {
      get
      {
        return (_ViewVisibility);
      }
      set
      {
        _ViewVisibility = value;
        RaisePropertyChanged("ViewVisibility");
      }
    }
    Visibility _ViewVisibility;

    [Import]
    public IMessageService MessageService 
    {
      get
      {
        return (this._messageService);
      }
      set
      {        
        this._messageService = value;

        // We assume that this is a one-off set operation.
        if (this._messageService != null)
        {
          SyncMessageHandlerForDowloadStart();
        }
      }
    }
    IMessageService _messageService;

    void SyncMessageHandlerForDowloadStart()
    {
      var channel = this._messageService.GetChannel<bool>("startDownloadChannel");

      channel.Subscribe(visibility =>
      {
        if (visibility)
        {
          StartDownload();
        }
        this.ViewVisibility = visibility ? Visibility.Visible : Visibility.Collapsed;
      });
    }

This DownloadingViewModel needs to handle a bunch of events from the ISessionDownloader about how the download progress is going in order that this can all be bound onto the screen and so I added a little class to collect all that state together and the DownloadingViewModel offers an instance of that class so that the view can data-bind to it;

image

with all that in place, I added a quick view to present the data (styling and layout can come later) which is bound to all of this data;

<UserControl
  x:Class="PDCTutorial.Views.Views.DownloadingView"
  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">
  <Grid
    x:Name="LayoutRoot"
    Background="#AA000000"
    Visibility="{Binding ViewVisibility}">
    <Grid
      Margin="48"
      Background="Black">
      <Grid.RowDefinitions>
        <RowDefinition />
        <RowDefinition
          Height="Auto" />
      </Grid.RowDefinitions>
      <StackPanel>
        <StackPanel.Resources>
          <Style
            TargetType="TextBlock">
            <Setter
              Property="Foreground"
              Value="White" />
          </Style>
        </StackPanel.Resources>
        <TextBlock
          Text="{Binding CurrentStatus.TotalSessions,
          StringFormat=Downloading a total of \{0\} sessions}" />
        <TextBlock
          Text="{Binding CurrentStatus.TotalFiles,
          StringFormat=Downloading a total of \{0\} files}" />
        <TextBlock
          Text="{Binding CurrentStatus.SessionsCompleted,
          StringFormat=Already done  \{0\} sessions}" />
        <TextBlock
          Text="{Binding CurrentStatus.FilesCompleted,
          StringFormat=Already done  \{0\} files}" />
        <TextBlock
          Text="{Binding CurrentStatus.CurrentSession.Name,
          StringFormat=Current Session Name \{0\}}" />
        <TextBlock
          Text="{Binding CurrentStatus.CurrentSession.TrackName,
          StringFormat=Current Session Track \{0\}}" />
        <TextBlock
          Text="{Binding CurrentStatus.CurrentSession.ThumbnailUrl,
          StringFormat=Current Session Picture URL \{0\}}" />
        <TextBlock
          Text="{Binding CurrentStatus.CurrentSession.Speaker.Name,
          StringFormat=Current Session Speaker \{0\}}" />
        <TextBlock
          Text="{Binding CurrentStatus.CurrentFile,
          StringFormat=Current File \{0\}}" />
        <TextBlock
          Text="{Binding CurrentStatus.KbTotalSize,
          StringFormat=Current File Size \{0\}}" />
        <TextBlock
          Text="{Binding CurrentStatus.KbDownloaded,
          StringFormat=Current File Already Downloaded \{0\}}" />
        <TextBlock
          Margin="0,5,0,0"
          Text="Errors encountered" />
        <ListBox
          ItemsSource="{Binding CurrentStatus.Errors}">
          <ListBox.ItemTemplate>
            <DataTemplate>
              <StackPanel
                Orientation="Horizontal">
                <TextBlock
                  Text="{Binding SessionCode}" />
                <TextBlock
                  Text="{Binding SessionName}" />
                <TextBlock
                  Text="{Binding FileName}" />
                <TextBlock
                  Text="{Binding ErrorMessage}" />
              </StackPanel>
            </DataTemplate>
          </ListBox.ItemTemplate>
        </ListBox>
      </StackPanel>
      <StackPanel
        Grid.Row="1"
        Orientation="Horizontal"
        HorizontalAlignment="Right">
        <Button
          Content="{Binding CancelOrDoneCommandText}" 
          Command="{Binding CancelOrDoneCommand}"/>
      </StackPanel>
    </Grid>
  </Grid>
</UserControl>

and that’s all hanging together reasonably well and the process of downloading seems to work quite nicely albeit the styling leaves a lot to be desired;

image

Where’s the Source?

Here’s the source for download.

What’s Next?

A little bit of styling, a lick of paint on that dialog box and I’m done…