MVVM pattern violation: MediaElement.Play() MVVM pattern violation: MediaElement.Play() wpf wpf

MVVM pattern violation: MediaElement.Play()


1) Do not call Play() from the view model. Raise an event in the view model instead (for instance PlayRequested) and listen to this event in the view:

view model:

public event EventHandler PlayRequested;...if (this.PlayRequested != null){    this.PlayRequested(this, EventArgs.Empty);}

view:

ViewModel vm = new ViewModel();this.DataContext = vm;vm.PlayRequested += (sender, e) =>{    this.myMediaElement.Play();};

2) You can expose in the view model a public boolean property, and bind the Visibility property of your controls to this property. As Visibility is of type Visibility and not bool, you'll have to use a converter.

You can find a basic implementation of such a converter here.This related question might help you too.


For all the late-comers,

There are many ways to achieve the same result and it really depends on how you would like to implement yours, as long as your code is not difficult to maintain, I do believe it's ok to break the MVVM pattern under certain cases.

But having said that, I also believe there is always way to do this within the pattern, and the following is one of them just in case if anyone would like to know what other alternatives are available.

The Tasks:

  1. we don't want to have direct reference from the ViewModel to any UI elements, i.e. the the MediaElement and the View itself.
  2. we want to use Command to do the magic here

The Solution:

In short, we are going to introduce an interface between the View and the ViewModel to break the dependecy, and the View will be implementing the interface and be responsible for the direct controlling of the MediaElement while leaving the ViewModel talking only to the interface, which can be swapped with other implementation for testing purposes if needed, and here comes the long version:

  1. Introduce an interface called IMediaService as below:

    public interface IMediaService{    void Play();    void Pause();    void Stop();    void Rewind();    void FastForward();}
  2. Implement the IMediaService in the View:

    public partial class DemoView : UserControl, IMediaService{    public DemoView()    {        InitializeComponent();    }    void IMediaService.FastForward()    {        this.MediaPlayer.Position += TimeSpan.FromSeconds(10);    }    void IMediaService.Pause()    {        this.MediaPlayer.Pause();    }    void IMediaService.Play()    {        this.MediaPlayer.Play();    }    void IMediaService.Rewind()    {        this.MediaPlayer.Position -= TimeSpan.FromSeconds(10);    }    void IMediaService.Stop()    {        this.MediaPlayer.Stop();    }}
  3. we then do few things in the DemoView.XAML:

    • Give the MediaElement a name so the code behind can access it like above:
       <MediaElement Source="{Binding CurrentMedia}" x:Name="MediaPlayer"/>
    • Give the view a name so we can pass it as a parameter, and
    • import the interactivity namespace for later use (some default namespaces are omitted for simplicity reason):
       <UserControl x:Class="Test.DemoView"     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"     xmlns:ia="http://schemas.microsoft.com/expression/2010/interactivity"     x:Name="MediaService">
    • Hookup the Loaded event through Trigger to pass the view itself to the view model through a Command
       <ia:Interaction.Triggers>         <ia:EventTrigger EventName="Loaded">             <ia:InvokeCommandAction Command="{Binding LoadedCommand}" CommandParameter="{Binding ElementName=MediaService}"></ia:InvokeCommandAction>         </ia:EventTrigger>     </ia:Interaction.Triggers>
    • last but not least, we need to hookup the media controls through Commands:
       <Button Command="{Binding PlayCommand}" Content="Play"></Button>    <Button Command="{Binding PauseCommand}" Content="Pause"></Button>    <Button Command="{Binding StopCommand}" Content="Stop"></Button>    <Button Command="{Binding RewindCommand}" Content="Rewind"></Button>    <Button Command="{Binding FastForwardCommand}" Content="FastForward"></Button> 
  4. We now can catch everything in the ViewModel (I'm using prism's DelegateCommand here):

    public class AboutUsViewModel : SkinTalkViewModelBase, IConfirmNavigationRequest{    public IMediaService {get; private set;}    private DelegateCommand<IMediaService> loadedCommand;    public DelegateCommand<IMediaService> LoadedCommand    {        get        {            if (this.loadedCommand == null)            {                this.loadedCommand = new DelegateCommand<IMediaService>((mediaService) =>                {                    this.MediaService = mediaService;                });            }            return loadedCommand;        }    }    private DelegateCommand playCommand;    public DelegateCommand PlayCommand    {        get        {            if (this.playCommand == null)            {                this.playCommand = new DelegateCommand(() =>                {                    this.MediaService.Play();                });            }            return playCommand;        }    }    .    . // other commands are not listed, but you get the idea    .}

Side note: I use Prism's Auto Wiring feature to link up the View and ViewModel. So at the View's code behind file there is no DataContext assignment code, and I prefer to keep it that way, and hence I chose to use purely Commands to achieve this result.


I use media element to play sounds in UI whenever an event occurs in the application. The view model handling this, was created with a Source property of type Uri (with notify property changed, but you already know you need that to notify UI).

All you have to do whenever source changes (and this is up to you), is to set the source property to null (this is why Source property should be Uri and not string, MediaElement will naturally throw exception, NotSupportedException I think), then set it to whatever URI you want.

Probably, the most important aspect of this tip is that you have to set MediaElement's property LoadedBehaviour to Play in XAML of your view. Hopefully no code behind is needed for what you want to achieve.

The trick is extremely simple so I won't post a complete example. The view model's play function should look like this:

    private void PlaySomething(string fileUri)    {        if (string.IsNullOrWhiteSpace(fileUri))            return;        // HACK for MediaElement: to force it to play a new source, set source to null then put the real source URI.         this.Source = null;        this.Source = new Uri(fileUri);    }

Here is the Source property, nothing special about it:

    #region Source property    /// <summary>    /// Stores Source value.    /// </summary>    private Uri _Source = null;    /// <summary>    /// Gets or sets file URI to play.    /// </summary>    public Uri Source    {        get { return this._Source; }        private set        {            if (this._Source != value)            {                this._Source = value;                this.RaisePropertyChanged("Source");            }        }    }    #endregion Source property

As for Visibility, and stuff like this, you can use converters (e.g. from bool to visibility, which you can find on CodePlex for WPF, SL, WP7,8) and bind your control's property to that of the view model's (e.g. IsVisible). This way, you control parts of you view's aspect. Or you can just have Visibility property typed System.Windows.Visibility on your view model (I don't see any pattern breach here). Really, it's not that uncommon.

Good luck,

Andrei

P.S. I have to mention that .NET 4.5 is the version where I tested this, but I think it should work on other versions as well.