65.9K
CodeProject 正在变化。 阅读更多。
Home

Silverlight 4 视频播放器

starIconstarIconstarIconstarIconstarIcon

5.00/5 (25投票s)

2010年4月12日

Ms-PL

10分钟阅读

viewsIcon

157341

downloadIcon

12661

一个基于 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 风格

image

关于 ViewModel 模式已经有很多文章,并且有各种不同的解释。在这个例子中,我们将尽可能少地编写代码来实现这个模式。我们将尽量以最简单的方式呈现它。ViewModel 模式的 Model 和 View 非常简单。Model 包含 Web 服务,View 是 UI(在 Expression Blend 中创建,无需编码)。唯一复杂的部分是 ViewModel。在 ViewModel 中,所有功能都将使用以下方式实现:

  1. 属性 (Properties) - 某一个值。这可能是一个 String 或一个 Object。它实现了 INotifyPropertyChanged,因此任何绑定到它的元素在它发生变化时都会自动收到通知。
  2. 集合 (Collections) - 某一个集合。这是 ObservableCollection 类型,因此任何绑定到它的元素在它发生变化时都会自动收到通知。
  3. 命令 (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 中:

  1. 点击 Asset 按钮
  2. 找到 MediaElement
  3. 将其拖到设计表面

确保通过 **取消选中** MediaElementProperties 中的框,将 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。为此,我们必须:

  1. MediaElement 绑定到 SelectedVideoProperty (URI)。这将导致 MediaElement 始终播放 ViewModel 设置到该属性的视频。
  2. 使用 InvokeCommand 行为来引发 MediaOpenedCommand (ICommand)。此行为配置为在 MediaElement 上引发 MediOpened 事件时触发(当 MediaElement 准备好播放视频时自动发生)。该行为还将 MediaElement 的引用作为参数传递给 ViewModel(ViewModel 将在调用 Start 和 Stop 等其他命令时使用此 MediaElement 引用)。

让我们一步步来看

Objects and Timeline 窗口中,选择 MediaElement

MediaElement 的属性中,选择 SourceAdvanced options

选择 Data Binding...

将其设置为 SelectedVideoProperty,然后点击 OK

您会知道一个元素已绑定,因为它周围会有一个金色的框。

在 **Assets** 中,点击并拖动一个 InvokeCommand 行为...

...并将其放在 Objects and Timeline 窗口中的 MediaElement 下方。

在行为的属性中,将 EventName 设置为 MediaOpened,然后点击 Command 旁边的 Data bind 按钮。

选择 MediaOpenedCommand,然后点击 OK

选择 CommandParameterAdvanced options

选择 Data Binding...

点击 Element Property 选项卡,选择 MediaElement,然后点击 OK

这部分是最难的。其余的只是绑定剩余的属性、集合和命令。

绑定属性和集合

这些是需要绑定的剩余属性和集合。

例如,ProgressBar 控件的 ValueMaximum 属性分别设置为 CurrentPositionPropertyTotalDurationProperty

ListBox 控件自然地绑定到 SilverlightVideoList,但 ListBoxSelectedIndex 也绑定到 SelectedVideoInListProperty。这样做的原因是,在列表加载**之后**才能设置 Selectedindex

ViewModel 填充 SilverlightVideoList,并且只有在填充该集合后,它才会检查是否有任何项,如果有,则设置 Selectedindex 值。

绑定命令

这些是需要绑定的剩余命令。要绑定它们,只需将 InvokeCommand 行为拖到控件上,并将属性设置为相应的命令。

例如,ListBox 使用 InvokeCommand 行为在其 SelectionChanged 事件被引发时调用 SetVideoCommand

它的 CommandParameter 绑定到 ListBoxSelectedItem

差不多就是这样。如果您是设计者,可以跳过下一节。

代码

代码的网站部分只是一个网站,其中包含一个 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

image

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() 方法设置 CurrentProgressPropertyCurrentPositionPropertyTotalDurationProperty

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 所需的免费培训。它还将涵盖设计原则,让您成为一名更好的设计师。

© . All rights reserved.