MediaElement and More with WPF

17 במאי 2009

12 תגובות

Download the code for this article here.

In this post I am going to describe a simple video player application written with WPF.

It is not a groundbreaker, but it provides easy-to-copy demonstrations of:

  1. Databinding between a collection of objects and a control.
  2. Writing a DataTemplate for ListBox.ItemTemplate
  3. Databinding between properties of two controls.
  4. Controlling the MediaElement, in particular how to create a Position slider that works.
  5. Writing a ControlTemplate for Button.Template
  6. Animating a property of a Button.

This is what we will build:

image

Application Layout

Let’s start by laying out the application in the XAML Designer:

   <Grid>

      <Grid.ColumnDefinitions>

         <ColumnDefinition Width="200" />

         <ColumnDefinition Width="500" />

      </Grid.ColumnDefinitions>

      <StackPanel Grid.Column="0">

         <ListBox Name="listBox">

         </ListBox>

         <Slider Name="sliderVolume"

                Minimum="0"

                Value="0.5"

                Maximum="1">

         </Slider>

      </StackPanel>

      <Grid Grid.Column="1">

         <Grid.RowDefinitions>

            <RowDefinition Height="*" />

            <RowDefinition Height="Auto" />

         </Grid.RowDefinitions>

         <Border Grid.Row="0"

                BorderBrush="Aqua"

                BorderThickness="2"

                Padding="2">

            <MediaElement Name="player">

            </MediaElement>

         </Border>

         <Grid Margin="10"

              Grid.Row="1">

            <Grid.ColumnDefinitions>

               <ColumnDefinition Width="Auto" />

               <ColumnDefinition Width="*" />

               <ColumnDefinition Width="Auto" />

            </Grid.ColumnDefinitions>

            <TextBlock Name="tbPosition"

                      Grid.Column="0"

                      Margin="2"

                      VerticalAlignment="Center">00:00:00</TextBlock>

            <Slider Name="sliderPosition"

                   Grid.Column="1"

                   Margin="2"></Slider>

            <TextBlock Name="tbDuration"

                      Grid.Column="2"

                      Margin="2"

                      VerticalAlignment="Center">00:00:00</TextBlock>

         </Grid>

      </Grid>

   </Grid>

As you can see, we have a Grid with two columns of fixed size.

The left column hosts a stackpanel with a ListBox on the top and a Slider on the bottom. The ListBox will present all the files in our video folder, and the Slider will control the volume.

The right column hosts another Grid, with two rows. The top row hosts a MediaElement within a Border. On the second row we have the Position control Slider which I placed in a Grid with 3 columns. The Slider itself is in the middle column, with the Position as text in the left column and the Duration as text in the right column.

Databinding Between a Collection of Objects and a Control

I would like the ListBox to present the files in the video folder. This can be done quite easily by binding to the result of a LINQ query in code behind (in the Loaded event of the Window).

        string videoFolder = Properties.Settings.Default.VideoFolder;

 

        private void Window_Loaded(object sender, RoutedEventArgs e)

        {

            listBox.ItemsSource =

                from string fileName in Directory.GetFiles(videoFolder)

                where Path.GetExtension(fileName) == ".wmv"

                select new FileInfo(fileName);

        }

As you can see, I read the videoFolder location from an application setting.

We are binding the ListBox to a list of FileInfo items. Interestingly, this binding works even without setting the DisplayMemberPath property of the ListBox. (The ListBox displays the FullName string property for each item, by default).

Writing a DataTemplate for ListBox.ItemTemplate

In order to control what gets displayed in the ListBox, and, indeed, in order to control every aspect of how that list will look, lets go back to the XAML and write the following DataTemplate for the ListBox:

         <ListBox Name="listBox">

            <ListBox.ItemTemplate>

               <DataTemplate>

                  <Button Width="170"

                         Margin="2"

                         Background="White"

                         HorizontalContentAlignment="Left"

                         Click="Button_Click"

                         Content="{Binding Path=Name}"

                         ToolTip="{Binding Path=Length}">

                  </Button>

               </DataTemplate>

            </ListBox.ItemTemplate>

         </ListBox>

This template is quite straightforward. The ListBox will now create a Button for each FileInfo to which it is bound. I chose a button rather than a Rectangle because I want to be able to handle a Click event (with Button_Click). As we will see, this handler will Play or Stop the MediaElement’s playback of the file associated with this Item.

Apart from being able to control the look of that Button the significant benefit of using the DataTemplate is that we can decide which properties of each item should be used in the representation of that item. In this case, I am binding the Name of the FileInfo (rather than the FullName) to the Content property of the Button and  the Length (the size in bytes) to the ToolTip property. Yes, I know the length in bytes of the file is of no use to anyone, but it demonstrates a principle, I think.

