NOTE: This article is outdated due to changes in the latest version of RX Extensions.
Introduction
This project demonstrates an implementation of the View Model Style pattern to create a fully "designable" Silverlight video player. This is not to be confused with a "skinable" video player. A skinable video player allows you to change the look and feel of the buttons that control the player. A designable player allows a designer to use any set of controls to implement the video player.
The View Model Style pattern allows a programmer to create an application that has absolutely no UI. The programmer only creates a ViewModel and a Model. A designer with no programming ability at all is then able to start with a blank page and completely create the View (UI) in Microsoft Expression Blend 4 (or higher).
Note: to build the project, you may need to install the Silverlight RX Extensions from http://msdn.microsoft.com/en-us/devlabs/ee794896.aspx.
The Power of View Model Style
This Silverlight project is not a full featured video player, but it actually works, and hopefully demonstrates a non-trivial example of a View Model Style Silverlight project. Certain functions such as the pause button, full screen support, and skip ahead were left out to simplify the example code as much as possible.
If you are new to View Model Style, it is suggested that you read Silverlight View Model Style: An (Overly) Simplified Explanation for an introduction.
View Model Style
Much has been written about the View Model Style pattern, and there are various interpretations. In this example, we will implement the pattern with as little code as possible. We will try to present it in the easiest possible way. The Model and the View of the View Model Style are very simple. The Model contains Web Services, and the View is the UI (that is created in Expression Blend with no coding). The only complex part is the ViewModel. In the ViewModel, all functionality will be implemented using:
- Properties - One of something. This could be a
String
or an Object
. Implements INotifyPropertyChanged
so that any element bound to it is automatically notified whenever it changes.
- Collections - A collection of something. This is of type
ObservableCollection
so that any element bound to it is automatically notified whenever it changes.
- Commands - An event that can be raised. Also, one parameter of type
Object
can be passed. This implements ICommand
.
For the commands, we will only need to use the InvokeCommandAction
behavior.
That's it. This is the "simplified" View Model Style.
A Silverlight View Model Style Video Player
Let's first start with the experience a designer would have in using the View Model Style pattern to completely redesign the video player.
When you download the attached code, you will need to place some .wmv videos in the Video folder.
When you run the project, the Web Service in the web project will detect what videos are in the folder and pass them to the Silverlight application.
- The list of videos will show in the combo box dropdown.
- You can select a video from the combo box and then click the Play button to play the video.
- A progress bar will show the current progress.
- The exact location of the video will display above the video.
- The current and total time of the video will also display.
- The Stop button will stop the video.
When you open the project in Expression Blend and open the MainView.xaml file, click on the Data tab...
You will see that there is a Data Context section. The Data Context is set to the ViewModel, and displays all the Properties, Collections, and Commands that the UI can interact with. The designer only needs to interact with these elements to implement their own video player.
The image above shows what is bound to what. However, note that while buttons are bound to, and raise ICommand
s, practically anything can raise an ICommand
, it does not have to be a button. In the tutorial TreeView SelectedItemChanged Using View Model Style, a TreeView
control is used to raise the ICommand
.
Redesign the Video Player
We can delete the existing MainPage.xaml file...
... and create a new one.
If we click on LayoutRoot in the Objects and Timeline window...
... and click on the New button next to DataContext...
... and select MainViewModel and click OK:
When we click on the Data tab...
... we will see the Data Context has been set for the page.
Let me repeat this important point: We will not need to create a single line of code to implement the video player.
Create the UI
The one control that is required is the MediaElement
. In Expression Blend:
- Click the Asset button
- Locate the
MediaElement
- Drag it to the design surface
Make sure you set the AutoPlay to false by un-checking the box in the Properties for the MediaElement
.
I went to the Alan Beasley article: http://www.codeproject.com/KB/expression/ArcadeButton.aspx and stole a button (he really does good work; when you click on the button, it actually animates and behaves like a real button).
I then created the "Masterpiece" you see above. Of course, anyone could do better. I just wanted to show how radically different the UI could be, yet still work without any code changes.
Wire-up the MediaElement - Learn to "Think View Model Style"
The UI is created, all we need to do is bind the UI elements to the ViewModel and the application is complete.
To understand how we bind the UI elements to the ViewModel, we need to learn a few things to "Think Like View Model Style". Basically, we:
- Bind UI elements to Properties or Collections, and when the ViewModel puts something in those Properties or Collections, events will automatically be raised on the bound UI elements.
- We can use the
InvokeCommand
behavior to respond to particular events and raise other events in the ViewModel.
In this application, we need to wire-up the MediaElement
to the ViewModel. To do this, we have to:
- Bind the
MediaElement
to the SelectedVideoProperty
(URI). This will cause the MediaElement
to always play the video that the ViewModel sets to that property.
- Use an
InvokeCommand
behavior to raise the MediaOpenedCommand
(ICommand
). This behavior is configured to fire when the MediOpened
event is raised on the MediaElement
(this happens automatically when the MediaElement
is ready to play the video). The behavior also passes a reference to the MediaElement
as a parameter to the ViewModel (the ViewModel will use this reference to the MediaElement
when other commands such as Start and Stop are called).
Let's walk through these steps:
In the Objects and Timeline window, select the MediaElement
.
In the properties for the MediaElement
, select the Advanced options for Source.
Select Data Binding...
Set it to SelectedVideoProperty
and click OK.
You will know an element is bound because it will have a gold box around it.
In Assets, click and drag an InvokeCommand
behavior...
... and drop it under the MediaElement
in the Objects and Timeline window.
In the properties for the behavior, set MediaOpened
for the EventName and click the Data bind button next to Command.
Select MediaOpenedCommand and click OK.
Select Advanced options for CommandParameter:
Select Data Binding...
Click the Element Property tab, and select MediaElement
, and click OK.
That was the hard part. The rest is just binding the remaining Properties, Collections, and Commands.
Binding Properties and Collections
These are the remaining Properties and Collections that need to be bound.
For example, the ProgressBar
control has its Value
and Maximum
properties set to CurrentPositionProperty
and TotalDurationProperty
, respectively.
The ListBox
control is naturally bound to SilverlightVideoList
, but the SelectedIndex
for the ListBox
is also bound to SelectedVideoInListProperty
. The reason for this is that you cannot set the Selectedindex
until after the list has been loaded.
The ViewModel fills the SilverlightVideoList
, and only after it has filled that collection does it check to see if there are any items, and if there are, sets the Selectedindex
value.
Binding Commands
These are the remaining Commands that need to be bound. To bind them, simply drop an InvokeCommand
behavior on the control and set the properties to the respective commands.
For example, the ListBox
uses an InvokeCommand
behavior to call the SetVideoCommand
when its SelectionChanged
event is raised.
Its CommandParameter
is bound to the SelectedItem
of the ListBox
.
That's about it. If you're a designer, you can skip the next section.
The Code
The website part of the code is simply a website with a Web Service that returns a list of any video that is in the Videos folder.
The Silverlight project consists of only a few files. The files that are important at this point are:
- DelegateCommand.cs - A helper file that allows us to easily create
ICommand
s (see: Silverlight View Model Style File Manager for a full explanation of this file).
- SilverlightVideos.cs - This is the Model. It consists of a single Web Service method.
- MainViewModel.cs - This is the ViewModel. This is where all the "magic" happens.
The Model
The Model for this application consists of a single web method in the SilverlightVideos.cs file. The "twist" is that we are using RX Extensions that call the Web Service and return an IObservable
.
public static IObservable<IEvent<GetVideosCompletedEventArgs>> GetVideos()
{
WebServiceSoapClient objWebServiceSoapClient =
new WebServiceSoapClient();
EndpointAddress MyEndpointAddress = new
EndpointAddress(GetBaseAddress());
objWebServiceSoapClient.Endpoint.Address = MyEndpointAddress;
IObservable<IEvent<GetVideosCompletedEventArgs>> observable =
Observable.FromEvent<GetVideosCompletedEventArgs>(
objWebServiceSoapClient, "GetVideosCompleted");
objWebServiceSoapClient.GetVideosAsync();
return observable;
}
The ViewModel
The ViewModel is where all the Properties, Collections, and Commands are created.
This is the constructor of the ViewModel:
public MainViewModel()
{
MediaOpenedCommand = new DelegateCommand(MediaOpened, CanMediaOpened);
PlayVideoCommand = new DelegateCommand(PlayVideo, CanPlayVideo);
StopVideoCommand = new DelegateCommand(StopVideo, CanStopVideo);
SetVideoCommand = new DelegateCommand(SetVideo, CanSetVideo);
GetListOfVideos();
}
It sets up the Commands by using the DeleagateCommand
method (from the DelegateCommand.cs file) to create ICommand
s. Next, it calls the GetListOfVideos()
method.
private void GetListOfVideos()
{
SilverlightVideos.GetVideos().Subscribe(p =>
{
if (p.EventArgs.Error == null)
{
foreach (string Video in p.EventArgs.Result)
{
SilverlightVideoList.Add(Video);
}
if (SilverlightVideoList.Count > 0)
{
SelectedVideoInListProperty = 0;
}
}
});
}
This method calls the GetVideos()
method in the Model that fills the SilverlightVideoList
collection. It also sets the SelectedVideoInListProperty
property if there are videos in the collection.
The SilverlightVideoList
collection is a simple ObservableCollection
:
private ObservableCollection<string> _SilverlightVideoList =
new ObservableCollection<string>();
public ObservableCollection<string> SilverlightVideoList
{
get { return _SilverlightVideoList; }
private set
{
if (SilverlightVideoList == value)
{
return;
}
_SilverlightVideoList = value;
this.NotifyPropertyChanged("SilverlightVideoList");
}
}
When the SelectedVideoInListProperty
property is set, the UI control that is bound to it (the listbox or the combo drop down box) should raise the SetVideoCommand
command that sets the SelectedVideoProperty
:
public ICommand SetVideoCommand { get; set; }
public void SetVideo(object param)
{
string tmpSelectedVideo = string.Format(@"{0}/{1}",
GetBaseAddress(), (String)param);
SelectedVideoProperty = new Uri(tmpSelectedVideo,
UriKind.RelativeOrAbsolute);
progressTimer.Stop();
}
private bool CanSetVideo(object param)
{
return (param != null);
}
The MediaElement
is bound to the SelectedVideoProperty
, and will automatically start loading the video. When it has opened the video, it will call the MediaOpenedCommand
command that will:
- Pass an instance of the
MediaElement
as a parameter so it can be stored in a private variable in the ViewModel (this will be used when the Start and Stop commands are raised).
- Starts a
DispatcherTimer
that will fire each second and check the current position of the MediaElement
(in the private variable) and update the CurrentPostionProperty
.
public ICommand MediaOpenedCommand { get; set; }
public void MediaOpened(object param)
{
MediaElement parmMediaElement = (MediaElement)param;
MyMediaElement = parmMediaElement;
this.progressTimer = new DispatcherTimer();
this.progressTimer.Interval = TimeSpan.FromSeconds(1);
this.progressTimer.Tick += new EventHandler(this.ProgressTimer_Tick);
SetCurrentPosition();
}
private bool CanMediaOpened(object param)
{
return true;
}
The code for the Start and Stop Commands is:
#region PlayVideoCommand
public ICommand PlayVideoCommand { get; set; }
public void PlayVideo(object param)
{
MyMediaElement.Play();
progressTimer.Start();
}
private bool CanPlayVideo(object param)
{
bool CanPlay = false;
if (MyMediaElement != null)
{
if (MyMediaElement.CurrentState != MediaElementState.Playing)
{
CanPlay = true;
}
}
return CanPlay;
}
#endregion
#region StopVideoCommand
public ICommand StopVideoCommand { get; set; }
public void StopVideo(object param)
{
MyMediaElement.Stop();
progressTimer.Stop();
}
private bool CanStopVideo(object param)
{
bool CanStop = false;
if (MyMediaElement != null)
{
if (MyMediaElement.CurrentState == MediaElementState.Playing)
{
CanStop = true;
}
}
return CanStop;
}
#endregion
The SetCurrentPosition()
method sets CurrentProgressProperty
, CurrentPositionProperty
, and TotalDurationProperty
:
private void SetCurrentPosition()
{
CurrentProgressProperty = string.Format(
"{0}:{1} / {2}:{3}",
Math.Floor(MyMediaElement.Position.TotalMinutes).ToString("00"),
MyMediaElement.Position.Seconds.ToString("00"),
Math.Floor(MyMediaElement.NaturalDuration.TimeSpan.TotalMinutes).ToString("00"),
MyMediaElement.NaturalDuration.TimeSpan.Seconds.ToString("00"));
CurrentPositionProperty = MyMediaElement.Position.TotalSeconds;
TotalDurationProperty = MyMediaElement.NaturalDuration.TimeSpan.TotalSeconds;
}
Wrap Your Head Around View Model Style
It takes a bit of practice to wrap your head around View Model Style. However, it is not hard, and you will find you will use less code than you would normally use. The main thing you want to get used to is binding everything. If you find yourself needing to raise an event from the UI and you are stuck, you are usually not binding enough things. Let a change in a value in the ViewModel raise the event for you through bindings.
How Can You Learn More About Expression Blend?
To learn how to use Expression Blend, all you have to do is go to: http://www.microsoft.com/design/toolbox/. This site will give you the free training you need to master Expression Blend. It will also cover the design principles to make you a better designer.