MVVM pattern violation: MediaElement.Play()
Asked Answered
A

4

19

I understand that ViewModel shouldn't have any knowledge of View, but how can I call MediaElement.Play() method from ViewModel, other than having a reference to View (or directly to MediaElement) in ViewModel?
Other (linked) question: how can I manage View's controls visibility from ViewModel without violating MVVM pattern?

Anachronistic answered 17/5, 2012 at 7:46 Comment(1)
the linked question is not there.. :(Tonguelashing
C
26

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.

Cobalt answered 17/5, 2012 at 7:49 Comment(6)
Thank you very much! P.s.: no need for converter if I expose a Visibility property instead of a bool oneAnachronistic
Better use bool with converter.Olympiaolympiad
@Anachronistic You're welcome :) But you shouldn't expose a property of type Visibility. This enumeration is located in System.Windows namespace, which - as the namespace says - means it is purely related to the view side of your application. Really, even if it requires more code, it's better to expose a boolean which isn't related to the view at all.Cobalt
@Cobalt I've followed your advice about Visibility, but I don't fully understand the "rule" behind it: how can I do without System.Windows namespace in the ViewModel if I have to extend DependencyObject and use DependencyProperty... :-?Anachronistic
@Anachronistic Why do you need dependency properties for your ViewModels? I don't think you should inherit your view models from DependencyObject. See #292018Cobalt
Good idea to operate controls in view and seperate with controls at same time.Osteoporosis
P
14

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.

Paleface answered 21/8, 2016 at 6:20 Comment(1)
Change public IMediaService {get; private set;} to public IMediaService MediaService {get; private set;} and add https://mcmap.net/q/425111/-how-do-i-determine-if-mediaelement-is-playing to get the MediaState and you have the perfect solution!Visitor
B
4

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.

Borneol answered 4/9, 2013 at 13:1 Comment(0)
K
0

Because MediaElement is a UI component, you would probably create an instance in your View, and then:

  • bind some actions specifically in the ViewModel (e.g. media source control),
  • and some will be implemented in your View (e.g. volume control, playback position control).

I have written a series of articles about the MediaElement provided by .NET MAUI Community Toolkit. If you are interested, you can start from here.

Kb answered 29/12, 2023 at 15:3 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.