Dissecting MainWindow.xaml

Since it can be difficult to explain exactly how you take a set of skeleton controls and turn them into a fully functioning UI, we'll dissect MainWindow.xaml into the three pieces, labeled 1, 2, and 3 in Figure 4-12. You can refer back to this screenshot to follow along with the expected UI.

The three Grid columns in MainWindow.xaml

Figure 4-12. The three Grid columns in MainWindow.xaml

Setting the MainWindow.xaml data context

Previously we defined a global variable named InnerTubeFeeds that holds the list of feeds and the corresponding list of videos. To data bind all of the controls in Main Window to InnerTubeFeeds, we will set the DataContext for the window using the Window_Loaded event, as shown in Example 4-55 and Example 4-56.

Example 4-55.  C# code to set the MainWindow.xaml data context

private void Window_Loaded(object sender, RoutedEventArgs e)
{
...
    this.DataContext = App.InnerTubeFeeds;
}

Example 4-56.  Visual Basic code to set the MainWindow.xaml data context

Private Sub Window_Loaded(ByVal sender As Object, ByVal e As RoutedEventArgs)
...
  Me.DataContext = App.InnerTubeFeeds
 End Sub

Part 1: The Feed List ListBox

For the Feed List UI, we will use a templated ListBox control, as shown in Example 4-57. As the main UI is comprised of a Grid control, we need to add the control to the first column of the Grid control by setting the Grid.Column attribute for ListBox to zero (the first column). We won't discuss this in detail here, but you'll notice that we have a ContextMenu defined to delete a feed (when right-clicked). Next, we'll delve into how we data bind using the ListBox ItemTemplate.

Example 4-57. XAML code for the first column in MainWindow.xaml

<!--First Column-->
<ListBox Grid.Column="0" x:Name="feedList" IsSynchronizedWithCurrentItem="True"
  ItemsSource="{Binding}"  Background="{x:Null}">
  <ListBox.ContextMenu>
    <ContextMenu>
      <MenuItem Click="DeleteFeed" CommandParameter="{Binding Path=/}"
        Header="Delete Feed" >
        <MenuItem.Icon>
          <Image Source="images/cross.png"></Image>
        </MenuItem.Icon>
      </MenuItem>
    </ContextMenu>
  </ListBox.ContextMenu>
  <ListBox.ItemTemplate>
    <DataTemplate>
      <StackPanel Orientation="Horizontal">
        <TextBlock FontWeight="Bold" Text="{Binding Path=FeedName}"/>
      </StackPanel>
    </DataTemplate>
  </ListBox.ItemTemplate>
</ListBox>

Data binding the feedList ListBox

To data bind the feedList ListBox control, we will set a couple of properties:

ItemSource

This is the data source you are binding to. Since we already set up a DataContext for all of MainWindow.xaml, this is just "{Binding}".

IsSynchronizedWithCurrentItem

This property must be set to true so that all of the controls on the window stay in sync, meaning that if a user clicks on another feed, the ListBox control that contains the list of videos and the details pane controls should update to show the videos in that feed.

DataTemplate

This template controls how each data-bound item will be displayed. In this example, we can data bind the Text property to the FeedName property from App.Inner TubeFeeds using Text="{Binding Path=FeedName}" as the binding expression.

Part 2: The VideoList ListBox

In the middle column, we will display a list of videos for the selected feed, including a thumbnail image, the video title, and the author of each video that belongs to the currently selected feed. Just like we did before, we declare the ListBox control and define the Grid.Column property, but this time we set it to 1, which is the second column. We also have a ContextMenu control that is bound to the Delete key by setting the InputGestureText, and we have a ListBox ItemTemplate that defines the UI for the ListBox, as shown in Example 4-58.

Example 4-58. XAML code for the second column in MainWindow.xaml

