Tuesday, April 13, 2010

Building Silverlight Chord Factory: Part 2 – The ViewModel

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.

REPOSITORY BASE CLASS
  1. public class RepositoryBase
  2. {
  3.     protected static Stream GetResourceStream(string resourceFile)
  4.     {
  5.         Uri uri = new Uri(resourceFile, UriKind.RelativeOrAbsolute);
  6.  
  7.         StreamResourceInfo info = Application.GetResourceStream(uri);
  8.         if (info == null || info.Stream == null)
  9.             throw new ArgumentException("Missing resource file: " + resourceFile);
  10.  
  11.         return info.Stream;
  12.     }
  13. }

CHORD REPOSITORY
  1. public class ChordRepository : RepositoryBase
  2. {
  3.     private readonly ObservableCollection<Chord> observableChords = new ObservableCollection<Chord>();
  4.  
  5.     public ChordRepository(string chordDataFile)
  6.     {
  7.         LoadChords(chordDataFile).ForEach(observableChords.Add);
  8.     }
  9.  
  10.     private static List<Chord> LoadChords(string chordDataFile)
  11.     {
  12.         using (Stream stream = GetResourceStream(chordDataFile))
  13.         using (XmlReader xmlRdr = XmlReader.Create(stream))
  14.             return (from chordElem in XDocument.Load(xmlRdr).Element("Chords").Elements("Chord")
  15.                     select
  16.                         Chord.CreateChord((string) chordElem.Element("Description"),
  17.                                           chordElem.Element("NoteList").Elements("NoteIndex").Select(
  18.                                               x => int.Parse(x.Value)).ToList())).ToList();
  19.     }
  20.  
  21.     public ObservableCollection<Chord> GetChords()
  22.     {
  23.         return observableChords;
  24.     }
  25. }

SCALE REPOSITORY
  1. public class ScaleRepository : RepositoryBase
  2. {
  3.     private readonly ObservableCollection<Scale> observableScales = new ObservableCollection<Scale>();
  4.  
  5.     public ScaleRepository(string scaleDataFile)
  6.     {
  7.         LoadScales(scaleDataFile).ForEach(observableScales.Add);
  8.     }
  9.  
  10.     private static List<Scale> LoadScales(string scaleDataFile)
  11.     {
  12.         using (Stream stream = GetResourceStream(scaleDataFile))
  13.         using (XmlReader xmlRdr = XmlReader.Create(stream))
  14.             return (from ScaleElem in XDocument.Load(xmlRdr).Element("Scales").Elements("Scale")
  15.                     select
  16.                         Scale.CreateScale((string)ScaleElem.Element("Description"),
  17.                                           ScaleElem.Element("NoteList").Elements("NoteIndex").Select(
  18.                                               x => int.Parse(x.Value)).ToList())).ToList();
  19.     }
  20.  
  21.     public ObservableCollection<Scale> GetScales()
  22.     {
  23.         return observableScales;
  24.     }
  25. }

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.