Databinding Between Properties of Two Controls

Next, lets set up the MediaElement – starting with the XAML:

            <MediaElement Name="player"

                         LoadedBehavior="Manual"

                         MediaOpened="mediaElement_MediaOpened"

                         MediaEnded="mediaElement_MediaEnded"

                         Volume="{Binding ElementName=sliderVolume, Path=Value}">

            </MediaElement>

The first thing to notice here is that the LoadedBehavior is set to Manual. This is the mode that allows us to call Play and Stop on the MediaElement in code. I am adding two event handlers that will be fired when a file is opened and when it stops playing, respectively. We will use those in just a moment.

The value given to Volume is the one that contains the data binding. In this case I am using the ElementName option to specify that the source of the data will be another (named) control and not any business object or unnamed control in the logical tree. The Path value of “Value” indicates that the binding source is the Value property of the sliderVolume Slider.

Its important here to make sure that sliderVolume is indeed setup correctly to provide values between 0 and 1 as this is the range of the Volume property.

Controlling The MediaElement

You probably already noticed that I didnt provide a Play or Stop button in this application. Instead, you play a video by clicking on its button in the file list. You stop the video playing by clicking on the same button again. Not perfect, but its one way to go.

I implemented this in the Button_Click event handler as follows:

        private void Button_Click(object sender, RoutedEventArgs e)

        {

            Button prevButton = player.Tag as Button;

            Button button = sender as Button;

            FileInfo fileInfo = button.DataContext as FileInfo;

 

            // If a file is playing, stop it

 

            if (prevButton != null)

            {

                player.Tag = null;

                player.Stop();

                prevButton.Background = Brushes.White;

 

                // if the one thats playing is the one that was clicked -> don't play it

 

                if (prevButton == button)

                    return;

            }

 

            // Play the one that was clicked

 

            player.Tag = button;

            player.Source = new Uri(fileInfo.FullName);

            player.Play();

            button.Background = Brushes.Aqua;

        }

First, note the third line of the method which demonstrates that the DataContext property of each button has been inherited from the ListBoxItem that contains it, and is the business object of type FileInfo that was bound to that item.

This allows us to readily derive the FullName (path) of the file that needs to be played.

Second, note the use of the Tag property of MediaElement (of any FrameworkElement, actually) to store the button that is bound to the currently playing file (or null if none is playing). This is needed so I can change the background of the buttons as files are played and stopped.

Clearly, I didn’t have to use the ‘Tag’ property. I could just as well have used a member of the Window class, but I think this approach is neater and more object oriented.

Creating a Position Slider That Works

It was easy to get that Volume control Slider working because MediaElement has a dependency property called VolumeProperty, but Position is not a dependency property of MediaElement. Moreover, for Position we need two-way binding rather than one way because the Slider should show the progress of the MediaElement as it plays a file.

I would imagine that the reason for not defining a dependency property for Position is to disuade us from binding it to Slider values. Seek operations are CPU intensive and mostly unnecessary during the time you are dragging a Slider.

So, instead, the recommended approach is polling.

Lets add a DispatcherTimer member to the Window and add the following to the Window_Loaded handler.

            timer = new DispatcherTimer();

            timer.Interval = TimeSpan.FromSeconds(1);

            timer.Tick += new EventHandler(timer_Tick);

This is a first draft of the time_Tick routine:

        void timer_Tick(object sender, EventArgs e)

        {

            sliderPosition.Value = player.Position.TotalSeconds;

        }

We call the Start and Stop methods of the timer in the mediaElement_MediaOpened and mediaElement_MediaEnded handlers respectively.

This code efficiently updates the Slider position as the player progresses.

Now for the Seek feature.

First we need to add some event handlers to the sliderPosition object in XAML, like so:

            <Slider Name="sliderPosition"

                   Grid.Column="1"

                   Minimum="0"

                   Margin="2"

                   ValueChanged="sliderPosition_ValueChanged"

                   Thumb.DragStarted="sliderPosition_DragStarted"

                   Thumb.DragCompleted="sliderPosition_DragCompleted"

                   ></Slider>

These two routed events of Track, DragStarted and DragCompleted, are not too conspicuous at first, but they really help solve the Seek problem well (these are Thumb dragging events, not to be confused with Drag and Drop, mouse related events).

