Silverlight 4 视频播放器





5.00/5 (25投票s)
一个基于 ViewModel 模式的 Silverlight 4 视频播放器示例,它不仅是“可换肤”的,而且是完全“可设计”的。
注意:由于 RX 扩展的最新版本发生变化,本文已过时。
引言
本项目演示了如何实现 **ViewModel 模式** 来创建一个完全“可设计”的 Silverlight 视频播放器。这与“可换肤”的视频播放器不同。“可换肤”的视频播放器允许您更改播放器控件按钮的外观和感觉。“可设计”的播放器允许设计者使用 **任何** 控件集来实现视频播放器。
**ViewModel 模式** 允许程序员创建一个完全没有 UI 的应用程序。程序员只需要创建一个 **ViewModel** 和一个 **Model**。然后,一个完全没有编程能力的设计者就可以从一个空白页面开始,在 **Microsoft Expression Blend 4** (或更高版本) 中完全创建 **View** (UI)。
注意:要构建项目,您可能需要从 http://msdn.microsoft.com/en-us/devlabs/ee794896.aspx 安装 Silverlight RX 扩展。
ViewModel 模式的力量
这个 Silverlight 项目并非一个功能齐全的视频播放器,但它确实可以工作,并有望展示一个非简单的 ViewModel 模式 Silverlight 项目示例。为了尽可能简化示例代码,暂停按钮、全屏支持和跳到指定位置等功能被省略了。
如果您对 ViewModel 模式不熟悉,建议您阅读 Silverlight ViewModel 模式:一个(过于)简化的解释 以获得介绍。
View Model 风格
关于 ViewModel 模式已经有很多文章,并且有各种不同的解释。在这个例子中,我们将尽可能少地编写代码来实现这个模式。我们将尽量以最简单的方式呈现它。ViewModel 模式的 Model 和 View 非常简单。Model 包含 Web 服务,View 是 UI(在 Expression Blend 中创建,无需编码)。唯一复杂的部分是 ViewModel。在 ViewModel 中,所有功能都将使用以下方式实现:
- 属性 (Properties) - 某一个值。这可能是一个
String
或一个Object
。它实现了INotifyPropertyChanged
,因此任何绑定到它的元素在它发生变化时都会自动收到通知。 - 集合 (Collections) - 某一个集合。这是
ObservableCollection
类型,因此任何绑定到它的元素在它发生变化时都会自动收到通知。 - 命令 (Commands) - 可以引发的事件。另外,可以传递一个类型为
Object
的参数。它实现了ICommand
。
对于命令,我们只需要使用 InvokeCommandAction
行为。
就是这样。这就是“简化版”的 ViewModel 模式。
一个 Silverlight ViewModel 模式视频播放器
首先,让我们从设计者使用 ViewModel 模式完全重新设计视频播放器的体验开始。
当您下载附件代码时,您需要将一些 .wmv 视频放在 Video 文件夹中。
当您运行项目时,Web 项目中的 Web 服务将检测文件夹中的视频,并将它们传递给 Silverlight 应用程序。
- 视频列表将显示在组合框下拉列表中。
- 您可以从组合框中选择一个视频,然后点击播放按钮来播放视频。
- 进度条将显示当前进度。
- 视频的确切位置将显示在视频上方。
- 视频的当前时间和总时间也将显示。
- 停止按钮将停止播放视频。
当您在 Expression Blend 中打开项目并打开 MainView.xaml 文件时,点击 Data 选项卡...
您将看到一个 **Data Context** 部分。Data Context 设置为 ViewModel,并显示 UI 可以交互的所有属性、集合和命令。设计者只需要与这些元素交互即可实现他们自己的视频播放器。
上图显示了什么绑定到什么。但是,请注意,虽然按钮已绑定到 ICommand
并引发它们,但几乎任何东西都可以引发 ICommand
,它不一定是按钮。在教程 使用 ViewModel 模式进行 Blend 4 TreeView SelectedItemChanged 中,使用了一个 TreeView
控件来引发 ICommand
。
重新设计视频播放器
我们可以删除现有的 MainPage.xaml 文件...
...并创建一个新的。
如果我们点击 Objects and Timeline 窗口中的 LayoutRoot...
...然后点击 DataContext 旁边的 New 按钮...
...并选择 MainViewModel,然后点击 OK。
当我们点击 Data 选项卡时...
...我们将看到页面的 Data Context 已设置。
我再说一遍这个重要点:我们将 **不需要** 编写任何代码来在 ViewModel 模式下实现视频播放器。
创建 UI
必需的一个控件是 MediaElement
。在 Expression Blend 中:
- 点击 Asset 按钮
- 找到
MediaElement
- 将其拖到设计表面
确保通过 **取消选中** MediaElement
的 Properties 中的框,将 AutoPlay 设置为 false。
我去了 Alan Beasley 的文章:https://codeproject.org.cn/KB/expression/ArcadeButton.aspx,并“偷”了一个按钮(他做得真好;点击按钮时,它实际上会动画并表现得像一个真正的按钮)。
然后我创造了上面你看到的“杰作”。当然,任何人都可以做得更好。我只是想展示 UI 可以有根本性的不同,但仍然可以在没有任何代码更改的情况下工作。
连接 MediaElement - 学习“像 ViewModel 模式一样思考”
UI 已创建,我们只需要将 UI 元素绑定到 ViewModel,应用程序就完成了。
要理解如何将 UI 元素绑定到 ViewModel,我们需要学习一些东西来“像 ViewModel 模式一样思考”。基本上,我们
- 将 UI 元素绑定到属性或集合,当 ViewModel 将某些内容放入这些属性或集合时,绑定到的 UI 元素会自动引发事件。
- 我们可以使用
InvokeCommand
行为来响应特定事件并在 ViewModel 中引发其他事件。
在此应用程序中,我们需要将 MediaElement
连接到 ViewModel。为此,我们必须:
- 将
MediaElement
绑定到SelectedVideoProperty
(URI)。这将导致MediaElement
始终播放 ViewModel 设置到该属性的视频。 - 使用
InvokeCommand
行为来引发MediaOpenedCommand
(ICommand
)。此行为配置为在MediaElement
上引发MediOpened
事件时触发(当MediaElement
准备好播放视频时自动发生)。该行为还将MediaElement
的引用作为参数传递给 ViewModel(ViewModel 将在调用 Start 和 Stop 等其他命令时使用此MediaElement
引用)。
让我们一步步来看
在 Objects and Timeline 窗口中,选择 MediaElement
。
在 MediaElement
的属性中,选择 Source 的 Advanced options。
选择 Data Binding...
将其设置为 SelectedVideoProperty
,然后点击 OK。
您会知道一个元素已绑定,因为它周围会有一个金色的框。
在 **Assets** 中,点击并拖动一个 InvokeCommand
行为...
...并将其放在 Objects and Timeline 窗口中的 MediaElement
下方。
在行为的属性中,将 EventName 设置为 MediaOpened
,然后点击 Command 旁边的 Data bind 按钮。
选择 MediaOpenedCommand,然后点击 OK。
选择 CommandParameter 的 Advanced options。
选择 Data Binding...
点击 Element Property 选项卡,选择 MediaElement
,然后点击 OK。
这部分是最难的。其余的只是绑定剩余的属性、集合和命令。
绑定属性和集合
这些是需要绑定的剩余属性和集合。
例如,ProgressBar
控件的 Value
和 Maximum
属性分别设置为 CurrentPositionProperty
和 TotalDurationProperty
。
ListBox
控件自然地绑定到 SilverlightVideoList
,但 ListBox
的 SelectedIndex
也绑定到 SelectedVideoInListProperty
。这样做的原因是,在列表加载**之后**才能设置 Selectedindex
。
ViewModel 填充 SilverlightVideoList
,并且只有在填充该集合后,它才会检查是否有任何项,如果有,则设置 Selectedindex
值。
绑定命令
这些是需要绑定的剩余命令。要绑定它们,只需将 InvokeCommand
行为拖到控件上,并将属性设置为相应的命令。
例如,ListBox
使用 InvokeCommand
行为在其 SelectionChanged
事件被引发时调用 SetVideoCommand
。
它的 CommandParameter
绑定到 ListBox
的 SelectedItem
。
差不多就是这样。如果您是设计者,可以跳过下一节。
代码
代码的网站部分只是一个网站,其中包含一个 Web 服务,该服务返回 Videos 文件夹中任何视频的列表。
Silverlight 项目只包含几个文件。此时重要的文件是:
- DelegateCommand.cs - 一个辅助文件,允许我们轻松创建
ICommand
(有关此文件的完整解释,请参阅:Silverlight ViewModel 模式文件管理器)。 - SilverlightVideos.cs - 这是 Model。它包含一个 Web 服务方法。
- MainViewModel.cs - 这是 ViewModel。这就是所有“魔法”发生的地方。
模型
此应用程序的 Model 由 SilverlightVideos.cs 文件中的单个 Web 方法组成。“诀窍”是我们使用 RX 扩展来调用 Web 服务并返回一个 IObservable
。
public static IObservable<IEvent<GetVideosCompletedEventArgs>> GetVideos()
{
// Uses http://msdn.microsoft.com/en-us/devlabs/ee794896.aspx
// Also see: http://programmerpayback.com/2010/03/11/
// use-silverlight-reactive-extensions-rx-to-build-responsive-uis/
// Also see: http://www.silverlightshow.net/items/
// Using-Reactive-Extensions-in-Silverlight-part-2-Web-Services.aspx
// Set up web service call
WebServiceSoapClient objWebServiceSoapClient =
new WebServiceSoapClient();
// Get the base address of the website that launched the Silverlight Application
EndpointAddress MyEndpointAddress = new
EndpointAddress(GetBaseAddress());
// Set that address
objWebServiceSoapClient.Endpoint.Address = MyEndpointAddress;
// Set up a Rx Observable that can be consumed by the ViewModel
IObservable<IEvent<GetVideosCompletedEventArgs>> observable =
Observable.FromEvent<GetVideosCompletedEventArgs>(
objWebServiceSoapClient, "GetVideosCompleted");
objWebServiceSoapClient.GetVideosAsync();
return observable;
}
ViewModel
ViewModel 是创建所有属性、集合和命令的地方。
这是 ViewModel 的构造函数:
public MainViewModel()
{
// Set the command property
MediaOpenedCommand = new DelegateCommand(MediaOpened, CanMediaOpened);
PlayVideoCommand = new DelegateCommand(PlayVideo, CanPlayVideo);
StopVideoCommand = new DelegateCommand(StopVideo, CanStopVideo);
SetVideoCommand = new DelegateCommand(SetVideo, CanSetVideo);
// Call the Model to get the collection of Videos
GetListOfVideos();
}
它使用 DelegateCommand
方法(来自 DelegateCommand.cs 文件)来创建 ICommand
,从而设置命令。接下来,它调用 GetListOfVideos()
方法。
private void GetListOfVideos()
{
// Call the Model to get the collection of Videos
SilverlightVideos.GetVideos().Subscribe(p =>
{
if (p.EventArgs.Error == null)
{
// loop thru each item
foreach (string Video in p.EventArgs.Result)
{
// Add to the SilverlightVideoList collection
SilverlightVideoList.Add(Video);
}
// if we have any videos, set the selected item value to the first one
if (SilverlightVideoList.Count > 0)
{
SelectedVideoInListProperty = 0;
}
}
});
}
该方法调用 Model 中的 GetVideos()
方法,该方法填充 SilverlightVideoList
集合。如果集合中有视频,它还会设置 SelectedVideoInListProperty
属性。
SilverlightVideoList
集合是一个简单的 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");
}
}
当 SelectedVideoInListProperty
属性被设置时,绑定到它的 UI 控件(列表框或组合下拉框)应该会引发 SetVideoCommand
命令,该命令会设置 SelectedVideoProperty
。
public ICommand SetVideoCommand { get; set; }
public void SetVideo(object param)
{
// Set Video
string tmpSelectedVideo = string.Format(@"{0}/{1}",
GetBaseAddress(), (String)param);
SelectedVideoProperty = new Uri(tmpSelectedVideo,
UriKind.RelativeOrAbsolute);
// Stop Progress Timer
progressTimer.Stop();
}
private bool CanSetVideo(object param)
{
// only set video if the parameter is not null
return (param != null);
}
MediaElement
绑定到 SelectedVideoProperty
,并将自动开始加载视频。当视频打开后,它将调用 MediaOpenedCommand
命令,该命令会:
- 将
MediaElement
的一个实例作为参数传递,以便它可以存储在 ViewModel 的私有变量中(当 Start 和 Stop 命令被引发时将使用此变量)。 - 启动一个
DispatcherTimer
,该计时器每秒触发一次,检查MediaElement
的当前位置(在私有变量中),并更新CurrentPostionProperty
。
public ICommand MediaOpenedCommand { get; set; }
public void MediaOpened(object param)
{
// Play Video
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;
}
Start 和 Stop 命令的代码是:
#region PlayVideoCommand
public ICommand PlayVideoCommand { get; set; }
public void PlayVideo(object param)
{
// Play Video
MyMediaElement.Play();
progressTimer.Start();
}
private bool CanPlayVideo(object param)
{
bool CanPlay = false;
// only allow Video to Play if it is not already Playing
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)
{
// Stop Video
MyMediaElement.Stop();
progressTimer.Stop();
}
private bool CanStopVideo(object param)
{
bool CanStop = false;
// only allow Video to Stop if it is Playing
if (MyMediaElement != null)
{
if (MyMediaElement.CurrentState == MediaElementState.Playing)
{
CanStop = true;
}
}
return CanStop;
}
#endregion
SetCurrentPosition()
方法设置 CurrentProgressProperty
、CurrentPositionProperty
和 TotalDurationProperty
。
private void SetCurrentPosition()
{
// Update the time text e.g. 01:50 / 03:30
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;
}
理解 ViewModel 模式
理解 ViewModel 模式需要一些练习。但是,这并不难,您会发现您使用的代码比平常要少。您需要习惯的主要事情是 **绑定所有内容**。如果您发现需要从 UI 引发一个事件但却卡住了,那通常是因为您绑定的内容不够多。让 ViewModel 中的值发生变化通过绑定为您引发事件。
如何学习更多关于 Expression Blend 的知识?
要学习如何使用 Expression Blend,您只需要访问:http://www.microsoft.com/design/toolbox/。该网站将提供您精通 Expression Blend 所需的免费培训。它还将涵盖设计原则,让您成为一名更好的设计师。