<!--Second Column-->
<ListBox Grid.Column="1"  IsSynchronizedWithCurrentItem="True"
  ItemsSource="{Binding Path=FeedVideos}" Name="VideoList"
  Background="{x:Null}" >
  <ListBox.ItemTemplate>
    <DataTemplate>
      <StackPanel Orientation="Horizontal">
        <StackPanel.ContextMenu>
          <ContextMenu Name="mnuDeleteVideo">
            <MenuItem Click="DeleteVideo" CommandParameter="{Binding}"
              Header="Foo" InputGestureText="Del" >
              <MenuItem.Icon>
                <Image Source="images/cross.png"></Image>
              </MenuItem.Icon>
            </MenuItem>
          </ContextMenu>
        </StackPanel.ContextMenu>
        <Image Margin="2,2,2,2" Source="{Binding Path=DownloadedImage,
          Converter={StaticResource img} }"
          Width="48" Height="48" VerticalAlignment="Center"></Image>
        <StackPanel VerticalAlignment="Center">
          <TextBlock  FontWeight="Bold" Text="{Binding Path=Title}"
            TextTrimming="WordEllipsis"  TextWrapping="Wrap" />
          <TextBlock Foreground="White"  Text="{Binding Path=Author}" />
        </StackPanel>
      </StackPanel>
    </DataTemplate>
  </ListBox.ItemTemplate>
</ListBox>

The Data Template UI

Let's take a second to understand how we get XAML to produce a template that looks like Figure 4-13. All of the controls in the data template are defined in a StackPanel with the orientation set to Horizontal, meaning they will stack left to right.

Starting left to right in Example 4-59, we have an image, which itself has a margin of 2 (device-independent) pixels on each side, and a fixed width and height. The next control is another StackPanel control, and because it doesn't explicitly have an Orientation, this means the Orientation is vertical (top to bottom). Inside the StackPanel we have two TextBlock controls, representing the title and author of the video.

The ListBox data template

Figure 4-13. The ListBox data template

Example 4-59. XAML data template for video list ListBox

<DataTemplate>
...
  <StackPanel Orientation="Horizontal">
    <Image Margin="2,2,2,2" Source="{Binding Path=DownloadedImage,
      Converter={StaticResource img} }"
      Width="48" Height="48" VerticalAlignment="Center">
    </Image>
    <StackPanel VerticalAlignment="Center">
      <TextBlock  FontWeight="Bold" Text="{Binding Path=Title}"
        TextTrimming="WordEllipsis"  TextWrapping="Wrap" />
      <TextBlock Foreground="White"  Text="{Binding Path=Author}" />
    </StackPanel>
  </StackPanel>
</DataTemplate>

Data Binding the Video List ListBox

Just like we did with the Feed List ListBox, we'll set a couple of properties to enable data binding for the Video List:

ItemSource

As the top-level data binding is set to InnerTubeFeeds, we want to bind to just FeedVideos, so we'll set ItemSource to "{Binding Path=FeedVideos}". The FeedVideos property is simply a collection of InnerTubeVideo classes, which means that we can set the Binding Path to any property in the InnerTubeVideo class.

IsSynchronizedWithCurrentItem

Just like before, we must be set this to true so that all of the controls on the window stay in sync.

Binding to an Image

Take a second to examine the binding syntax for the Image and the two TextBlock controls in Example 4-59. While the TextBlock controls simply bind to the Title and Author properties, the Image binding adds something new, a Converter and a Static Resource. We'll quickly explain what's going on here and how to use converters and static resources.

Building the Image Value Converter

We're using a converter for the image because we want to have some custom code run when we data bind, to make sure that the DownloadedImage value actually exists; if it doesn't, we can swap in a default image. A Converter is a .NET class that implements an interface named IValueConverter. IValueConverter has two methods, Convert and ConvertBack, which both receive a value of type object and have a return type of object, as shown in Example 4-60 and Example 4-61.

Example 4-60.  C# code for the IValueConverter interface

public interface IValueConverter
{
  object Convert(object value, Type targetType,
    object parameter, CultureInfo culture);

  object ConvertBack(object value, Type targetType,
    object parameter, CultureInfo culture);
}

Example 4-61.  Visual Basic code for the IValueConverter interface

Public Interface IValueConverter
  Function Convert(ByVal value As Object, ByVal targetType As Type, _
    ByVal parameter As Object, ByVal culture As CultureInfo) As Object
  Function ConvertBack(ByVal value As Object, ByVal targetType As Type, _
    ByValparameter As Object, ByVal culture As CultureInfo) As Object
