Introduction
The purpose of this solution is to retrieve files in directories and sub-directories that you specify and display still and motion pictures out of these files in a random order.
Description of the Interface
The display is composed of:
- Menu
- Picture
- Bottom bar appearing mostly in yellow background, contains both information and allows actions on the picture set
The Menu items allow you to change all the factors provided in the configuration file.
The picture is “clickable”, mouse selection of the picture toggles the rotate / hold-still mode of picture rotation. The rotation mode is accompanied with an indicator represented as a traffic-light circle kind of indicator, in the bottom bar. Green for rotating automatically mode, Red for holding-still.
The bottom bar allows you to move back and forth through the pictures in the order they were displayed, using the left arrow and the right arrow at each end of the bottom bar. The bottom bar also contains the full path of the picture displayed.
Installation
Modify the App.config file by modifying the following, value, entries in the appSettings
section:
<appsettings>
<add key="Initial Folders" value="G:\Pictures"/>
<add key="Max picture tracker depth" value="1000"/>
<add key="Still pictures" value=".jpg;.bmp;.gif;.png;.psd;.tif"/>
<add key="Motion pictures" value=".mov;.avi;.mpg;.mp4;.wmv;.3gp"/>
<add key="Image stretch" value="Uniform"/>
<add key="Timespan between pictures [Seconds]" value="10"/>
<add key="First picture to display" value="G:\Pictures\Ben\IMG_0840-1.JPG"/>
<add key="On start image rotating" value="True"/>
</appsettings>
Also modify the value
entry of file type="log4net.Util.PatternString"
, part of the appender
entry in the log4net
config section:
<log4net>
...
<appender name="AppRollingFile" type="log4net.Appender.RollingFileAppender">
<file type="log4net.Util.PatternString"
value="G:\Logs\RotatePictures\RotatePictures.%date{yyyyMMdd}.log"/>
...
</appender>
...
</log4net>
Using the Code
The solution is a relatively simple MVVM project, resulting in an executable that is about 100KB.
Model
The model, represented by the PictureModel
class, is a file name repository. PictureModel
relies on the PictureCollection
class that stores all the file names in the directories provided in the "Initial Folders" of the configuration file, filtered by the allowable extensions.
The model also relies on the SelectionTracker
class that stores the file names that were displayed and allow for back and forth motion of pictures using the arrows in the bottom bar.
The ViewModel, MainWindowViewModel
, holds a reference to the model, though it does not instantiate the class. which is left to a factory:
_model = (PictureModel)ModelFactory.Inst.Create("PictureFileRepository");
The model, PictureModel
, loads the pictures asynchronously:
_taskModel = Task.Run(() => RetrievePictures(), _cts.Token);
Thereafter, the pictures are selected in a random fashion:
var cnt = _picCollection.Count;
var index = _rand.Next(cnt);
return _picCollection[index];
ViewModel
The ViewModel
, for example, MainWindowViewModel
drives the View, MainWindow
. The ViewModel
section includes other window controls, that are used to display and alter information provided in the .config file.
Communication Between the ViewModels
The purpose of the MVVM pattern is to separate concerns between the disparate logical components, so the View is responsible for the display of information only which is separate from the model that is responsible for retrieving and saving data which is separate from the “glue” between them called the ViewModel
.
However, we need to communicate between the various ViewModel
components and we should not let one ViewModel
component hold an instance of another ViewModel
component. The communication between the ViewModel
components is done through a Messenger
, creatively named Messenger
.
The recipient of a message needs to register and potentially unregister the message. Between the time that the message is registered and before it is unregistered, any class can send the registered message to be handled by the class that registered to handle the message.
The ViewModel FileTypesToRotateViewModel
registers the message SelectedMetadataMessage
and processes requests as follows:
Step 1
Messenger.DefaultMessenger.Register<selectedmetadatamessage>(
this, OnMetaDataProcess, MessageContext.SelectedMetadataViewModel);
The third (3rd) parameter, MessageContext.SelectedMetadataViewModel
, is a string
used as context for the message. Context, in this scenario, is a differentiating mechanism for messages.
This registering class, FileTypesToRotateViewModel
, is not instantiated until the Main Window, MainWindowViewModel
, asks for it be instantiated, and as such it cannot receive nor handle messages until it is instantiated.
Step 2
Upon selecting the menu item Tools / Pictures meta data…
The command processing for this menu item, part of the MainWindowViewModel
, calls:
_pictureMetadataService.ShowDetailDialog(metaData);
Where the parameter, metaData
, is the data needed to populate the View, FileTypesToRotateView
and _pictureMetadataService
is an instance of the service, FileTypeToRotateService
, responsible for displaying the FileTypesToRotateView
window, as a dialog. The metaData
parameter will be passed on in the SelectedMetadataMessage
message, after the FileTypesToRotateView
window was created by the service, FileTypeToRotateService
. The metaData
will be sent via the following message:
Messenger.DefaultMessenger.Send(
new SelectedMetadataMessage(metadata),
MessageContext.SelectedMetadataViewModel);
Step 3
FileTypesToRotateViewModel
’s callback method, OnMetaDataProcess
(see Step 1 registration), processes the message, in our case, the callback displays the information within the window, FileTypesToRotateView
.
On the Way Back
When the user selects the OK button, in the FileTypesToRotateView
class, the above three (3) steps are used again to update the result of the window. The main window’s ViewModel
, MainWindowViewModel
, register this same SelectedMetadataMessage
but a different context.
Step 1
Messenger.DefaultMessenger.Register<SelectedMetadataMessage>(
this, OnSetMetadataAction, MessageContext.SetMetadata);
Step 2
The handling of the OK button in the FileTypesToRotateViewModel
sends:
Messenger.DefaultMessenger.Send(
new SelectedMetadataMessage(metadata),
MessageContext.SetMetadata);
Then still part of handling the OK button, FileTypesToRotateViewModel
unregisters from the SelectedMetadataMessage
message:
Messenger.DefaultMessenger.Unregister(
this,
MessageContext.SelectedMetadataViewModel);
The Unregister
is necessary, otherwise every time the window FileTypesToRotateView
is instantiated, a new call to the register will happen which will duplicate and triplicate the handling.
Step 3
MainWindowViewModel
handles the updates from the FileTypesToRotateViewModel
.
View
The MVVM Equation
The views are XAML defined controls. Each View control instantiates the appropriate ViewModel
class and assigns its DataContext
to it. However, the View
does not instantiate the ViewModel
directly, it calls a factory class.
DataContext = VmFactory.Inst.Create(this);
The VmFactory
class is a simple class using reflection to instantiate the ViewModel
class.
Handling Errors
The FileTypesToRotateView
needs to handle errors. The XAML file adds the following template:
<grid.resources>
<style targettype="{x:Type TextBox}" type="text/css">
<Setter Property="Validation.ErrorTemplate">
<Setter.Value>
<ControlTemplate>
<StackPanel>
<AdornedElementPlaceholder>
<Border BorderBrush="Red" BorderThickness="2"/>
</AdornedElementPlaceholder>
<ItemsControl ItemsSource="{Binding}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding ErrorContent}"
Foreground="Red"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</ControlTemplate>
</Setter.Value>
</Setter></style>
</grid.resources>
Which paints the border of the TextBox
containing the error red and displays the error message under the TextBox
.
The TextBox
that it needs to opt into the error program needs to mark its binding with ValidatesOnDataErrors=True
, and potentially ValidatesOnExceptions=True
, as follows:
<textbox grid.column="1" grid.columnspan="2" grid.row="1"
margin="0,3,2,16" text="{Binding PictureFolders,
Mode=TwoWay, UpdateSourceTrigger=PropertyChanged,
ValidatesOnDataErrors=True, ValidatesOnExceptions=True}"
tooltip="You may specify more than one full-pathed folder.
Folders are semicolon separated"
tooltipservice.showduration="30000" x:name="InitialFolder">
</textbox>
Then the VM, FileTypesToRotateViewModel
, in addition to implementing the INotifyPropertyChanged
, it needs to implement IDataErrorInfo
.
I accomplished it through a class, ViewModelBase
, that FileTypesToRotateViewModel
inherits.
The ViewModelBase
implements both INotifyPropertyChanged
and IDataErrorInfo
. We will concentrate on the IDataErrorInfo
. The IDataErrorInfo
calls for two (2) properties:
public string this[string propertyName] { get { ... } }
public string Error { get { ... } }
The way the ViewModelBase
class implements it is separating the base class from the rules. The base class defines an ErrBinder
class and a dictionary of (key=property name, value=ErrBinder
instance), in _errBinderMap
:
private readonly Dictionary<string, errbinder> _errBinderMap = new Dictionary<string, errbinder>();
Then a rule agnostic AddRule
method:
protected void AddRule(string propertyName, Func<bool> ruleDelegate, string errorMessage)
{
var rv = new ErrBinder(ruleDelegate, errorMessage);
_errBinderMap.Add(propertyName, rv);
}
That is used by the VM class, FileTypesToRotateViewModel
. This VM class knows what the rules should be and as such defines them.
The ErrBinder
class’s main function is to allow an error evaluation upon request through the method: ErrEvaluate()
.
private class ErrBinder
{
private readonly Func<bool> _ruleValidate;
private readonly string _message;
internal ErrBinder(Func<bool> ruleValidate, string message)
{
_ruleValidate = ruleValidate;
_message = message;
}
internal string Error { get; set; }
internal bool HasError { get; set; }
internal void ErrEvaluate()
{
Error = null;
HasError = false;
try
{
var rc = _ruleValidate();
HasError = !rc;
if (rc) return;
Error = _message;
HasError = true;
}
catch (Exception e)
{
Error = e.Message;
HasError = true;
}
}
}
Now filling in the implementation of IDataErrorInfo
is easy:
public string this[string propertyName]
{
get
{
if (!_errBinderMap.ContainsKey(propertyName)) return null;
_errBinderMap[propertyName].ErrEvaluate();
return _errBinderMap[propertyName].Error;
}
}
public string Error
{
get
{
var errors = _errBinderMap.Values.Where(b => b.HasError).Select(b => b.Error);
return string.Join(Environment.NewLine, errors);
}
}
In addition, we need a HasErrors
property, this method will allow us to set the CanExecute
method on the OK button of the form.
protected bool HasErrors
{
get
{
var values = _errBinderMap.Values.ToList();
values.ForEach(b => b.ErrEvaluate());
return values.Any(b => b.HasError);
}
}
Points of Interest
There are two types of extensions that we filter on: the still pictures extensions and the motion pictures extensions, therefore we need to filter all files based on the union of both sets of extensions. Linq provides a Union()
method. This Linq Union()
needs to compare objects for equality, so in our extensions case, it is string
comparison which by default is case sensitive. We are looking for case insensitive comparison, “.mov
” is the same as “.MOV
”.
Linq provides a Union()
overload method with the following signature:
public static IEnumerable<TSource> Union<TSource>(this IEnumerable<TSource> first,
IEnumerable<TSource> second, IEqualityComparer<TSource> comparer)
See the site http://sourceof.net, where Microsoft publishes the source code to .NET, then search for Enumerable.Union
, searching for Union
only yields too many hits.
At this point, we can provide a specific solution for our problem at hand but what we really would like is a generic solution where we could write the Union()
method as follows:
_stillExt.Union(_motionExt,
(x, y) => string.Compare(x, y, StringComparison.OrdinalIgnoreCase) == 0);
This is possible by defining our own LinqExtensions static
class:
public static class LinqExtensions
{
public static IEnumerable<TSource> Union<TSource>(
this IEnumerable<TSource> first,
IEnumerable<T> second,
Func<TSource, TSource, bool> cmpr)
{
return first.Union(second, new LinqComparer<TSource>(...));
}
}
LinqComparer
will be a class that inherits from IEqualityComparer<in T>
(see http://sourceof.net). The IEqualityComparer<in T>
interface demands implementation of:
bool Equals(T x, T y);
int GetHashCode(T obj);
So, for example:
public class LinqComparer<TSource> : IEqualityComparer<TSource>
{
private readonly Func<TSource, TSource, bool> _linqCmp;
public LinqComparer(Func<TSource, TSource, bool> cmp)
{
_linqCmp = cmp ?? throw new ArgumentException(
@"comparison function may not be null ", nameof(cmp));
}
public bool Equals(TSource x, TSource y) => _linqCmp(x, y);
public int GetHashCode(TSource x) => 0;
}
This last GetHashCode(TSource x) => 0
is an issue for a large set, a return of 0
for every GetHashCode()
means that a full comparison will be done on every comparison, as opposed to leverage integer comparison via the HashCode
.
As such, we will include a different approach. LinqComparer
will be as follows:
public class LinqComparer<TSource> : IEqualityComparer<TSource>
{
private readonly Func<TSource, TSource, bool> _linqCmp;
private readonly Func<TSource, int> _hashCode;
public LinqComparer(Func<TSource, TSource, bool> cmp,
Func<TSource, int> hashCode = null)
{
_linqCmp = cmp ?? throw new ArgumentException(
@"comparison function may not be null", nameof(cmp));
_hashCode = hashCode ?? (T => 0);
}
public bool Equals(TSource x, TSource y) => _linqCmp(x, y);
public int GetHashCode(TSource x) => _hashCode(x);
}
And the LinqExtensions
class will be:
public static class LinqExtensions
{
public static IEnumerable<T> Union<T>(
this IEnumerable<T> a,
IEnumerable<T> b,
Func<T, T, bool> cmpr,
Func<T, int> hashCode = null) =>
a.Union(b, new LinqComparer<T>(cmpr, hashCode));
}
Now calling the Union
method is formed as follows:
_stillExt.Union(_motionExt,
(x, y) => string.Compare(x, y, StringComparison.OrdinalIgnoreCase) == 0),
x => x.ToLower().GetHashCode());
Achieving our goal of using a lambda expression for the comparison of the Union()
method.
Conclusion
We have achieved separation of concerns and a working picture rotation application.
Thank you for reading this article!