My First WPF App: A Music Player Using MVVM Pattern
This post is part of the 2020 C# Advent Calendar. Please check out all the great posts. In this post, I examine a WPF application to play mp3 music. This is the first application I've written in WPF. I'm traditionally a web technologies developer using MVC and frontend Javascript frameworks but I wanted to explore WPF and how I can utilize the MVVM pattern to make code more testable. At the end of this post, you will have an understanding of the MVVM pattern and how to apply it to a WPF application with unit tests to test the state of the application after each user action. The application was written using .NET core 3.1 and Visual Studio 2019. The full source code can be found in GitHub.
The MVVM Pattern
MVVM stands for Model, View, View Model. It is a structural design pattern that allows for the separation of code into 3 groups. The view is the UI. The model is the business logic. The view model sits between the view and the model and is responsible for translating data back and forth between them. The key component to this pattern is the use of data binding. The data binder is responsible for synchronizing data between the view and view model. Two way data binding will update data both ways. If the data changes in the view, the view model is updated to reflect the changes and if data is changed in the view model, the view is updated. Typically, there is also a collection of services, which contain logic to help the model carry out it's functions like data persistence.
Two big reasons to use the MVVM pattern are separation of concerns and unit testing. By keeping the view logic in a view model, the UI that makes up the view can be largely declaritive. It is also easier to unit test the state of the view model when it's decoupled from the code behind of the view. I'll show some unit tests for this application later in the post.
The Application
The application is a WPF application to play mp3s. It allows the user to specify a folder containing the mp3 files and loads them into a song list to be played. Simple playback controls are provided as well as album art and lyrics, if available in the meta data. To keep the application simple there is only one view and one view model. There is no support for a media library. The song list is only driven by the files in the provided folder. The application uses TagLib Sharp to read meta tags from the mp3 files.
The Model
The application contains two model classes. The first class holds the properties of a Song. TheSong class is below.
public class Song
{
public string Artist { get; set; }
public string Album { get; set; }
public string Title { get; set; }
public string FilePath { get; set; }
public int TrackNumber { get; set; }
public TimeSpan Duration { get; set; }
public string DisplayDuration
{
get
{
return Duration.ToString("mm\\:ss");
}
}
public string Lyrics { get; set; }
public BitmapImage AlbumArt { get; set; }
public string Year { get; set; }
}
You probably noticed the
DisplayDuration property above. The MVVM pattern allows for the separation of business logic from view logic and this property is clearly for the view. My thoughts here are based around how I would structure this in an MVC application. I would have a model with only the TimeSpan Duration property and a view model with only the string DisplayDuration property. The controller in MVC would only return the view model. In this case, the view model is like the controller and is responsible for much more than one song and contains the list of songs to play. Therefore, I put DisplayDuration in the model that represents one song.
The model also contains a
SongCollection class that holds a collection of songs that make up the list of songs to be played. The only reason to have this class is to load the songs using the FileQueueLoader service I describe below. This keeps all dependencies to the service in the model. The FileQueueLoader could also be called from the view model. This would create a dependency between the services and view model but then there is no need for this additional class which is listed here.
public class SongCollection
{
private readonly IQueueLoader _loader;
public ObservableCollection<Song> SongList { get; }
public SongCollection(IQueueLoader ql)
{
SongList = new ObservableCollection<Song>();
_loader = ql;
}
public void Load(string filepath)
{
var songs = _loader.Load(filepath);
foreach (Song s in songs)
{
SongList.Add(s);
}
}
public double TotalSeconds()
{
return SongList.Sum(s => s.Duration.TotalSeconds);
}
}
Services
The application contains one service calledFileQueueLoader that implements the IQueueLoader interface. This service loads the songs into the song list from the provided folder path. The interface allows the queue loader implementation to be stubbed for unit testing. I did not include the implementation of these here as they are not directly part of the model, view, or view model but the source code can be found in the GitHub repository.
The View
The view is (mostly) made from XAML. It uses standard controls like buttons, text boxes, etc. I did not include all of the XAML code here but included an example of the label that displays the currently playing track number and song title. It is possible to fit all the logic of this application into the code behind of the view. However, that makes it harder to test and maintain. This is where the view model comes in. TheDataContext of the view is set to an instance of the view model. This allows public properties of the view model to be binded to view elements and the view is aware of what property it is referring to. The constructor of the view is below.
private MainWindowViewModel _vm;
public MainWindow()
{
InitializeComponent();
Player.LoadedBehavior = MediaState.Manual;
IQueueLoader ql = new FileQueueLoader();
SongCollection collection = new SongCollection(ql);
_vm = new MainWindowViewModel(this, collection);
DataContext = _vm;
}
Data binding is used to bind properties from the view model to elements in the view. Below is an example of the view element that displays the track and title of the currently playing song. I found this similar to binding properties to attributes or elements in HTML using a javascript framework like knockout or angularjs but a little more structured. The
Content property is binded to the TrackTitleInfo property of the view model. I'll explain how the view is notified of the property change when I look at the view model in the next section.
<Label
x:Name="NowPlayingLabel"
Content="{Binding TrackTitleInfo}"
Grid.Column="2"
HorizontalAlignment="Left"
Margin="349,61,0,0"
VerticalAlignment="Top"
FontSize="14"
Width="358" Height="28"/>
Actions the user can execute through the UI, like button clicks, also use data binding on a view element's
Command property. The Add to Queue button is below with the AddToQueueCommand command binded to the Command property of the button element. I will show how the command is executed when invoked via a button click in the next section.
<Button
x:Name="AddToQueueBtn"
Command="{Binding AddToQueueCommand}"
Content="Add to Queue"
HorizontalAlignment="Left"
Margin="518,12,0,0"
VerticalAlignment="Top"
Grid.Column="2"
Width="96"/>
One challenge I had was directly controlling the
MediaElement, the control that handles playback, from the view model. To make this possible without passing the MediaElement instance directly to the view model, I created an interface that exposes methods for playback called IMusicPlayer. The code behind of the view implements this interface and calls the appropriate method against the actual MediaElement instance. The instance of IMusicPlayer is passed to the view model instead of the MediaElement directly. This indirection keeps any UI specific controls out of the view model and allows for IMusicPlayer to be stubbed or mocked for unit testing. The interface and implementation are below. The key is to keep the implementation as small as possible and put additional logic for handling playback in the view model. Since the implementation is only 1 or 2 lines, I feel comfortable not providing unit tests for these and the view model contains the logic that needs to be tested.
IMusicPlayer.cs
public interface IMusicPlayer
{
void Play(Uri filePath);
void Play();
void Pause();
void Stop();
void FastForward(double milliseconds);
void Rewind(double milliseconds);
bool IsDone();
}
MainWindow.xaml.cs
public void Play(Uri filePath)
{
Player.Source = filePath;
Player.Play();
}
public void Play()
{
Player.Play();
}
public void Pause()
{
Player.Pause();
}
public void Stop()
{
Player.Stop();
}
public void FastForward(double milliseconds)
{
Player.Position += TimeSpan.FromMilliseconds(milliseconds);
}
public void Rewind(double milliseconds)
{
Player.Position -= TimeSpan.FromMilliseconds(milliseconds);
}
public bool IsDone()
{
return Player.Position >= Player.NaturalDuration;
}
Another challenge I had was wiring up events in the view that belong to items dynamically generated like each song in the list. This is another place where code behind can help. In this case, I want a double click handler on each item in the song list, so when a song is double clicked, it starts playing. I added the event handler to the code behind of the view and it calls the view model to execute the logic.
public void ListViewItem_MouseDoubleClick(object sender, MouseEventArgs e)
{
_vm.PlaySong.Execute(null);
}
View Model
The final piece is the view model. The view model contains everything needed to drive the view. This includes properties displayed in the view and the commands that are executed by the user through the UI. This application only has one view so the view model is namedMainWindowViewModel. If there were multiple views, each would have a corresponding view model and the name should match the view that uses it. The view model can be broken down into 4 sections private fields, properties, commands, and command actions.
There are a few private fields the view model uses to manage it's state. The biggest are timers to manage the playing progress bar and advancing to the next song in the list when the current one finishes. I'm not going to go into detail on the timers but they are local to the view model. It also contains an instance of
IMusicPlayer to control music playback and a SongCollection which is the song list to be played.
The view model also contains a number of properties that are bound to the view to display current playing progress and details of the song being played. The view model implements the
INotifyPropertyChanged interface so the view is notified to update when a property changes. The SongList property is an ObservableCollection which already implements this interface as well as, INotifyCollectionChanged, the interface that notifies listeners when the collection changes. When a property value changes it needs to invoke the PropertyChanged event for the property that was changed. For example, when a new song starts playing, the TrackTitleInfo property is updated which calls PropertyChanged and the view gets updated to reflect the changes. The PropertyChanged event can be implemented with one line, public event PropertyChangedEventHandler PropertyChanged = delegate { };. Below is the code for the TrackTitleInfo property. The other properties follow the same pattern.
private string _trackTitleInfo;
public string TrackTitleInfo
{
get { return _trackTitleInfo; }
set
{
_trackTitleInfo = value;
PropertyChanged(this, new PropertyChangedEventArgs("TrackTitleInfo"));
}
}
The view model contains the commands for each action the user can execute from the UI. Command execution uses another pattern called the command pattern. Wikipedia has an overview of the command pattern. Basically, there is a command handler that implements the
ICommand interface. This takes an Action to be executed and determines if the command is enabled or not. A generic implementation that works for this application is the CommandHandler class.
Below is a list of the constructors for all commands in the view model.
AddToQueueCommand = new CommandHandler(() => AddToQueueAction(), () => true);
ClearQueueCommand = new CommandHandler(() => ClearQueueAction(), () => true);
PlaySong = new CommandHandler(() => PlaySongAction(), () => true);
PauseSong = new CommandHandler(() => PauseSongAction(), () => true);
StopSong = new CommandHandler(() => StopSongAction(), () => true);
FastForwardCommand = new CommandHandler(() => FastForwardAction(), () => true);
RewindCommand = new CommandHandler(() => RewindAction(), () => true);
In the view section above, I pointed out the command to add songs to the song list. This is implemented using the
AddToQueueCommand command and AddToQueueAction action. The user clicks the button, it executes the command bounded to it, in this case AddToQueueCommand, and the command executes the action assigned to this command, in this case AddToQueueAction. The action contains the logic that needs to happen when the command is executed and this is a regular C# method in the view model. The implementation of the AddToQueueAction is below.
private void AddToQueueAction()
{
_currentQueue.Load(QueueFilePath);
if (_currentQueue.SongList.Count > 0)
{
double queueDuration = _currentQueue.TotalSeconds();
string format = "hh\\:mm\\:ss";
if (queueDuration < 3600)
{
format = "mm\\:ss";
}
TimeSpan totalDuration = TimeSpan.FromSeconds(queueDuration);
QueueInfo = _currentQueue.SongList.Count + " songs - " + totalDuration.ToString(format);
}
}
Unit Tests
Each command of the view model can be unit tested. These can be found in theMusicPlayer.Tests project. It contains stub implementations of the IMusicPlayer and IQueueLoader interfaces and a unit test for each command defined in the view model. The test executes the command and then verifies the state of the view model. I won't show every test here but highlight the PlayCommand test as an example.
[Test]
public void PlayCommand_Test()
{
IMusicPlayer mp = new MusicPlayerStub();
IQueueLoader ql = new QueueLoaderStub();
SongCollection collection = new SongCollection(ql);
MainWindowViewModel vm = new MainWindowViewModel(mp, collection);
vm.AddToQueueCommand.Execute(null);
vm.SelectedIndex = 0;
vm.SelectedSong = vm.SongList[vm.SelectedIndex];
vm.PlayingSong = vm.SongList[1];
vm.PlaySong.Execute(null);
// Assert playing song properties match selected song properties
Assert.AreEqual("Test", vm.PlayingSong.Artist);
Assert.AreEqual("Title", vm.PlayingSong.Title);
Assert.AreEqual("Test Album", vm.PlayingSong.Album);
// Assert viewmodel state
Assert.AreEqual("Test - Test Album [2018]", vm.ArtistAlbumInfo);
Assert.AreEqual("1. Title", vm.TrackTitleInfo);
}