End Interface

The next piece of code defines our implementation for IValueConverter. Let's explain exactly what will happen when we data bind. Since we declared the Converter for the Image source, WPF will first pass the value of DownloadedImage property to Converter Image.Convert. The Convert method will check whether the DownloadedImage value is null, empty, or doesn't exist. If it does exist, it'll just return the image path. If the image doesn't exist, then we'll instead return a default "placeholder" image. The full code for the ConverterImage class is shown in Example 4-62 and Example 4-63.

Example 4-62.  C# code for the Image Converter

[ValueConversion(typeof(object), typeof(string))]
public class ConverterImage : IValueConverter
{
public object Convert(object value, Type targetType, object parameter,
  System.Globalization.CultureInfo culture)
{
  string path = (string)value;
  return SetImage(path);
}

public static object SetImage(string imagePath)
{
  //if the path doesn't exist
  if (String.IsNullOrEmpty(imagePath) || (!File.Exists(imagePath)))
  {
    //use default image
    return FileHelper.DefaultImage;
  }
  else
  {
    return imagePath;
  }
}

public object ConvertBack(object value, Type targetType,
  object parameter, System.Globalization.CultureInfo culture)
  {
    // we don't intend this to ever be called
    return null;
  }
}

Example 4-63.  Visual Basic code for the Image Converter

<ValueConversion(GetType(Object), GetType(String))> _
Public Class ConverterImage
  Implements IValueConverter

Public Function Convert(ByVal value As Object, ByVal targetType As Type, _
  ByVal parameter As Object, _
  ByVal culture As System.Globalization.CultureInfo) As Object
  Implements IValueConverter.Convert

  Dim path As String = CStr(value)
    Return SetImage(path)
End Function

Public Shared Function SetImage(ByVal imagePath As String) As Object
  'if the path doesn't exist
  If String.IsNullOrEmpty(imagePath) OrElse ((Not File.Exists(imagePath))) Then
     'use default image
     Return Path.Combine(App.Settings.SubPath, FileHelper.DefaultImage)
  Else
    Return imagePath
  End If
End Function

Public Function ConvertBack(ByVal value As Object, ByVal targetType As Type, _
  ByVal parameter As Object, _
  ByVal culture As System.Globalization.CultureInfo) As Object
  Implements IValueConverter.ConvertBack
  ' we don't intend this to ever be called
  Return Nothing
 End Function
End Class

Setting Up a Class As a Static Resource

Now that we've seen how to define a class that implements IValueConverter, let's show how you can call that class declaratively using XAML by setting it as a Static Resource in MainWindow.xaml. A static resource is a way to define a class or other resource file in XAML. We can set the ConverterImage class shown earlier to be a Static Resource by doing a couple of things. First, we need to explicitly define the namespace that the ConverterImage class lives in and set up an alias for the namespace, which I chose to be Util, as shown in Example 4-64.

Example 4-64. Code to declare a .NET Namespace in XAML

<Window
        xmlns:Util="clr-namespace:InnerTube"

Second, we define the ConverterImage class as a StaticResource by declaring it in a ResourceDictionary in the format shown in the next code example. Notice that we reference the InnerTube namespace (aka Util), followed by the class name, and then define the Key that we can use to reference the StaticResource in XAML, as shown in Example 4-65.

Example 4-65. Code to declare a class and assign a key in XAML

<Window.Resources>
<ResourceDictionary>
     <!-- Value Converters for databinding-->
        <Util:ConverterImage x:Key="img" />
    </ResourceDictionary>
</Window.Resources>

Part 3: The Details Pane

As is evident by the name, the details pane displays the details of the currently selected InnerTubeVideo. The pane itself is made up of a Canvas element with a Grid.Column property of 2, which indicates the third column since the Grid Index starts at zero. The Canvas is made up of a set of controls that define their Canvas.Top and Canvas.Left properties, as shown in Example 4-66.

Note

