Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

MVVM pattern violation: MediaElement.Play()

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?

like image 501
italianogrosso Avatar asked May 17 '12 07:05

italianogrosso


2 Answers

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.

like image 123
ken2k Avatar answered Oct 05 '22 11:10

ken2k


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.

like image 13
SunnyHoHoHo Avatar answered Oct 05 '22 10:10

SunnyHoHoHo