This article describes how to play foreground and background sounds in a XAML-based Universal Windows app. I know that this sounds easy (pun intended). But I will do this while sticking to the MVVM pattern, which makes it a bit more challenging. I’m not targeting a specific framework here. The described architecture can be applied in MVVM Light, Universal Prism, Caliburn.Micro as well as in your home brewed MVVM library.
Here’s how the attached sample app looks like. It comes with two buttons that trigger a different sound effect, a switch to mute the background sound, and a button to navigate to a new page (very uncommon in a single-page app ):
What's the challenge for an MVVM solution to play sounds? Well, a universal XAML app can only make noise through a MediaElement, and that MediaElement should be part of the runtime structure of XAML objects known as the visual tree. As a result of that, only a small number of app components -the currently active Views- have access to it. In most cases however it’s the business logic in the Model or the ViewModel that knows which specific sound effect should be played at which particular moment. So the ‘Sound’ functionality should not be restricted to a some Views, but it should be generally available to all application components and component types.
In most MVVM ecosystems, this type of global functionality ends up in a so-called Service (other examples include logging, authorization, and toast notification). And so does the SoundPlayer: it’s a global service that comes with a Play method. That method has two parameters:
- a reference to the sound effect (from a developer-friendly enumeration), and
- a boolean indicating whether the sound should be played in the foreground (once) or in the background (in a loop).
Since a MediaElement can only play one sound at a time, the SoundPlayer is connected to two of them – one for the foreground and one for background. The background player can be disabled (muted) by the app.
Here’s an class diagram of the SoundPlayer, together with the Visual Studio solution structure. The platform specific projects for Windows 8.1 and Windows Phone 8.1 are collapsed, since they’re empty – except for the tile images. All the code is in the Shared project:
This is how everything was set up. The first challenge is to make sure that every Page of the app is decorated with two MediaElement instances. An easy way to do this, is to put these elements in the app’s root frame. This can be done through a custom style, e.g. in the App.xaml file:
<Application.Resources>
<!-- Injecting media players on each page -->
<Style x:Key="RootFrameStyle"
TargetType="Frame">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Frame">
<Grid>
<!-- Foreground Player -->
<MediaElement IsLooping="False" />
<!-- Background Player -->
<MediaElement IsLooping="True" />
<ContentPresenter />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Application.Resources>
Make sure that the style is applied when the app is launched, by adding an extra line of code in the standard OnLaunched method in App.xaml.cs:.
protected override void OnLaunched(LaunchActivatedEventArgs e)
{
Frame rootFrame = Window.Current.Content as Frame;
if (rootFrame == null)
{
rootFrame = new Frame();
// Injecting media players on each page.
rootFrame.Style = this.Resources["RootFrameStyle"] as Style;
// ...
}
// ...
}
As long as you navigate within the frame, the media elements remain available. But when you programmatically switch to another root Frame, you have to make sure to apply this style to it. That’s what the navigate button in the sample app does:
var rootFrame = new Frame();
rootFrame.Style = App.Current.Resources["RootFrameStyle"] as Style;
Window.Current.Content = rootFrame ;
rootFrame.Navigate(typeof(MainPage));
Achievement unlocked: every page will always two MediaElement instances on it. We just have to make them available to the SoundPlayer service. This assignment is done in the Initialize call in the SoundPlayer class itself. Make sure to preserve the state since we might be hosted in a new frame with brand new UI elements at their default -unmuted- state:
public void Initialize()
{
// Register media elements to the Sound Service.
try
{
DependencyObject rootGrid = VisualTreeHelper.GetChild(Window.Current.Content, 0);
var foregroundPlayer = (MediaElement)VisualTreeHelper.GetChild(rootGrid, 0) as MediaElement;
var backgroundPlayer = (MediaElement)VisualTreeHelper.GetChild(rootGrid, 1) as MediaElement;
SoundPlayer.ForegroundPlayer = foregroundPlayer;
// Keep the state.
var isMuted = this.IsBackgroundMuted;
SoundPlayer.BackgroundPlayer = backgroundPlayer;
this.IsBackgroundMuted = isMuted;
}
catch (Exception)
{
// Most probably you forgot to apply the custom root frame style.
SoundPlayer.ForegroundPlayer = null;
SoundPlayer.BackgroundPlayer = null;
}
}
Every Page should make a call to this Initialize after loading, so I factored out the call into a common base class for all of the app’s Page-type views.
public class ViewBase : Page
{
public ViewBase()
{
this.Loaded += this.OnLoaded;
}
protected virtual void OnLoaded(object sender, RoutedEventArgs e)
{
SoundPlayer.Instance.Initialize();
}
}
public sealed partial class MainPage : ViewBase
{
// ...
}
To make the SoundPlayer service globally accessible, it was implemented as a Singleton. A static class would not work, since property-changed notification requires an instance. In most MVVM frameworks this instance would be served to you by Dependency Injection, or it would be listening to a Messenger of some sort. Here’s the core class definition:
internal class SoundPlayer : BindableBase
{
private static SoundPlayer instance = new SoundPlayer();
private static MediaElement ForegroundPlayer { get; set; }
private static MediaElement BackgroundPlayer { get; set; }
public static SoundPlayer Instance
{
get
{
return instance;
}
}
public bool IsBackgroundMuted
{
get
{
if (BackgroundPlayer == null)
{
return false;
}
return BackgroundPlayer.IsMuted;
}
set
{
if (BackgroundPlayer != null)
{
BackgroundPlayer.IsMuted = value;
this.OnPropertyChanged("IsBackgroundMuted");
}
}
}
public async Task Play(Sounds sound, bool inBackground = false)
{
var mediaElement = inBackground ? BackgroundPlayer : ForegroundPlayer;
if (mediaElement == null)
{
return;
}
string source = string.Format("ms-appx:///Assets/{0}.mp3", sound.ToString());
mediaElement.Source = new Uri(source);
await mediaElement.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
{
mediaElement.Stop();
mediaElement.Play();
});
}
}
The Play method was made asynchronous to keep the UI responsive. The list of available sound effects is contained in an enumeration that maps the physical mp3 assets that you can see in the previous screen shot. That makes it easy for all components to select the sound effect they want to play:
public enum Sounds
{
Nature,
Sweep,
Bell
}
By the way, if you’re looking for royalty-free sound effects, you may want to check SoundGator or SoundBible.
The SoundPlayer is now accessible from the View components. So you could decide to start some background music after a page is loaded:
protected async override void OnLoaded(object sender, RoutedEventArgs e)
{
base.OnLoaded(sender, e);
await SoundPlayer.Instance.Play(Sounds.Nature, true);
}
The SoundPlayer is also accessible from the (View)Model components. Here's the code for the Bell command in the MainViewModel:
private async void Bell_Executed()
{
await SoundPlayer.Instance.Play(Sounds.Bell);
}
None of these app components are actually aware of the MediaElement instances that are doing the work. That knowledge is encapsulated in the custom Frame style and the SoundPlayer itself.
Here’s the full source code, it was written with Visual Studio 2013 Update 2. Feel free to adapt it to your favorite MVVM framework: U2UC.WinUni.Sound.zip (1.3MB)
Enjoy!
XAML Brewer