In the previous article, I’ve described a light exposure timer application using MVVM. I’ve described the interface of the MVVM for this application and showed how the view binds to MVVM. This article will take a look at how MVVM is implemented.
The MVVM interface is listed below:
public interface ITreatmentViewModel : INotifyPropertyChanged
{
void Play();
void Stop();
void Load(Stream s);
void Save(Stream s);
void AddTimeDelay(int timeDelta);
ObservableCollection<TreatmentStep> Steps { get; }
int CurrentPos { get; }
TreatmentStep CurrentStep { get; }
int DurationLeft { get; }
bool IsPlaying { get; }
IList<TreatmentStep> TreatmentPlan { get; set; }
int TotalDuration { get; }
}
I’ve created DefaultTreatmentViewModel to implement this interface:
public class DefaultTreatmentViewModel : ITreatmentViewModel
{
ObservableCollection<TreatmentStep> _steps = new ObservableCollection<TreatmentStep>();
int _currPos = 0;
DispatcherTimer _timer = new DispatcherTimer();
int _timeLeft = 0;
public DefaultTreatmentViewModel()
{
_timer.Tick += new EventHandler(_timer_Tick);
_timer.Interval = TimeSpan.FromSeconds(1);
AddDefaultSteps();
}
…
ObservableCollection is commonly used in WPF for bindable collection; this replaces the BindingList in WinForms. I use the DispatchTimer so that property change firing will be on the proper thread.
_timer_Tick timer callback function will be responsible for updating various properties related to Playing – CurrentPos, CurrentStep, DurationLeft, and IsPlaying; it is the “brain” of this MVVM class:
private void _timer_Tick(object sender, EventArgs e)
{
// Reduce time left by 1. Done if still time left
DurationLeft = _timeLeft – 1;
if (_timeLeft > 0)
{
return;
}
// Try to move to next step. If no more next step, done
int nextStep = _currPos + 1;
if (nextStep >= _steps.Count)
{
IsPlaying = false;
return;
}
// Otherwise, move to next step
CurrentPos = nextStep;
DurationLeft = _steps[nextStep].Duration;
}
Property change notification will be automatically generated when property setter is called:
public int DurationLeft
{
get { return _timeLeft; }
private set
{
_timeLeft = value;
PropertyChanged(this, new PropertyChangedEventArgs("DurationLeft"));
}
}
public bool IsPlaying
{
get { return _timer.IsEnabled; }
private set
{
_timer.IsEnabled = value;
PropertyChanged(this, new PropertyChangedEventArgs("IsPlaying"));
}
}
public int CurrentPos
{
get { return _currPos; }
private set
{
_currPos = value;
PropertyChanged(this, new PropertyChangedEventArgs("CurrentPos"));
PropertyChanged(this, new PropertyChangedEventArgs("CurrentStep"));
}
}
Another slightly tricky aspect of this MVVM is the AddTimeDelay function. Since the TreatmentStep does not implement INotifyPropertyChanged interface, modifying the object will not trigger update in DataGrid. So, I use the simple trick of removing it and then inserting it into the same position:
public void AddTimeDelay(int timeDelta)
{
_steps.Where(x => x.BodySide != TreatmentStep.Side.None).ForAll(x => x.Duration += timeDelta);
Enumerable.Range(0, _steps.Count).ForAll(x => UpdateStep(x));
}
private void UpdateStep(int n)
{
TreatmentStep step = _steps[n];
_steps.RemoveAt(n);
_steps.Insert(n, step);
}
Play/Stop just set the IsPlaying property, which enables/disables timer from firing:
public void Play()
{
if (_steps.Count == 0)
{
return;
}
_timeLeft = 0;
_currPos = -1;
IsPlaying = true;
}
public void Stop()
{
IsPlaying = false;
}
Finally, we use XM L serialization for Saving/Loading:
public void Load(System.IO.Stream s)
{
List<TreatmentStep> steps;
XmlSerializer xml = new XmlSerializer(typeof(List<TreatmentStep>));
steps = xml.Deserialize(s) as List<TreatmentStep>;
TreatmentPlan = steps;
}
public void Save(System.IO.Stream s)
{
List<TreatmentStep> steps = new List<TreatmentStep>(TreatmentPlan);
XmlSerializer xml = new XmlSerializer(steps.GetType());
xml.Serialize(s, steps);
}
This completes the implementation of the MVVM class.
Going back to the view class and look at sound generation. Instead of embedding the code into the view class, I created an Announcer class which “binds” to the MVVM:
Announcer _sndPlayer;
public TreatmentView()
{
_vmModel = new DefaultTreatmentViewModel();
DataContext = _vmModel;
_vmModel.PropertyChanged += new System.ComponentModel.PropertyChangedEventHandler(_vmModel_PropertyChanged);
_sndPlayer = new Announcer(_vmModel);
InitializeComponent();
Text to Speech (TTS) synethsis is quite easy in .NET 4.0 using the System.Speech.Synthesis library. For example, to say, “Front in 3”, just do this:
SpeechSynthesizer _speech = new SpeechSynthesizer();
private void PrepareToSwitch(TreatmentStep.Side side, int timeLeft)
{
Debug.WriteLine("PrepareToSwitch " + side + "," + timeLeft);
string txt = side.ToString() + " in " + timeLeft;
_speech.SpeakAsync(txt);
}
The “brain” of Announcer class is in the property change notifier, which decides when to say what:
private void PropChange(object sender, PropertyChangedEventArgs args)
{
string propName = args.PropertyName;
if (propName == "DurationLeft")
{
int duration = _vmModel.DurationLeft;
if (duration == 3)
{
// Get next step if any
int nextStep = _vmModel.CurrentPos + 1;
if (nextStep >= _vmModel.Steps.Count)
{
return;
}
TreatmentStep step = _vmModel.Steps[nextStep];
PrepareToSwitch(step.BodySide, duration);
}
else if (duration < 3 && duration > 0)
{
CountDown(duration);
}
}
else if (propName == "CurrentPos")
{
if (_vmModel.CurrentStep.BodySide != TreatmentStep.Side.None)
{
Switch(_vmModel.CurrentStep.BodySide);
}
}
else if (propName == "IsPlaying")
{
if (!_vmModel.IsPlaying)
{
Finish();
}
}
}
The Announcer class validates the beauty of MVVM model. When MVVM was created, there was no consideration of sound synthesis. Sound synethesis is added subsequently without intrusion into either MVVM or the display view.
This concludes my short venture into MVVM.