I found a solution which works, I'll keep it an answer until a better solution will come up.
The solution bases are upon custom Behavior Generic Class.
It is important to note that the question and the answer are in global scope, rather than MediaElement
scope. Accordingly, this solution is completely relevant for every FrameworkElement
derived control which supports ViewModel-First approach.
I didn't remove any code for clarity on purpose.
ViewTemplates.xaml
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApplication2"
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity">
<DataTemplate DataType="{x:Type local:MediaElementViewModel}">
<MediaElement Source="{Binding Source}" Volume="{Binding Volume}"
LoadedBehavior="Manual" UnloadedBehavior="Manual">
<i:Interaction.Behaviors>
<local:MediaElementBehavior/>
</i:Interaction.Behaviors>
</MediaElement>
</DataTemplate>
</ResourceDictionary>
MediaElementViewModel.cs
public MediaElementViewModel()
{
Volume = 0.5;
}
private Uri _source;
public Uri Source
{
get { return _source; }
set
{
_source = value;
RaisePropertyChanged("Source");
}
}
private double _volume;
public double Volume
{
get { return _volume; }
set
{
_volume = value;
RaisePropertyChanged("Volume");
}
}
public Action Play { get; set; }
public Action Stop { get; set; }
public Func<bool> Focus { get; set; }
MediaElementBehavior.cs
public MediaElementBehavior()
{
}
protected override void OnAttached()
{
base.OnAttached();
MediaElement player = (MediaElement)this.AssociatedObject;
MediaElementViewModel viewModel = (MediaElementViewModel)this.AssociatedObject.DataContext;
player.Dispatcher.Invoke(() =>
{
// backing up the player methods inside its view-model.
if (viewModel.Play == null)
viewModel.Play = player.Play;
if (viewModel.Stop == null)
viewModel.Stop = player.Stop;
if (viewModel.Focus == null)
viewModel.Focus = player.Focus;
});
}
protected override void OnDetaching()
{
base.OnDetaching();
}
The following is an example of the above solution usage:
App.xaml
<Application x:Class="WpfApplication2.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
StartupUri="MainWindow.xaml">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="ViewTemplates.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>
MainWindow.xaml
<Window x:Class="WpfApplication2.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApplication2"
Title="MainWindow" Height="350" Width="525">
<Window.DataContext>
<local:MainViewModel/>
</Window.DataContext>
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<ContentControl Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="3" Content="{Binding CurrentMediaElement}"/>
<Button Grid.Row="1" Grid.Column="0" Content="Set Source" Command="{Binding SetSourceCommand}"/>
<WrapPanel Grid.Row="1" Grid.Column="2">
<Button Grid.Row="1" Grid.Column="2" Content="Stop" Command="{Binding StopCommand}"/>
<Button Content="Focus" Command="{Binding FocusCommand}"/>
</WrapPanel>
<Button Grid.Row="1" Grid.Column="1" Content="Play" Command="{Binding PlayCommand}"/>
<TextBlock Grid.Row="2" Grid.Column="0" Text="{Binding CurrentMediaElement.Source}" TextWrapping="Wrap"/>
<Label Grid.Row="2" Grid.Column="1" Content="{Binding ElementName=SliderVolume, Path=Value}"/>
<Slider x:Name="SliderVolume" Value="{Binding CurrentMediaElement.Volume}" Grid.Row="2" Grid.Column="2" Minimum="0" Maximum="1" Orientation="Horizontal"/>
</Grid>
</Window>
MainViewModel.cs
public MainViewModel()
{
CurrentMediaElement = new MediaElementViewModel();
}
private MediaElementViewModel _currentMediaElement;
public MediaElementViewModel CurrentMediaElement
{
get { return _currentMediaElement; }
set
{
_currentMediaElement = value;
RaisePropertyChanged("CurrentMediaElement");
}
}
private RelayCommand _setSourceCommand;
public ICommand SetSourceCommand
{
get
{
return _setSourceCommand ??
(_setSourceCommand = new RelayCommand(SetSourceExecute));
}
}
private RelayCommand _playCommand;
public ICommand PlayCommand
{
get
{
return _playCommand ??
(_playCommand = new RelayCommand(PlayExecute));
}
}
private RelayCommand _stopCommand;
public ICommand StopCommand
{
get
{
return _stopCommand ??
(_stopCommand = new RelayCommand(StopExecute));
}
}
private RelayCommand _focusCommand;
public ICommand FocusCommand
{
get
{
return _focusCommand ??
(_focusCommand = new RelayCommand(FocusExecute));
}
}
/// <summary>
/// Invoked whenever focusing media element;
/// </summary>
private void FocusExecute()
{
bool isFocused = this.CurrentMediaElement.Focus();
}
/// <summary>
/// Invoked whenever setting a media source.
/// </summary>
private void SetSourceExecute()
{
// Assume the media file location is Debug/bin/Resources/
this.CurrentMediaElement.Source = new Uri(AppDomain.CurrentDomain.BaseDirectory + "Resources\\media.mp3");
}
/// <summary>
/// Invoked whenever playing media.
/// </summary>
private void PlayExecute()
{
this.CurrentMediaElement.Play();
}
/// <summary>
/// Invoked whenerver stopping media.
/// </summary>
private void StopExecute()
{
this.CurrentMediaElement.Stop();
}
As you can see (at MainViewModel.cs), I invoked "pure" View methods from ViewModel using ViewModel-First approach. (PlayExecute
, StopExecute
and FocusExecute
).