Here is the implementations of the handlers (first draft):

        bool isDragging = false;

 

        private void sliderPosition_ValueChanged(

            object sender, RoutedPropertyChangedEventArgs<double> e)

        {

            TimeSpan ts = TimeSpan.FromSeconds(e.NewValue);

            tbPosition.Text =

                String.Format("{0:00}:{1:00}:{2:00}",

                ts.Hours, ts.Minutes, ts.Seconds);

        }

 

        private void sliderPosition_DragStarted(

            object sender, DragStartedEventArgs e)

        {

            isDragging = true;

        }

 

        private void sliderPosition_DragCompleted(

            object sender, DragCompletedEventArgs e)

        {

            isDragging = false;

            player.Position = TimeSpan.FromSeconds(sliderPosition.Value);

        }

As you can see, when the Slider position changes, I only update the tbPosition text, so while dragging the Slider there is no attempt to Seek. Instead, the Seek occurs once when the drag is completed.

This implementation doesnt allow you to see a preview of the video as you drag, but it is efficient and responds well to the user.

There is one more detail that shouldn’t be overlooked. While the slider is being dragged we need to cancel the effect of the timer. This is why I defined and manage the isDragging member. This is the updated timer_Tick routine:

        void timer_Tick(object sender, EventArgs e)

        {

            if (!isDragging)

            {

                sliderPosition.Value = player.Position.TotalSeconds;

            }

        }

 

Before we finish up with some styling in XAML, let me just show you the mediaElement_MediaOpened handler which is called whenever the MediaElement opens a new file for playing:

        private void mediaElement_MediaOpened(object sender, RoutedEventArgs e)

        {

            if (player.NaturalDuration.HasTimeSpan)

            {

                TimeSpan ts = player.NaturalDuration.TimeSpan;

 

                sliderPosition.Maximum = ts.TotalSeconds;

                sliderPosition.SmallChange = 1;

                sliderPosition.LargeChange = Math.Min(10, ts.Seconds / 10);

 

                tbPosition.Text = String.Format("00:00:00");

                tbDuration.Text = String.Format("{0:00}:{1:00}:{2:00}",

                    ts.Hours, ts.Minutes, ts.Seconds);

            }

            timer.Start();

        }

 

In this event we can discover the duration of the file and set the scale for the Slider and the value that is written to the tbDuration TextBlock.

OK, now for some fun with templates and animation…

Writing a ControlTemplate for Button.Template

First, lets replace the insides of the Button inside the ListBox DataTemplate.

The ContentPresenter must stay, thats the placeholder for whatever content we are placing in the button – but everything else can be replaced. There is nothing to exciting here about this Border with its rounded edges, but of course, there is no limit to what you could do instead.

Note the use of the TemplateBinding syntax which allows the template to be configured through properties on the control (Button) itself. In case you were wondering, the ContentPresenter should also really include a TemplateBinding expression binding it to the Content property of the Button. As it so happens, that binding is implicitly present and doesnt need to be written in explicitly.

   <Button Width="170"

          Margin="2"

          Background="White"

          HorizontalContentAlignment="Left"

          Click="Button_Click"

          Content="{Binding Path=Name}"

          ToolTip="{Binding Path=Length}">

      <Button.Style>

         <Style TargetType="Button">

            <Setter Property="Template">

               <Setter.Value>

                  <ControlTemplate TargetType="Button">

                     <Border CornerRadius="5"

                            BorderBrush="Black"

                            Background="{TemplateBinding Background}"

                            BorderThickness="1"

                            Padding="2">

                        <ContentPresenter>

                        </ContentPresenter>

                     </Border>

                  </ControlTemplate>

               </Setter.Value>

            </Setter>

         </Style>

      </Button.Style>

   </Button>

Styles can be used to set the value of any property of their target control, but this one has only one purpose – to set the Template property of the Button to a new ControlTemplate. ControlTemplates offer a much higher level of control over the internals of a Control, allowing us to completely redesign the ones provided with WPF.

Typically Styles are defined in a ResourceDictionary in the scope of an Application, Window or a container control. The reason for this is to allow Styles to be applied to more than one control and to help us build applications that are consistent in their style and easy to restyle. In this case, I have no other use for this particular style, so my basic instinct to encapsulate wherever possible got the upper hand. I therefore used the Element-Property technique in XAML to specify the Style within this Button which is the only control that uses it.

Animating a Property of a Button

When you try the application out for yourself you will no doubt notice that when you press any of the buttons in the playlist the button reacts by slowly expanding in the horizontal direction and shrinking back to its normal size.

In Windows Forms, we would have had to work quite hard to implement this effect, and the implementation would probably really clutter up our code.