Experienced WPF developers normally don't like using the Canvas control, as it has fixed pixel values and doesn't resize as gracefully as other controls. But it is fine for some scenarios, and there are benefits of using Visual Studio's snap lines to align labels and their corresponding values without having to manually muck with XAML.

Example 4-66. XAML code for the details pane

<!-- 3rd Column -->
<Canvas Grid.Column="2" Name="canvas1">
  <!-- Title -->
<TextBlock Text="{Binding Path=FeedVideos/Title}" Name="VideoTitle"
  TextAlignment="Center" Height="28.453" Canvas.Left="18.859" Canvas.Top="9.228"
  Width="438.133" FontSize="16" FontWeight="Bold"></TextBlock>

  <!-- Video -->
<Util:MediaPlayer InnerTubeVideoFile="{Binding Path=FeedVideos/}"
  x:Name="VideoPlayer" Canvas.Left="18.571" Canvas.Top="47.678" Height="398.342"
  Width="438.421" />

  <!-- Description -->
  <ScrollViewer Canvas.Left="32.867" Canvas.Top="458.425" Height="86.684"
  Name="scrollViewer1" Width="424.413">
  <TextBlock Height="180" Text="{Binding Path=FeedVideos/Description}"
    Name="txtDescription" TextDecorations="None" TextWrapping="Wrap" />
  </ScrollViewer>

  <!-- Author -->
  <Label Canvas.Left="46" Canvas.Top="551.777" Height="28.339" Name="lblAuthor"
    Width="120" HorizontalContentAlignment="Right">Author:</Label>
  <TextBlock Text="{Binding Path=FeedVideos/Author}" Canvas.Left="173"
    Canvas.Top="559.209" Height="20.907" Name="txtAuthor" Width="120"/>

  <!-- Views -->
  <Label Canvas.Left="46" Canvas.Top="580.116" Height="28.339" Name="lblViews"
    Width="120" HorizontalContentAlignment="Right">Views:</Label>
  <TextBlock Text="{Binding Path=FeedVideos/Views,StringFormat=N0 }"
    Name="txtViews" Canvas.Left="173" Canvas.Top="587.548" Height="20.907"
    Width="120"/>

  <!-- Average Rating -->
  <Label Canvas.Left="46" Canvas.Top="610.122" Height="28.339" Name="lblRaters"
    Width="120" HorizontalContentAlignment="Right">Average Rating: </Label>
  <Util:RatingUserControl StarRating="{Binding Path=FeedVideos/AvgRating}"
    x:Name="Rating" Canvas.Left="173" Canvas.Top="615.123" Height="16.67" />

  <!-- Number of Ratings -->
  <Label Name="lblNumRating" Canvas.Left="46" Canvas.Top="636.794" Height="28.339"
    Width="120" HorizontalContentAlignment="Right"># of Ratings:</Label>
  <TextBlock Text="{Binding Path=FeedVideos/NumRaters, StringFormat=N0}"
    Name="txtNumRating" Canvas.Left="173" Canvas.Top="641.795" Height="21.671"
    Width="120" />

  <!-- Publish Date -->
  <Label Canvas.Left="46" Canvas.Top="660.132" Height="28.339" Name="lblPublished"
    Width="120" HorizontalContentAlignment="Right">Published:</Label>
  <TextBlock Text="{Binding Path=FeedVideos/Published}" Name="txtPublished"
    Canvas.Left="173" Canvas.Top="665.133" Height="21.671" Width="216" />
</Canvas>

Formatting Numbers in WPF

For fields such as txtViews and txtNumRating, we can format the values using the StringFormat property and set its value to a valid .NET Framework formatting string such as N0 (numbers with commas), as shown in Example 4-67.

Example 4-67. Data binding the total views for a video using a value converter

<TextBlock Text="{Binding Path=FeedVideos/Views,StringFormat=N0 }" ...

As the txtViews property is an int data type, if we were to data bind without the StringFormat property, we would get the following example value: 8675309.

After setting the StringFormat property, the txtViews property will appear as 8,675,309.

Data Binding in the Details Pane

Unlike the previous two columns, the details pane is made up of separate, unlinked controls, so we do not define an ItemSource or set the IsSynchronizedProperty.

