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.
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.
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>
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 theFeedName
property fromApp.Inner TubeFeeds
usingText="{Binding Path=FeedName}"
as the binding expression.
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>
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.
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>
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 justFeedVideos
, so we'll setItemSource
to"{Binding Path=FeedVideos}"
. TheFeedVideos
property is simply a collection ofInnerTubeVideo
classes, which means that we can set theBinding Path
to any property in theInnerTubeVideo
class.-
IsSynchronizedWithCurrentItem
Just like before, we must be set this to true so that all of the controls on the window stay in sync.
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.
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
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.
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.
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>
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.
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.
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.
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>
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) ...
The final thing we'll look at is the ToggleButton Click
event, which is declared as shown in Example 4-75.
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.