Introduction
ImageFanReloaded is a light-weight image viewer for .NET 4.5.1, supporting multi-core processing.
ImageFanReloaded is a brand-new effort that replicates and improves upon the functionality of the previous ImageFan project, while using contemporary technology (WPF, .NET Framework 4.5.1, Moq) and employing powerful principles and practices (abstract factory, dependency inversion, Model-View-Presenter, asynchronous programming with async / await, interaction testing).
Background
I have always wanted to exploit the capabilities of .NET to create an image viewer offering a managed counterpart to the existing C / C++ solutions. Also, the lack of 64 bit image viewers has enticed me to pursue a .NET solution that would implicitly run as a 64 bit application, hardware and software platform permitting.
Visual Layout and Functionality
The application is designed in the traditional style of modern image viewers as follows:
- A folders and drives tree on the left-hand side of the window
- A thumbnails panel on the right-hand side, revealing the thumbnails of the images contained within the selected tree node folder or drive
Selecting a specific folder or drive fills the thumbnails panel with the contained thumbnails. The thumbnail list can be navigated by using the WASD, arrows, Space-Backspace and PgUp-PgDn keys. A thumbnail can be selected by navigating to it using the configured keys, clicking on it, or as a result of the full screen view or image view navigation.
If the thumbnail is clicked while selected or pressed Enter upon, it will set the image to full screen view, resizing it if necessary to fit on the screen. In the full screen mode, one can traverse the images list by using the WASD, arrows, Space-Backspace and PgUp-PgDn keys and the mouse wheel. The user can exit this mode by pressing the Esc key.
While in full-screen mode, clicking or pressing Enter will change the display mode to windowed, containing the image in full size, scrollable if at least one dimension of the image is greater than the corresponding screen size dimension. In the windowed mode, one can traverse the images list by using the WASD, arrows, Space-Backspace and PgUp-PgDn keys. The user can exit this mode by clicking or pressing the Esc or Enter keys.
Implementation Challenges and Constraints
The project structure is revealed in the diagram below. I will explain each relevant source code element in turn.
Types MainPresenter, IMainView and MainView
These three types, along with the supporting model types, feature the Model-View-Presenter pattern, used to decouple the UI from the business logic code. Although WPF is most often used with the Model-View-ViewModel pattern, in the case of an image visualization application I preferred the MVP pattern as being more flexible, while maintaining the elegance of dependency inversion and separation of concerns.
In the previous ImageFan project there was a class named TasksDispatcher
that partitioned the individual thumbnail generation tasks over the number of processor cores availalable. In this project, I rely on the fine-grained concurrency support exposed by the async-await pattern in .NET 4.5.1 instead. The responsibility of the tasks dispatcher has thus been taken over by a single async method in the MainPresenter
type, reducing the code base and simplifying the logic greatly.
private async void PopulateThumbnails(string folderPath)
{
var entered = false;
try
{
Monitor.Enter(_populateThumbnailsLockObject, ref entered);
using (_cancellationTokenSource = new CancellationTokenSource())
{
var token = _cancellationTokenSource.Token;
var getImageFilesTask = Task.Run<IEnumerable<ThumbnailInfo>>(() =>
{
var imageFiles = TypesFactoryResolver.TypesFactoryInstance
.DiscQueryEngineInstance
.GetImageFiles(folderPath);
var thumbnailInfoCollection =
imageFiles
.Select(anImageFile => new ThumbnailInfo(anImageFile))
.ToArray();
return thumbnailInfoCollection;
});
var thumbnails = await getImageFilesTask;
if (token.IsCancellationRequested)
return;
_mainView.ClearThumbnailBoxes();
for (var thumbnailCollection = thumbnails;
thumbnailCollection.Any();
thumbnailCollection =
thumbnailCollection.Skip(GlobalData.ProcessorCount))
{
var currentThumbnails =
thumbnailCollection.Take(GlobalData.ProcessorCount);
var readThumbnailInputTask = Task.Run<TaskStatus>(() =>
{
foreach (var aThumbnail in currentThumbnails)
{
if (token.IsCancellationRequested)
return TaskStatus.Canceled;
aThumbnail.ImageFile.ReadThumbnailInputFromDisc();
}
if (token.IsCancellationRequested)
return TaskStatus.Canceled;
else
return TaskStatus.RanToCompletion;
}, _cancellationTokenSource.Token);
var readThumbnailInputTaskStatus = await readThumbnailInputTask;
if (readThumbnailInputTaskStatus == TaskStatus.Canceled)
return;
_mainView.PopulateThumbnailBoxes(currentThumbnails);
var getThumbnailsTask = Task.Run(() =>
{
currentThumbnails
.AsParallel()
.AsOrdered()
.ForAll(aThumbnail =>
{
if (!token.IsCancellationRequested)
{
var currentThumbnail = aThumbnail.ImageFile.Thumbnail;
currentThumbnail.Freeze();
aThumbnail.ThumbnailImage = currentThumbnail;
}
});
}, _cancellationTokenSource.Token);
}
}
}
finally
{
if (entered)
Monitor.Exit(_populateThumbnailsLockObject);
}
}
Type FileSystemEntryTreeViewItem
This type is a WPF user control, extending the TreeViewItem
control. Each such control displays a drive or folder name and its icon within a WPF TreeView
control. When a FileSystemEntryTreeViewItem
node is selected, it triggers the display of thumbnails in the thumbnails panel.
Type ThumbnailBox
This type is a WPF custom control, inheriting from the UserControl
type. It is a variable-size control that displays an image thumbnail box, containing the image thumbnail itself, decorated with the image file name. Its largest dimension, whether width or height, is scaled to GlobalData.ThumbnailSize
(200 pixels), while the other dimension is scaled proportionally.
Type ImageView
ImageView
is a WPF Window that is shown as a dialog, featuring the modes full-screen and windowed. The full-screen mode resizes the image, if it is larger than the full screen dimensions available, while maintaining the initial aspect ratio. The windowed mode preserves the image size, enabling scrolling in case the image exceeds the screen size.
Type DiscQueryEngine
This type returns the list of disc drives and special folders of the system, as well as the sub-folders and image files within a specified drive or folder.
Type ImageFile
The type reads the image file from the disc on demand only. It returns the verbatim image as read from disc, a thumbnail of the image, or a full-screen resizing of the image. It operates internally on disposable Image
objects, after which the results are converted to the WPF-specific non-disposable ImageSource
objects. ImageFile
exhibits only partial IDisposable
behaviour, thus it does not implement the IDisposable
interface.
Type ImageResizer
This type features two image-resizing operations: to a thumbnail with a provided maximum dimension and to an image whose width and height are explicitly given.
Type GlobalData
This class contains references to constant and read-only application-wide data: resource images, the thumbnail size, navigation keys and the number of processor cores in the system.
Testing
When a visually-intensive application is being developed, the following question arises naturally: "What can be tested automatically?". The proper answer is: "Quite a lot.". ImageFanReloaded
has been designed with testability in mind. The testing framework employed is MSTest
.
The application features classical unit tests, interaction unit tests (using Moq
) and functional tests. All of the logic depends on abstract interfaces, rather than concrete types, therefore supporting the dependency inversion principle. To aid in its testability, the Abstract Factory
design pattern has been used, with one factory for normal production use and another returning stub instances for testing purposes.
Wrap-up
The construction of the application from ground up, while maintaining the requirement from its Windows Forms
.NET 2.0 predecesor, the ImageFan project, has been a rewarding undertaking.
It has granted me the opportunity to move to a newer technology (WPF
), break former dependencies between types, use the Model-View-Presenter
pattern, add tests, favour a simpler concurrency model for thumbnail generation and much more.
In terms of the perceived satisfaction of using the application, I have found this WPF
implementation smoother and slicker, with better reponsiveness and having a much more pleasant, quite unnoticeable, thumbnails refresh, when scrolling the thumbnails panel. On the downside, the memory footprint is somewhat larger, while the reliance of WPF
on the non-disposable ImageSource
type, as opposed to the Windows Forms
disposable Image
type, makes for some memory usage spikes, before the expected garbage collection occurs.
Since it is written in a managed language (C#.NET), ImageFanReloaded
is just-in-time compiled, specifically targeting the user's platform, whether x86 or x64. As such, it uses a single binary, while offering the full benefits of the running platform.
Source Code and Application Download
The complete source code of the ImageFanReloaded
application (a Google Code project) can be accessed here. If one is only interested in the binaries, they can be downloaded from this link.
I would gladly welcome contributions and feedback to this ImageFan open-source (GPL v3) project.
References
- [1] The Microsoft Developer Network (MSDN) pages
- [2] Robert C. Martin, Clean Code, Prentice Hall, 2008
- [3] Roy Osherove, The Art of Unit Testing: with Examples in .NET, Manning Publications, 2009
History
- Version 0.1 - Initial submission - 2 March 2015
- Version 0.2 - Updated source code and binaries, minor article editing - 13 March 2015