In WPF, as I am sure you know, animations (at least simple ones) can be added quite efficiently in XAML. Animations can be activated using an EventTrigger which is contained in a Trigger collection. In this demo I will attach the TriggerCollection to the ControlTemplate that we already designed.

         <ListBox Name="listBox">

            <ListBox.ItemTemplate>

               <DataTemplate>

                  <Button Width="170"

                         Margin="2"

                         Background="White"

                         HorizontalContentAlignment="Left"

                         Click="Button_Click"

                         Content="{Binding Path=Name}"

                         ToolTip="{Binding Path=Length}">

                     <Button.Style>

                        <Style TargetType="Button">

                           <Setter Property="Template">

                              <Setter.Value>

                                 <ControlTemplate TargetType="Button">

                                    <Border CornerRadius="5"

                                           BorderBrush="Black"

                                           Background="{TemplateBinding Background}"

                                           BorderThickness="1"

                                           Padding="2">

                                       <ContentPresenter>

                                       </ContentPresenter>

                                    </Border>

                                    <ControlTemplate.Triggers>

                                       <EventTrigger RoutedEvent="Button.Click">

                                          <BeginStoryboard>

                                             <Storyboard

                                                Storyboard.TargetProperty="Width">

                                                <DoubleAnimation From="170"

                                                                To="180"

                                                                Duration="0:0:0.5"

                                                                AutoReverse="True">

                                                </DoubleAnimation>

                                             </Storyboard>

                                          </BeginStoryboard>

                                       </EventTrigger>

                                    </ControlTemplate.Triggers>

                                 </ControlTemplate>

                              </Setter.Value>

                           </Setter>

                        </Style>

                     </Button.Style>

This animation increases the Width property over 0.5 seconds (and then reverses the operation due to the AutoRevers=True setting.

Now compile the project and run (dont forget to set the VideoFolder setting in App.Config to point to a folder with WMV files).

Enjoy!

Summary

In this article we reviewed samples of the following WPF features:

  1. Writing a DataTemplate for ListBox.ItemTemplate
  2. Databinding between properties of two controls.
  3. Controlling the MediaElement, in particular how to create a Position slider that works.
  4. Writing a ControlTemplate for Button.Template
  5. Animating a property of a Button.

Feel free to send me feedback on this article – and to use the code in whole or portion for your own needs, as you see fit.

You can download the code from here.

הוסף תגובה
facebook linkedin twitter email

כתיבת תגובה

האימייל לא יוצג באתר. (*) שדות חובה מסומנים

12 תגובות

  1. scottman6 בספטמבר 2009 ב 20:42

    There is no active link to download the code…

    להגיב
  2. crashandburn20 באוקטובר 2009 ב 2:05

    Thx man,i did banging my head two days to solve problem with media seeking and with draging slider!

    Good job! ;)

    להגיב
  3. dtcSearch29 בדצמבר 2009 ב 17:54

    You save my life ;)

    thx for your code, before, I've use MediaTimeLine for AutoUpdate the Seeker (Slider), but when I want change the current position of the media, ~Exeception~, a problem with the Clock (isn't null)…

    but now, it's OK, thanks a lot David

    להגיב
  4. 5Rick521 בינואר 2010 ב 20:26

    I am currently learning c# and wpf. Your video player has helped me in many ways. Your LINQ query that listed the the items in a directory was especially helpful. One question I have is "How do I get the next video to play after the current video completes"? Everything I try fails. Thanks again, David

    להגיב
  5. 5Rick521 בינואר 2010 ב 21:57

    my apologies, my question should have read "How I get the next video to play automatically after the current video completes"? Like a playlist. Thank You

    להגיב
  6. Rajendra12 בפברואר 2010 ב 11:18

    this is not working in my system.download your code in my system and run but vedio file not run.

    להגיב
  7. Vladislav Carpenco16 במרץ 2010 ב 23:18

    It is awesome example. Thank you or sharing this technique. I am wondering if adding fullscreen mode to play video is very difficult feature to add to this project. Any ideas on how to approach it. Many thanks in advance.

    להגיב
  8. Sudarsan Srinivasan27 בספטמבר 2010 ב 12:27

    I did not want to do polling on the position, but couldn't help it !!
    Thanks

    להגיב
  9. Jason Wilson16 ביוני 2011 ב 20:15

    Great work here. The only issue I have with this technique is the DispatchTimer causes a hitch in the video when the tick event fires. I've tried other timers to the same effect. Any recommendations?

    להגיב
  10. Jason Wilson17 ביוני 2011 ב 16:22

    Nevermind on the video hitching. I had code in place to support clicking on the slider to move video position. The code would pause the video before moving the slider — when the timer updated the video there was a brief pause that was sometimes perceptable. I've removed the pause and added a private variable that would keep the click update from conflicting with the timer update

    להגיב
  11. harsha24 בפברואר 2012 ב 14:24

    when creating timer for seekbar exception is occuring.exactly we using "string"to texxtbox then error occuring.how can i solve that error

    להגיב