As a result, the binding syntax for each of the controls is slightly different. Specifically, each Binding Path will start at the root of the InnerTubeFeed class, so to get to the title of a particular video, we need to set the path starting at the FeedVideos property, as in Example 4-68.

Example 4-68. Data binding the video title

<TextBlock Text="{Binding Path=FeedVideos/Title}" ...

Data Binding to User Controls

Back in the details pane XAML listing, you'll notice that for actually playing the video, we are data binding to a custom user control named MediaPlayer, which has a custom property named InnerTubeVideoFile. As you can see, the Path value is set to FeedVideos/, which means that we will pass in the entire currently selected Inner TubeVideo class to the InnerTubeVideoFile property when we data bind, as shown in Example 4-69.

Example 4-69. XAML code to pass an InnerTubeVideo to the MediaPlayer user control

<Util:MediaPlayer InnerTubeVideoFile="{Binding Path=FeedVideos/}"

MediaPlayer User Control UI

The UI for the MediaPlayer user control is a Grid that contains four elements. The first two are the Image and MediaElement controls, which are placed directly on top of each other (notice the same Margin values for both), as shown in Example 4-70. We will use the Image control to display a video's preview image when the control first loads, but once a video starts playing, we will hide the image. As both of the elements are stacked right on top of each other, we can use the Grid.ZIndex attribute to state that the Image, which has a higher ZIndex, should appear on top of the MediaElement.

Next is a StackPanel control, which contains one element, a ToggleButton to toggle between playing and pausing a video. Inside of it is a Path, which is an XAML shape for the Play button.

Example 4-70. XAML for the MediaPlayer user control

<Grid>
  <Image Grid.ZIndex="1" Margin="5,0,5,73" Name="PreviewImage" ></Image>
  <MediaElement Grid.ZIndex="0" Margin="5,0,5,73" Name="VideoPlayer"
    LoadedBehavior="Manual"></MediaElement>
  <StackPanel Orientation="Horizontal" HorizontalAlignment="Center"
    VerticalAlignment="Bottom">
    <ToggleButton Margin="0,0,0,10" Height="50" Width="50"
      Name="PlayButton" Style="{StaticResource PlayButton}"
      Click="Play">
      <Path Data="F1M149.333,406L149.333,598 317.333,502 149.333,406z"
        Fill="#80000000" Height="14" Margin="4,0,0,0" Name="Path" Stretch="Fill"
        Width="14" />
    </ToggleButton>
  </StackPanel>
</Grid>

MediaPlayer Data Binding

To enable data binding with the MediaPlayer control, we need to create a special type of WPF property known as a dependency property, which enables you to make your controls data-bindable in WPF (among other benefits).

