This is part 2 in my series of posts on building the latest MVVM incarnation of my ChordFactory hobby project; in Part 1 I discussed modelling musical chord and scale data in XML – the Model in the MVVM (Model-View-ViewModel) pattern. In this post I will look at the next part of the pattern – the ViewModel.
Loading the data
In the MVVM pattern, data is held in properties of the ViewModel to allow the Views to data-bind UI elements directly onto those properties. In the ChordFactory application, the mechanics of loading the data from the XML and building the Chords and Scales collections to supply the View-Model are implemented using the Repository pattern with individual repository classes, deriving from a repository base class, for the Chords and the Scales collections. These classes implement private static methods to load their respective data and then surface the collections retrieved via public methods that return ObservableCollections of Chords and Scales respectively.
- public class RepositoryBase
- {
- protected static Stream GetResourceStream(string resourceFile)
- {
- Uri uri = new Uri(resourceFile, UriKind.RelativeOrAbsolute);
- StreamResourceInfo info = Application.GetResourceStream(uri);
- if (info == null || info.Stream == null)
- throw new ArgumentException("Missing resource file: " + resourceFile);
- return info.Stream;
- }
- }
- public class ChordRepository : RepositoryBase
- {
- private readonly ObservableCollection<Chord> observableChords = new ObservableCollection<Chord>();
- public ChordRepository(string chordDataFile)
- {
- LoadChords(chordDataFile).ForEach(observableChords.Add);
- }
- private static List<Chord> LoadChords(string chordDataFile)
- {
- using (Stream stream = GetResourceStream(chordDataFile))
- using (XmlReader xmlRdr = XmlReader.Create(stream))
- return (from chordElem in XDocument.Load(xmlRdr).Element("Chords").Elements("Chord")
- select
- Chord.CreateChord((string) chordElem.Element("Description"),
- chordElem.Element("NoteList").Elements("NoteIndex").Select(
- x => int.Parse(x.Value)).ToList())).ToList();
- }
- public ObservableCollection<Chord> GetChords()
- {
- return observableChords;
- }
- }
- public class ScaleRepository : RepositoryBase
- {
- private readonly ObservableCollection<Scale> observableScales = new ObservableCollection<Scale>();
- public ScaleRepository(string scaleDataFile)
- {
- LoadScales(scaleDataFile).ForEach(observableScales.Add);
- }
- private static List<Scale> LoadScales(string scaleDataFile)
- {
- using (Stream stream = GetResourceStream(scaleDataFile))
- using (XmlReader xmlRdr = XmlReader.Create(stream))
- return (from ScaleElem in XDocument.Load(xmlRdr).Element("Scales").Elements("Scale")
- select
- Scale.CreateScale((string)ScaleElem.Element("Description"),
- ScaleElem.Element("NoteList").Elements("NoteIndex").Select(
- x => int.Parse(x.Value)).ToList())).ToList();
- }
- public ObservableCollection<Scale> GetScales()
- {
- return observableScales;
- }
- }
Data in the ViewModel
The ViewModel uses the Repositories to load the data and provides it as bindable collections together with other bindable properties such as the currently selected items in the collections and implementation of change notification so that bound UI can respond to updates in the ViewModel.
- public class ChordsViewModel : INotifyPropertyChanged
- {
- private readonly ObservableCollection<Chord> chords;
- private readonly ObservableCollection<Scale> scales;
- private List<int> selectedChord;
- private List<int> selectedScale;
- private RootNotes rootNote;
- private Inversion inversion;
- public event PropertyChangedEventHandler PropertyChanged;
- private readonly List<Inversion> inversions = new List<Inversion>
- {
- Inversion.Basic,
- Inversion.First,
- Inversion.Second,
- Inversion.Third,
- Inversion.Fouth
- };
- public ChordsViewModel()
- {
- chords = new ChordRepository("/Openfeature.ChordFactory;component/Data/chords.xml").GetChords();
- scales = new ScaleRepository("/Openfeature.ChordFactory;component/Data/scales.xml").GetScales();
- }
- public ObservableCollection<Chord> Chords
- {
- get { return chords; }
- }
- public ObservableCollection<Scale> Scales
- {
- get { return scales; }
- }
- public List<Inversion> Inversions { get { return inversions; } }
- public List<int> SelectedChord
- {
- get { return selectedChord; }
- private set
- {
- selectedChord = value;
- OnPropertyChanged("SelectedChord");
- }
- }
- public List<int> SelectedScale
- {
- get { return selectedScale; }
- private set
- {
- selectedScale = value;
- OnPropertyChanged("SelectedScale");
- }
- }
- public Inversion Inversion
- {
- get { return inversion; }
- set
- {
- inversion = value;
- OnPropertyChanged("Inversion");
- }
- }
- public RootNotes RootNote
- {
- get { return rootNote; }
- set
- {
- rootNote = value;
- OnPropertyChanged("RootNote");
- }
- }
- public void ChordSelectionChanged(object sender, SelectionChangedEventArgs e)
- {
- SelectedChord = ((Chord)e.AddedItems[0]).Notes;
- }
- public void InversionSelectionChanged(object sender, SelectionChangedEventArgs e)
- {
- Inversion = (Inversion)e.AddedItems[0];
- }
- public void RootNoteChanged(object sender, SelectionChangedEventArgs e)
- {
- RootNote = (RootNotes)e.AddedItems[0];
- }
- public void ScaleSelectionChanged(object sender, SelectionChangedEventArgs e)
- {
- SelectedScale = ((Scale)e.AddedItems[0]).Notes;
- }
- private void OnPropertyChanged(string propertyName)
- {
- if (PropertyChanged != null)
- {
- PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
- }
- }
- }
The Chords and Scales collection properties of the ViewModel are bound to UI elements, (initially ComboBoxes – UI/UX enhancement will have to come later), and their SelectionChanged events wired back to the ViewModel using the CallDataMethod behaviour from the excellent Expression Blend Samples on Codeplex:
- <StackPanel x:Name="BoundData" Orientation="Horizontal" Margin="10,10,10,20" >
- <ComboBox x:Name="ChordsList" ItemsSource="{Binding Chords}" DisplayMemberPath="Description" Margin="10,0">
- <i:Interaction.Triggers>
- <i:EventTrigger EventName="SelectionChanged">
- <si:CallDataMethod Method="ChordSelectionChanged" />
- </i:EventTrigger>
- </i:Interaction.Triggers>
- </ComboBox>
- <ComboBox x:Name="ScalesList" ItemsSource="{Binding Scales}" DisplayMemberPath="Description" Margin="10,0">
- <i:Interaction.Triggers>
- <i:EventTrigger EventName="SelectionChanged">
- <si:CallDataMethod Method="ScaleSelectionChanged" />
- </i:EventTrigger>
- </i:Interaction.Triggers>
- </ComboBox>
- <ComboBox x:Name="InversionsList" Margin="10,0" ItemsSource="{Binding Inversions}">
- <i:Interaction.Triggers>
- <i:EventTrigger EventName="SelectionChanged">
- <si:CallDataMethod Method="InversionSelectionChanged" />
- </i:EventTrigger>
- </i:Interaction.Triggers>
- </ComboBox>
- </StackPanel>
The ViewModel handles selection changes and sets its SelectedChord and SelectedScale properties appropriately. The piano keyboard which displays the notes from the selected chord and scale is written as a Silverlight Control and it too has SelectedChord and SelectedScale properties; these bind to the properties on the ViewModel with the same name. The keyboard control also responds to left-mouse clicks in order to allow the selection of the root note of the chord or scale.
So now I have my data loaded and in a bindable ViewModel, creating a user interface in XAML to represent it to the user of the Silverlight ChordFactory is next, together with some stuff about testing. That’s for part 3.