Source code is available at Temperature Converter CodePlex site
Prelude
The project is a simple Temperature Converter WPF Application which will allow user to convert from Celsius to Fahrenheit and vice versa. The objective however is to demonstrate techniques that are important when building large-scale front-end enterprise software. I will walk you through initial project inception, system analysis, requirements gathering and a top level conceptual logical diagram. Thereafter I’ll discuss how the conceptual logical design will guide us with the choice of Framework. Subsequently I’ll dig deep into the world of coding while all the while keeping MVVM and SOLID patterns as the guiding torch.
Along the way we’ll learn how in production code we use WPF, MEF Discovery Composition and IOC, Rx Concurrency and Data Modelling around it and its useful side effects, Custom Dependency Properties, Control Templates and Style Template and Style Triggers. All the above techniques are to facilitate MVVM (with a word of caution, as to not to make the converters too intelligent as we cannot unit test those). We’ll also write a simple Log4Net Appender to provide us with useful inmemory log messages. Finally we see practical Rx-Testing Virtual Time Scheduling MOQ Unit Testing in play.
System Analysis
I looked at various temperature converters and finally settled with the google temperature converter for its simplicity, sleek design and more usability with minimum clicks. The Google Temperature Converter looks like below.
Our Final System
Functional Requirements.
By playing around I noted the functional requirements as below.
Non Functional Requirements
Conceptual Design
I personally prefer top level conceptual design. It allows to more naturally understand the appropriate design patterns and accordingly help us model the system. Once we understand the relevant pattern and the associated design pattern we can design relevant data model, service models and relevant frameworks to be used.
We see that the right text box observes on the sequence of input on the left text box and reacts to the changes. Similarly we see that when the user inputs on the right text box, the left text box observes on the sequence of input and reacts to it.
What we see here is an Observer Pattern in play. There are two observers, the left sequence Observer and the right sequence Observer. Well from technical point of view one may argue why two observers and why not simply let the ViewModel have one observer which handles the conversion logic. Why have observer at all and instead simply implement the logic using databinding and WPF behaviours. Well the argument goes one. There are no just right answer to anything. The way I look at things is to unbias my mind from technology and look at the system at its pure natural form.
The Text Boxes are like two observers which observes the data thrown by the user. The job of the Data Model is only to hold the data and provide the observable orchestration. The actual processing of the data is done by the controller who decide who gets a point and who loses. The ViewModel walks up and informs the View about the decision taken by the controller and asks the view to display the decision. The reason for two observers is because it more naturally depicts the system we are handling and SOLID pattern insist separation of concern as its guiding 1st principal. Following these principles helps to alleviate future problems and issues with design while the product matures.
Both of these observers react to the changes to the subject data sequence. We will further like to handle the reaction action concurrently without blocking the UI Main Thread. Moreover if you decide to handle concurrency in UI, we have a restriction such that anytime you update a Visual, the work is to be sent through the Dispatcher to the UI thread. The control itself can only be touched by it’s owning thread. If you try to do anything with a control from another thread, you’ll get a runtime unsupported operation exception. Overall concurrency may be an overkill for this simple Temperature converter, but the objective here is to show techniques for building large-scale front-end enterprise system. Therefore given we have decided our requirement for concurrency, there are many patterns at our disposal to run a piece of work in the background:
new Thread(() => { }).Start()
ThreadPool.QueueUserWorkItem(_ => { }, null)
Task.Factory.StartNew(() => { })
syncCtx.Post(_ => { }, null)
Dispatcher.BeginInvoke(() => { })
Rx, has abstracted all the above mechanisms for concurrency using a single interface called IScheduler
. Given RX facilitates Observable sequence and also abstracts concurrency, it is therefore an ideal framework of choice. The observers naturally involve a sequence of data which are input by the user.
Data Model Design
As discussed above, the Data Model primarily observes a sequence of numbers thereby inheriting from IObservable<T>
where T is the sequence type. The Model does not persist the sequence of result except for the result of the latest calculated sequence value. This result is therefore represented by the property Value. The Data Model is also a Subject for observation and hence it aggregates a _innerObservable Subject<T>
of type T, in our case decimal. The logical unit of the result “Value” is represented by logical unit U.
public class ObservableSequence<T,U> : IObservable<T>
{
private readonly Subject<T> _innerObservable = new Subject<T>();
public event EventHandler ValueChanged = delegate { };
public event EventHandler UnitChanged = delegate { };
private static ILog Log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
private T _value;
private U _unit;
public T Value
{
get { return _value; }
set
{
_value = value;
_innerObservable.OnNext(value);
var localValueChanged = ValueChanged;
localValueChanged(this, new EventArgs());
}
}
public U Unit
{
get { return _unit; }
set {
_unit = value;
var localUnitChanged = UnitChanged;
localUnitChanged(this,new EventArgs());
}
}
public IDisposable Subscribe(IObserver<T> observer)
{
return _innerObservable
.DistinctUntilChanged()
.AsObservable()
.Do(o => {Log.Debug(string.Format(<code>"OnNext</code> {0}<code>"</code>,o));})
.Subscribe(observer);
}
public IObservable<T> GetObservable()
{
return this;
}
}
Note that this Data Model is a generic base implementation hence the IObservable<T>
interface is also exposed as method GetObservable()
, which will then allow the Observable to be exposed through upper stack interfaces ILeftSequenceObserverModel and IRightSequenceObserverModel. The upper stack interfaces are composed by MEF , IOC injected and orchestrated in the MVVM play. As will be shown in the unit tests, the GetObservable()
interface will allow us to inject TestScheduler
which facilitates virtual time scheduling and is absolute necessity for any RX related testing . In the MVVM paradigm, while ViewModel may not be directly using the controls but ViewModel runs and updates the UI using WPF databinding on the UI Dispatcher Thread Context. However when we will unit test the ViewModel there will be no dispatcher thread context. Therefore we will use bridge pattern to decouple abstraction from implementation. Rx has already abstracted the concurrency via the IScheduler interface as discussed above and we will leverage that as below.
public interface ISequenceObserverController
{
void RewireSequenceObservers(UnitConversionMode conversionMode,
TemperatureUnit currentTemperatureUnit, IScheduler subscribeScheduler, IScheduler observeScheduler);
void ToggleUnWireSequenceObservers();
}
ViewModel Usage
public ICommand ObserveInputSequence
{
get
{
return new RelayCommand<temperatureunit>(d =>
{
Log.Debug("ObserveInputSequence " + ( d == CurrentLeftTemperatureUnit ? UnitConversionMode.LeftToRight : UnitConversionMode.RightToLeft));
_controllerService.RewireSequenceObservers(d == CurrentLeftTemperatureUnit ? UnitConversionMode.LeftToRight :
UnitConversionMode.RightToLeft, d, Scheduler.Default, DispatcherScheduler.Current);
});
}
}
</temperatureunit>
Unit Test Usage
sequenceControllerService.RewireSequenceObservers(UnitConversionMode.RightToLeft, TemperatureUnit.Celsius, _testScheduler, _testScheduler);
...
...
[SetUp]
public void Setup()
{
_testScheduler = new TestScheduler();
_mockRightObservableModel = new Mock<irightsequenceobservermodel>();
_mockLeftObservableModel = new Mock<ileftsequenceobservermodel>();
_onNextRightObservedCount = 0;
_mockLeftObservableModel.Setup(x => x.GetObservable()).Returns(
() =>
{
return Observable.Create<decimal>(o =>
{
++_onNextRightObservedCount;
Debug.WriteLine("OnNext => RightObserved Count {0}", _onNextRightObservedCount);
return Observable.Never<decimal>().StartWith(1M).Subscribe(o);
});
});
_mockRightObservableModel.Setup(x => x.GetObservable()).Returns(
() =>
{
return Observable.Create<decimal>(o =>
{
++_onNextLeftObserverCount;
Debug.WriteLine("OnNext => LeftObserved {0}", _onNextLeftObserverCount);
return Observable.Never<decimal>().StartWith(1M).Subscribe(o);
});
});
}
</decimal></decimal></decimal></decimal></ileftsequenceobservermodel></irightsequenceobservermodel>
Now above is a test Setup, but there is a subtle “bug”. Hint the above Test will work in debug Test but will fail in Test using release code.
[Test]
public void RewireSequenceObserversRightToLeftFailsWhenCurrentUnitIsDifferent()
{
var sequenceControllerService = new SequenceObserverController(_mockRightObservableModel.Object,
_mockLeftObservableModel.Object);
_mockRightObservableModel.SetupProperty(_ => _.Unit, TemperatureUnit.Farenheit);
sequenceControllerService.RewireSequenceObservers(UnitConversionMode.RightToLeft, TemperatureUnit.Celsius, _testScheduler, _testScheduler);
_onNextRightObservedCount = 0;
_onNextLeftObserverCount = 0;
_testScheduler.AdvanceBy(TimeSpan.FromHours(1).Ticks);
Assert.GreaterOrEqual(_onNextRightObservedCount, 0);
Assert.GreaterOrEqual(_onNextLeftObserverCount, 0);
}
Note below that the data Model also exposes events to loosely notify changes to value and unit. One thing of interest is that I’m not checking if reference equals null for the event delegate. The reference check is not necessary because I’m initializing the event delgates to no-op delegate {}. There is yet one more thing of interest which is I’ve not applied double check lock pattern on the Value Setter. That is a potential bug because although OnNext
gurantees that it will not overlap; but that gurantee makes sense within the perspective of RX implementation. When we are implementing our own logic we should ensure that the RX Grammer is intact else we can have surprises in the production code. We can easily achieve that by implementing double check lock pattern over the Setters. Alternately a lock free implementation will be you assign the event delegate to a local delegate and then invoke on the local event delegate. As because delegates are immutable and thread has its own private stack , we are naturally guarded without implementing any lock. I have said many words but the implementation is quite simple like below.
public T Value
{
get { return _value; }
set
{
_value = value;
_innerObservable.OnNext(value);
var localValueChanged = ValueChanged;
localValueChanged(this, new EventArgs());
}
}
Now you can argue that _value is not guarded. But that is not a worry in this critical section of the code because we are instead using stack value which is thread safe anyway.
I’m logging the sequence as a side effect using Do extension which does not change the sequence, and also called prior to OnNext
.
public IDisposable Subscribe(IObserver<t> observer)
{
return _innerObservable
.DistinctUntilChanged()
.AsObservable()
.Do(o => {Log.Debug(string.Format("OnNext {0}",o));})
.Subscribe(observer);
}
public IObservable<t> GetObservable()
{
return this;
}
</t></t>
Observable Sequence Controller
As per the use case when the user inputs the left textbox; the left textbox becomes subject and the input sequence is observed by the right textbox and vice versa. The Observable Sequence Controller does the job of wiring and unwiring the appropriate Observable Sequence data Model. Note specifically in the Toggle Event, the controllers unwires both the observers, because any changes subsequent are due to change in the unit of conversion only rather than any new data sequence. MEF composes and injects the Models into the Sequence controller.
public interface ISequenceObserverController
{
void RewireSequenceObservers(UnitConversionMode conversionMode,
TemperatureUnit currentTemperatureUnit, IScheduler subscribeScheduler, IScheduler observeScheduler);
void ToggleUnWireSequenceObservers();
}
[Export(typeof(ISequenceObserverController))]
[PartCreationPolicy(CreationPolicy.Shared)]
public class SequenceObserverController : ISequenceObserverController
{
private static ILog Log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
private readonly IRightSequenceObserverModel _rightSequenceObserverModel;
private readonly ILeftSequenceObserverModel _leftSequenceObserverModel;
private IDisposable _disposableLeftSequenceObserver;
private IDisposable _disposableRightSequenceObserver;
[ImportingConstructor]
public SequenceObserverController(IRightSequenceObserverModel rightSequenceObserverModel,
ILeftSequenceObserverModel leftSequenceObserverModel)
{
_rightSequenceObserverModel = rightSequenceObserverModel;
_leftSequenceObserverModel = leftSequenceObserverModel;
}
public void RewireSequenceObservers(UnitConversionMode conversionMode,TemperatureUnit currentTemperatureUnit,IScheduler subscribeScheduler,IScheduler observeScheduler)
{
Log.Debug("SequenceObserverController RewireSequenceObservers");
...
...
}
}
ViewModel Design
The MainPageViewModel is the ViewModel for the view MainWindow.xaml. This ViewModel is located by the ViewModel Locator and injected as the DataContext of the view. The Models and controller are IOC injected into the ViewModel constructor. The KeyBoardFocussed
property is implemented as behaviour attached property so we can associate with a bindable command and command parameter. Note if otherwise we had used IsKeyBoardFocussed
as Trigger, we would not be able to use our viewModel in setters because only dependency properties could be use in setters.
[ImportingConstructor]
public MainPageViewModel(IMemoryLogAppenderService logAppenderService,ISequenceObserverController controllerService,
IRightSequenceObserverModel rightSequenceObserverModel, ILeftSequenceObserverModel leftSequenceObserverModel)
{
_logAppenderService = logAppenderService;
_logAppenderService.LogAppend += (o, e) => { _logMessages.Add(string.Format("{0} : {1}",e.LoggingEvent.Level.DisplayName,e.LoggingEvent.MessageObject.ToString())); };
Log.Debug("Application is starting");
_controllerService = controllerService;
_rightSequenceObserverModel = rightSequenceObserverModel;
_leftSequenceObserverModel = leftSequenceObserverModel;
_rightSequenceObserverModel.ValueChanged += (o, e) => { RaisePropertyChanged("RightSequence"); };
_rightSequenceObserverModel.UnitChanged += (o, e) => { RaisePropertyChanged("CurrentRightTemperatureUnit"); };
_leftSequenceObserverModel.ValueChanged += (o, e) => { RaisePropertyChanged("LeftSequence"); };
_rightSequenceObserverModel.UnitChanged += (o, e) => { RaisePropertyChanged("CurrentLeftTemperatureUnit"); };
}
public ICommand ObserveInputSequence
{
get
{
return new RelayCommand<temperatureunit>(d =>
{
Log.Debug("ObserveInputSequence " + ( d == CurrentLeftTemperatureUnit ? UnitConversionMode.LeftToRight : UnitConversionMode.RightToLeft));
_controllerService.RewireSequenceObservers(d == CurrentLeftTemperatureUnit ? UnitConversionMode.LeftToRight :
UnitConversionMode.RightToLeft, d, Scheduler.Default, DispatcherScheduler.Current);
});
}
}
</temperatureunit>
ViewLocator Injects ViewModel as DataContext
<window class="emphasis" mc:ignorable="d" name="AppWindow" span="" x:class="AlanAamy.Net.TemperatureConverter.Views.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:behaviours="clr-namespace:AlanAamy.Net.TemperatureConverter.Infrastructure.Behaviours" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">DataContext="{Binding Path=MainPage, Source={StaticResource Locator}}"
Title="Temperature Converter (C) Arup Banerjee, 2015" MinHeight="325" MinWidth="575" MaxWidth="575"
Height="Auto" SizeToContent="WidthAndHeight" Icon="..\Infrastructure\Resources\temperature-64x64.png">
<border borderbrush="WhiteSmoke" borderthickness="3" cornerradius="10" dockpanel.dock="Top" horizontalalignment="Center" margin="5,5,5,5" maxwidth="550" minheight="300" verticalalignment="Center">
</border></window>
Unit Tests
Some crucial Unit Tests techniques and design have been discussed above. These below test covers the functional specification Testing. You can check the details of implementation in the code included.
Code Coverage
Code Coverage is an important tool, which gives an idea how our code is being used. But we should not get biased with the overall percentage. For example I do not use attached code and hence InitializeComponent can be safely removed. Similarly I use only the generic version of the RelayCommand<T>
as I’m using commandParameter. However keeping the non Generic within the libray does not harm even though it is not use in this ViewModel. ViewModelLocator shows coverage 0%, but that is not exactly true, because the ViewModelLocator is only used once to inject the MainView . The coverage and snapshot starts well after the View appears, by which time the ViewModelLocator has already done its job. Hence Code coverage is very useful, but we should use the figures judiciously.
Nuget Packages
Nuget is a versatile package management tool and has been used in this project.
Application package List
<packages>
<package id="log4net" targetframework="net40" version="2.0.3">
<package id="Rx-Core" targetframework="net40" version="2.2.5">
<package id="Rx-Interfaces" targetframework="net40" version="2.2.5">
<package id="Rx-Linq" targetframework="net40" version="2.2.5">
<package id="Rx-Main" targetframework="net40" version="2.2.5">
<package id="Rx-PlatformServices" targetframework="net40" version="2.2.5">
<package id="Rx-WPF" targetframework="net40" version="2.2.5">
<package id="Rx-XAML" targetframework="net40" version="2.2.5">
</package></package></package></package></package></package></package></package></packages>
Unit Test package List
<packages>
<package id="Moq" targetframework="net45" version="4.2.1502.0911">
<package id="NUnit" targetframework="net45" version="2.6.4">
<package id="Rx-Core" targetframework="net45" version="2.2.5">
<package id="Rx-Interfaces" targetframework="net45" version="2.2.5">
<package id="Rx-Linq" targetframework="net45" version="2.2.5">
<package id="Rx-Main" targetframework="net45" version="2.2.5">
<package id="Rx-PlatformServices" targetframework="net45" version="2.2.5">
<package id="Rx-Testing" targetframework="net45" version="2.2.5">
</package></package></package></package></package></package></package></package></packages>
Download Code
You can download the code AlanAamy.Net.TemperatureConverter. Note that the package binaries are not included. You will need to be online while building the solution which will automatically restore the required packages.