You can see the definition for the InnerTubeVideoFile dependency property in the MediaPlayer code-behind file shown next. The first step is to build a property like you normally would, with the getter returning the DependencyProperty value (C# developers should not use C# 3.0's automatic properties for this), as shown in Example 4-71 and Example 4-72.

Next, you declare and register the dependency property, passing in the name, the type you'll receive, the class you are registering for, and finally an event handler that will fire when the value is changed. This event handler replaces the set code in the property because that code will never execute, and therefore change values should be handled via the event handler.

Example 4-71.  C# code for the InnerTubeVideoFile dependency property

public InnerTubeVideo InnerTubeVideoFile
{
  get { return (InnerTubeVideo)GetValue(InnerTubeVideoProperty); }
  set { /* don't write code here it won't execute */ }
}

public static readonly DependencyProperty InnerTubeVideoProperty =
  DependencyProperty.Register("InnerTubeVideoFile",
  typeof(InnerTubeVideo),
  typeof(MediaPlayer),
  new UIPropertyMetadata(MediaPlayer.InnerTubeVideoFileChanged));

Example 4-72.  Visual Basic code for the InnerTubeVideoFile dependency property

Public Property InnerTubeVideoFile() As InnerTubeVideo
  Get
    Return CType(GetValue(InnerTubeVideoProperty), InnerTubeVideo)
  End Get
  Set(ByVal value As InnerTubeVideo)
    'Don't write code here it won't execute
  End Set
End Property

Public Shared ReadOnly InnerTubeVideoProperty As DependencyProperty = _
  DependencyProperty.Register("InnerTubeVideoFile", _
  GetType(InnerTubeVideo), _
  GetType(MediaPlayer), _
  New UIPropertyMetadata(AddressOf MediaPlayer.InnerTubeVideoFileChanged))

Let's now take a look at a snippet of the InnerTubeVideoFile event handler, which will fire any time the property changes (Example 4-73 and Example 4-74). There are a couple of things to notice here. First, the event is static and therefore doesn't have access to instance members of the current MediaPlayer control (using this.foo will not work in the event handler). The nice thing is that the current instance class is passed into you as the DependencyObject d, which you can then cast to a MediaPlayer and set instance properties.

The second thing to note is that we can retrieve the passed-in class and cast it to an InnerTubeVideo through the DependencyPropertyChangedEventArgs e variable.

Now that we have the current instance and the new value, we can set the properties for the Image and the MediaElement controls, as shown in the code snippets.

Note

The MediaElement control does not support MP4 video playback, so we must data bind to a Windows Media (WMV) file.

Example 4-73.  Snippets from the C# event that fires when the InnerTubeVideFile property changes

private static void InnerTubeVideoFileChanged(DependencyObject d,
  DependencyPropertyChangedEventArgs e)
{
  MediaPlayer player = (MediaPlayer)d;
  InnerTubeVideo newVideo = (InnerTubeVideo)e.NewValue;
...
  ImageSourceConverter imageConvert = new ImageSourceConverter();
...
  player.PreviewImage.Source =
 (ImageSource)imageConvert.ConvertFromString(newVideo.DownloadedImage);
...
  //Set Video File
  player.VideoPlayer.Source = new Uri(newVideo.DownloadedWmv);
...

Example 4-74.  Snippets from the Visual Basic event that fires when the InnerTubeVideoFile property changes

Private Shared Sub InnerTubeVideoFileChanged(ByVal d As DependencyObject, _
  ByVal e As DependencyPropertyChangedEventArgs)
  Dim player As MediaPlayer = CType(d, MediaPlayer)
  Dim newVideo As InnerTubeVideo = CType(e.NewValue, InnerTubeVideo)
...
  Dim imageConvert As New ImageSourceConverter()
...
  player.PreviewImage.Source = CType(imageConvert.ConvertFromString( _
    newVideo.DownloadedImage), ImageSource)
...
  'Set Video File
  player.VideoPlayer.Source = New Uri(newVideo.DownloadedWmv)
...

Playing and Pausing Video

The final thing we'll look at is the ToggleButton Click event, which is declared as shown in Example 4-75.

Example 4-75. Declaring the ToggleButton in XAML

< ToggleButton ... Name="PlayButton" Click="Play">

In the Play event, we can cast the ToggleButton to see whether it's currently checked, and do things such as hide the preview image, as shown in Example 4-76 and Example 4-77. The MediaElement control has static methods to Play() and Pause() audio and video files, assuming a source has been set.

Example 4-76.  C# code for the ToggleButton's Click method

void Play(object sender, RoutedEventArgs args)
{
  ToggleButton tb = (ToggleButton)sender;
  if (isVideo)
  {
    //Hide image if it's a video
    this.PreviewImage.Visibility = Visibility.Hidden;
    if ((bool)tb.IsChecked)
    {
      VideoPlayer.Play();
    }
    else
    {
      VideoPlayer.Pause();
    }
  }
}

Example 4-77.  Visual Basic code for the ToggleButton's Click method

Private Sub Play(ByVal sender As Object, ByVal args As RoutedEventArgs)
  Dim tb As ToggleButton = CType(sender, ToggleButton)
  If isVideo Then
    'Hide image if it's a video
    Me.PreviewImage.Visibility = Visibility.Hidden
    If CBool(tb.IsChecked) Then
      VideoPlayer.Play()
    Else
      VideoPlayer.Pause()
    End If
  End If
 End Sub

Get Coding4Fun now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.