CHORDS VIEW-MODEL
  1. public class ChordsViewModel : INotifyPropertyChanged
  2. {
  3.     private readonly ObservableCollection<Chord> chords;
  4.     private readonly ObservableCollection<Scale> scales;
  5.     private List<int> selectedChord;
  6.     private List<int> selectedScale;
  7.     private RootNotes rootNote;
  8.     private Inversion inversion;
  9.  
  10.     public event PropertyChangedEventHandler PropertyChanged;
  11.  
  12.     private readonly List<Inversion> inversions = new List<Inversion>
  13.                                                       {
  14.                                                           Inversion.Basic,
  15.                                                           Inversion.First,
  16.                                                           Inversion.Second,
  17.                                                           Inversion.Third,
  18.                                                           Inversion.Fouth
  19.                                                       };
  20.  
  21.     
  22.     public ChordsViewModel()
  23.     {
  24.         chords = new ChordRepository("/Openfeature.ChordFactory;component/Data/chords.xml").GetChords();
  25.         scales = new ScaleRepository("/Openfeature.ChordFactory;component/Data/scales.xml").GetScales();
  26.     }
  27.  
  28.     public ObservableCollection<Chord> Chords
  29.     {
  30.         get { return chords; }
  31.     }
  32.  
  33.     public ObservableCollection<Scale> Scales
  34.     {
  35.         get { return scales; }
  36.     }
  37.  
  38.     public List<Inversion> Inversions { get { return inversions; } }
  39.  
  40.     public List<int> SelectedChord
  41.     {
  42.         get { return selectedChord; }
  43.         private set
  44.         {
  45.             selectedChord = value;
  46.             OnPropertyChanged("SelectedChord");
  47.         }
  48.     }
  49.  
  50.     public List<int> SelectedScale
  51.     {
  52.         get { return selectedScale; }
  53.         private set
  54.         {
  55.             selectedScale = value;
  56.             OnPropertyChanged("SelectedScale");
  57.         }
  58.     }
  59.  
  60.     public Inversion Inversion
  61.     {
  62.         get { return inversion; }
  63.         set
  64.         {
  65.             inversion = value;
  66.             OnPropertyChanged("Inversion");
  67.         }
  68.     }
  69.  
  70.     public RootNotes RootNote
  71.     {
  72.         get { return rootNote; }
  73.         set
  74.         {
  75.             rootNote = value;
  76.             OnPropertyChanged("RootNote");
  77.         }
  78.     }
  79.  
  80.     public void ChordSelectionChanged(object sender, SelectionChangedEventArgs e)
  81.     {
  82.         SelectedChord = ((Chord)e.AddedItems[0]).Notes;
  83.     }
  84.  
  85.     public void InversionSelectionChanged(object sender, SelectionChangedEventArgs e)
  86.     {
  87.         Inversion = (Inversion)e.AddedItems[0];
  88.     }
  89.  
  90.     public void RootNoteChanged(object sender, SelectionChangedEventArgs e)
  91.     {
  92.         RootNote = (RootNotes)e.AddedItems[0];
  93.     }
  94.  
  95.     public void ScaleSelectionChanged(object sender, SelectionChangedEventArgs e)
  96.     {
  97.         SelectedScale = ((Scale)e.AddedItems[0]).Notes;
  98.     }
  99.  
  100.     private void OnPropertyChanged(string propertyName)
  101.     {
  102.         if (PropertyChanged != null)
  103.         {
  104.             PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
  105.         }
  106.     }
  107. }

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:

COLLECTION PROPERTIES BINDINGS
  1. <StackPanel x:Name="BoundData" Orientation="Horizontal" Margin="10,10,10,20" >
  2.     <ComboBox x:Name="ChordsList" ItemsSource="{Binding Chords}" DisplayMemberPath="Description" Margin="10,0">
  3.         <i:Interaction.Triggers>
  4.             <i:EventTrigger EventName="SelectionChanged">
  5.                 <si:CallDataMethod Method="ChordSelectionChanged" />
  6.             </i:EventTrigger>
  7.         </i:Interaction.Triggers>
  8.     </ComboBox>
  9.     <ComboBox x:Name="ScalesList" ItemsSource="{Binding Scales}" DisplayMemberPath="Description" Margin="10,0">
  10.         <i:Interaction.Triggers>
  11.             <i:EventTrigger EventName="SelectionChanged">
  12.                 <si:CallDataMethod Method="ScaleSelectionChanged" />
  13.             </i:EventTrigger>
  14.         </i:Interaction.Triggers>
  15.     </ComboBox>
  16.     <ComboBox x:Name="InversionsList" Margin="10,0" ItemsSource="{Binding Inversions}">
  17.         <i:Interaction.Triggers>
  18.             <i:EventTrigger EventName="SelectionChanged">
  19.                 <si:CallDataMethod Method="InversionSelectionChanged" />
  20.             </i:EventTrigger>
  21.         </i:Interaction.Triggers>
  22.     </ComboBox>
  23. </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.