In this article, you will learn about a WPF smart client demonstrating the nitty gritty of MVVM for a real life LOB application.
Contents
The internet is teeming with examples demonstrating MVVM. The beginner however, often has to run to different sources for the nitty gritties involved in implementing MVVM in "real life" LOB apps. XPence started as a POC to deepen my understanding of Metro (aka. Modern) UI design principles, but gradually became a labour of love. This app brings almost all the best practices required to implement MVVM in a Line of business application, at one place.
A friend of mine once argued that MVVM forces the use of many frameworks like IOC Containers or MVVM frameworks. I repeat here what I replied to him: MVVM sets guidelines for application architecture that lets you write robust and easy to change software. The frameworks are for making our lives easy by avoiding writing of boilerplate code. MVVM does not mandate using any framework. While writing XPence, use of any framework has been deliberately avoided so that the purity of MVVM is not shadowed by the framework. Having said that, I strongly recommend the use of frameworks. They help keeping the code cleaner and more organised. A lot of open source frameworks are available and they are designed to be MVVM friendly. To list a few:
- Cinch by Sacha Barber: An awesome MVVM Framework
- MVVM Light: A lightweight and effective MVVM framework
- Caliburn Micro: A very popular framework that provides IOC container as well as MVVM base classes
- MEF: My personal favourite IOC container that may be used along with PRISM
Who Is This Article For?
It is often a difficult task to mark the target audience. All I can say is, it would interest:
- Beginners who are starting to implement MVVM in LOB apps and are still bugged by a lot of "how"s. However, the article expects an elementary understanding of MVVM (what it is, the magic of binding, etc.). If you are absolutely new to MVVM, you can find a lot of resources over the internet. My suggestions would be the following few:
- Also the article assumes you are good with WPF concepts like DependencyProperties, Styles, Templates, Converters, etc.
- The MVVM Pros who don't mind revisiting the concepts and are open to debate on some of them.
Before we proceed further, I would request you to read the bold text in the That's it! section.
To build the code, you would need Visual Studio 2022 (Express/ Professional/ Ultimate).
Few external DLLs are also used but they are packaged with the source code so you need not worry about getting them separately. To know more, visit the Acknowledgements and external components section.
For persisting data, MS SQL Server CE is used. Before you could run the application, you will need MS SQL Server CE 4.0. It involves a few points to consider before you actually install it. This page provides the details you would need for installing the MS SQL Server CE.
If you don't want to install SQL Server CE 4.0, there is still a way out.
- Download the SQL Server CE DLLs (SQLDlls) from the top of this page and place the two folders (amd64 and x86) inside the Output directory (refer Directory structure).
- Go to the app.config file of XPence project and uncomment the
system.data
tag.
Rebuild the project and it should work. I tested this on a 64 bit machine that did not have SQL Server CE 4.0 installed. However, the DLLs provided are 32 bit DLLs. If your machine is 64 bit, these DLLs would still run as 32 bit.
I am an advocate of using open source and hence XPence uses a lot of external components. Some are open source and the rest are shameless thefts from other authors. Credits and acknowledgements should go where they belong:
- XPence uses Mahapps Metro for the metro/ modern UI. It is an awesome WPF library for building Metro looking UIs.
- XPence uses the Apex grid for cleaner and shorter XAML files. Apex provides many other features too, though not utilized by XPence.
- Syncfusion's free Metro Studio came in very handy for the XAML figures used in XPense.
- A lot of code for creating an image cropper control has been taken from this article.
- A sliding content control came in handy for metro experience. Mahapps metro provide their own metro content control but I preferred this for more vibrant sliding.
- XPence encrypts its passwords for saving in the DB. This article provided all the code for that.
- The compact data viewer came in very handy during development.
- NHibernate is used as the (preferred) ORM (over EntityFramework!)
- For charting, Modern UI Charts library is used.
- For logging, XPense uses log4net.
- This blog helped me overcome a WPF bug that bugged me for a while.
My sincere thanks to all the authors, whose code has been used in the application. You guys rock!
Let's have a look at what XPense is and how it is supposed to work. XPence is an expense tracker software for a small team. It is a "single admin and multiple users system" where the Admin enjoys certain enhanced authorization over the system. Let's have a walk through of the application as an admin and see what all it offers. Be sure to download the TestData (from the top of this page) to run it yourself!
On launch, the login screen appears.
Punch in the admin credentials and the home screen would be launched.
You may change your password or the theme and accent of the application from the top right settings icon. On clicking the icon, the Settings flyout opens:
XPence offers you to save the settings for future.
From the home screen, you may go to the all expense view where you can perform all CRUD operations for expenses entered by you or other users (normal users can do this only to expenses entered by them). Here is the screen shot for the all expenses screen:
You can filter the expenses using the filter flyout. Here is a screen shot for that:
Mind it, the normal users won't have the "filter by user" field enabled in their filter flyout.
And finally, the manage screen, to where you can navigate from the home screen, is something that only the admin has access to. You can change a user's name or picture and the same would be available on the emblem (top right corner of application window) when they login. Here is a screen shot of the manage screen:
That's all about "What XPence does?".
Before starting with the internals, let's have a look at the directory structure and understand how the code is organised.
-
As you extract the XPence.zip file, you will find three folders:
All in all, this structure makes distribution of executable and source code easy and also reduces the rebuild time!
-
Opening the solution in Visual Studio will present six projects. Remember the "separation of concerns" starts at this very level of creating projects. In my opinion, one should be a bit choosy when it comes to adding references to his project. I try to make the following things sure:
- The ViewModels should not be aware about the view and hence view specific DLLs like PresentationFramework.dll and System.Xaml.dll should not be referred to by the
ViewModel
project. Remember whatever your ViewModel
project refers will also be referred by its test project and ViewModel
developers should not at all be dependent on any View components for testing. - The
Model
project should not be referred to by the View
project directly (more on this later). It's a temptation for the beginners to put enumerations in Model
project(s). But this may not be right for every case. If you have an ItemsControl
(like ComboBox
) in View
that expects the enumeration for its DataSource
, the View
project would need to refer to the Model
. Therefore, it's better to put the enumerations in a shared project that would be referred to by the View
, ViewModel
and Model
projects independently. - Constants, Resource strings, etc. that may be shared across
View
, ViewModel
and Model
should be placed in shared project. This project may contain the enumerations too.
Here is a diagram that summarizes the above points:
These points help keep the code maintainable and testable in spite of multiple change requirements (that inevitably keep coming!), especially for large projects.
Here is a brief of the overview.
The Application Architecture
This is how the application architecture looks like:
I believe, most of the WPF developers following the MVVM pattern will find this familiar. The diagram is self explanatory so I won't explain much. However, a few comments that I would add are:
- The benefit of repository is that it is injected into the
ViewModel
against interface and therefore changing the implementation would have least impact. Whatever be your data source, ViewModel
is agnostic about it. - A model, no matter how simple and small, should always be wrapped inside a
ViewModel
before being rendered to the View
. A model's sole responsibility is to carry data and (may be) validate it. The model should not be compliant with any of the requirements of the View.
My colleague did not like my idea of wrapping models that contained just one string
property inside a ViewModel
. The models would form items for a combobox
. A few days later, a requirement came where the users wanted a button with every combobox item. The wrapper Viewmodel
exposed an ICommand
property and the requirement was over!
- Since the data operations are abstracted by the
Repository
, the choice of using any ORM becomes wider. NHibernate does not go very well with WPF but not if you have a repository.
The UI Sections
Here is a brief of UI sectioning.
If you have any doubt about the diagram above, be patient as we will have a detailed look when we see the components.
The various components that are worth making a note of are listed below:
Messaging service is responsible for a number of tasks, viz:
- Showing a message box
- Showing a pop up window (modal or otherwise) with custom content
- Showing a busy message and blocking the application window whenever the app is doing some background work
- Showing a flyout with custom content
The messaging service is an interface that is provided to any ViewModel
through constructor injection. The ViewModel
, thus is never aware of any View
specific code (e.g., Message box, Pop up window, etc.) This is how the interface looks. It provides all members that are required for the above operations.
public interface IMessagingService
{
void ShowMessage(string message);
void ShowMessage(string message, string header);
DialogResponse ShowMessage(string message, DialogType dialogueType);
DialogResponse ShowMessage
(string message, string header, DialogType dialogueType);
void ShowProgressMessage(string header, string message);
void CloseProgressMessage();
void ShowCustomMessageDialog
(string viewKey, ModalDialogViewModelBase viewModel);
void ShowCustomMessage(string viewKey, ModalDialogViewModelBase viewModel);
void RegisterFlyout(FlyoutViewModelBase flyoutViewModelBase);
}
Note that the DialogType
and DialogResponse
enumerations reside in the XPence.Infrastructure
project. The internal implementation of this interface is maintained as a singleton instance by MessageServiceFactory
class. This is where I missed an IOC container the most. Without constructor injection stubbing data for Unit Testing would become a nightmare and so the singleton instance is created at the application start up, but passed to every view model through constructor injection.
Custom Pop Up Screens
It's worth spending a moment looking at how the custom pop up messages are being shown in XPence. If you have noticed, the IMessagingService
interface you might have noticed that the parameters of two members, viz: ShowCustomMessageDialog
and ShowCustomMessage
, expects a string
viewKey and a parameter of type ModalDialogViewModelBase
. As you may have guessed correctly, this is the ViewModel
that serves any pop up view. The key is used by the messaging service to identify the type of UserControl
that will be bound to the instance of ModalDialogViewModelBase
passed. We will see in a while from where the messaging service gets the type of UserControl
using the key. First let's have a look at how the ModalDialogViewModelBase
looks. Here is the code:
public abstract class ModalDialogViewModelBase : ViewModelBase
{
#region Public Members
public bool IsCancelled
{
get { return _isCancelled; }
set
{
if (value != _isCancelled)
{
_isCancelled = value;
OnPropertyChanged(GetPropertyName(() => IsCancelled));
}
}
}
public bool IsOk
{
get { return _isOk; }
set
{
if (value != _isOk)
{
_isOk = value;
OnPropertyChanged(GetPropertyName(() => IsOk));
}
}
}
public string TitleText
{
get { return _titleText; }
set
{
if (value != _titleText)
{
_titleText = value;
OnPropertyChanged(GetPropertyName(() => TitleText));
}
}
}
#endregion
#region Commands
public ICommand OkSelectedCommand { get; private set; }
public ICommand CancelSelectedCommand { get; private set; }
#endregion
#region Constructor
protected ModalDialogViewModelBase()
{
OkSelectedCommand = new RelayCommand(OkSelected);
CancelSelectedCommand = new RelayCommand(CancelSelected);
}
#endregion
#region Protected Overidable methods
protected virtual void OnOkSelected()
{
}
protected virtual void OnCancelSelected()
{
}
#endregion
#region Internal Events
internal event EventHandler DialogResultSelected
{
add { _dialogResultSelected += value; }
remove { _dialogResultSelected -= value; }
}
#endregion
#region Private Methods
private void OkSelected()
{
IsOk = true;
NotifyView(DialogResponse.Ok);
}
private void CancelSelected()
{
IsCancelled = true;
NotifyView(DialogResponse.Cancel);
}
private void NotifyView(DialogResponse dialogResponse)
{
if (null != _dialogResultSelected)
_dialogResultSelected(this, EventArgs.Empty);
}
#endregion
#region Private Members
private bool _isCancelled;
private bool _isOk;
private string _titleText;
internal event EventHandler _dialogResultSelected;
#endregion
}
The ModalDialogViewModelBase
gives the basic properties of IsOk
and IsCancelled
that gives the user choice made on that dialog. The consumer code can then carry on with its operations depending on the user choice. It also offers the OkSelectedCommand
and CancelSelectedCommand
that may be bound to any ICommandSource (usually Button
) in the pop up view. It offers an event DialogResultSelected
to which the view will hook. Whenever the user makes a choice (Ok/Cancel), the ModalDialogViewModelBase
records the response and fires this event. The pop up window, which is hooked to this event understands that it has to close now. If it is not very clear, please be patient, read on!
The Messaging Service uses a common Window called the ModalCustomMessageDialog
. All the Messaging Service does is:
- Open the
ModalCustomMessageDialog
. - Sets the
UserControl
, that it got using key, in the window's content. - Binds the window to the instance of
ModalDialogViewModelBase
passed.
The Window is intelligent enough to listen to its DataContext
for any notifications. Here is the code behind for ModalCustomMessageDialog
.
public partial class ModalCustomMessageDialog
{
#region Constructors
static ModalCustomMessageDialog()
{
}
public ModalCustomMessageDialog()
{
InitializeComponent();
}
#endregion
#region Static Event Handlers
private static void OnActualContentPropertyChanged
(DependencyObject source, DependencyPropertyChangedEventArgs e)
{
var modalWindow = source as ModalCustomMessageDialog;
if (null != modalWindow)
{
if (null != modalWindow.ActualContentHolder)
modalWindow.ActualContentHolder.Content = e.NewValue;
}
}
#endregion
public object ActualContent
{
get { return GetValue(ActualContentProperty); }
set { SetValue(ActualContentProperty, value); }
}
public static readonly DependencyProperty ActualContentProperty =
DependencyProperty.Register("ActualContent", typeof(object),
typeof(ModalCustomMessageDialog), new PropertyMetadata
(null, OnActualContentPropertyChanged));
#region Base class overrides
protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e)
{
if (e.Property == DataContextProperty)
{
var oldViewModel = e.OldValue as ModalDialogViewModelBase;
if (null != oldViewModel)
oldViewModel.DialogResultSelected -= OnViewNotified;
var newViewModel = e.NewValue as ModalDialogViewModelBase;
if (null != newViewModel)
newViewModel.DialogResultSelected += OnViewNotified;
}
else
{
base.OnPropertyChanged(e);
}
}
#endregion
#region Private Helpers
private void OnViewNotified(object sender, EventArgs e)
{
var viewModel = DataContext as ModalDialogViewModelBase;
if (null != viewModel)
viewModel.DialogResultSelected -= OnViewNotified;
Close();
}
#endregion
}
Now let's arrive at the question from where does the Messaging Service procure the type of UserControl
that it sets inside the modal dialog window. The answer is: from a singleton instance of a class called ModalViewRegistry
. At application start up, all pop up user control types are fed in the ModalViewRegistry
against their keys. It's as simple as that! Here is the code for ModalViewRegistry
class:
public class ModalViewRegistry
{
#region Public Members
public static ModalViewRegistry Instance { get { return _instance.Value; } }
#endregion
#region Public Methods
public void RegisterView(string key, Type userControlType)
{
if (string.IsNullOrEmpty(key))
throw new ArgumentException("key");
if (null == userControlType)
throw new ArgumentException("userControl");
if (!userControlType.IsSubclassOf(typeof(UserControl)))
throw new InvalidCastException
("Only a user control type can be assigned.");
if (_modalViewRegistry.ContainsKey(key))
throw new InvalidOperationException("Key already exists.");
_modalViewRegistry[key] = userControlType;
}
public bool ContainsKey(string viewKey)
{
return _modalViewRegistry.ContainsKey(viewKey);
}
#endregion
#region Internal Methods
internal UserControl GetViewByKey(string key)
{
Type userControlType = _modalViewRegistry[key];
return Activator.CreateInstance(userControlType) as UserControl;
}
#endregion
#region Member Variables
private readonly IDictionary<string,> _modalViewRegistry;
private static readonly Lazy<modalviewregistry> _instance
= new Lazy<modalviewregistry>(() => new ModalViewRegistry());
#endregion
#region Contructor
private ModalViewRegistry()
{
_modalViewRegistry = new Dictionary<string,>();
}
#endregion
}
One of the major features and attractions of Metro UI is Flyouts. MahApps metro developers have done a wonderful job of offering the flyouts of a MetroWindow as an ItemsControl
. This means the view model serving the MainWindow
of the application may contain a list of objects that can serve as flyouts. A Flyout
control of Mahapps metro has all the Dependency Properties for controlling the Visibility, Theme and Direction. Therefore, if we have a base ViewModel
that carries all the base properties for controlling a flyout, an inherited class from it can serve as the ViewModel
for a flyout that is shown in the MainWindow
. All we need is a collection of these base view models in the MainWindowModel
(aka. flyout container). This is how the FlyoutViewModelBase
looks:
public abstract class FlyoutViewModelBase : ViewModelBase
{
#region Public properties
public string Header
{
get { return _header; }
set
{
if (_header == value)
return;
_header = value;
OnPropertyChanged(GetPropertyName(() => Header));
}
}
public VisibilityPosition Position
{
get { return _position; }
set
{
if (_position == value)
return;
_position = value;
OnPropertyChanged("Position");
}
}
public bool IsOpen
{
get { return _isOpen; }
set
{
if (_isOpen == value)
return;
_isOpen = value;
OnPropertyChanged("IsOpen");
}
}
public FlyoutTheme Theme
{
get { return _theme; }
set
{
if (_theme == value)
return;
_theme = value;
OnPropertyChanged("Theme");
}
}
#endregion
#region protected members.
protected string _header;
protected VisibilityPosition _position;
protected bool _isOpen;
protected FlyoutTheme _theme;
#endregion
}
The next requirement that arises is any ViewModel
in the project may demand a flyout
. How to let that ViewModel
add an instance of a FlyoutViewModelBase
to the flyout
container? If we had an IOC container, we could have easily procured a singleton instance of the flyout
container. But we aren't using one. MessagingService
comes to our rescue by providing a method to add a FlyoutViewModel
from anywhere in the application. Here is a simplified diagram that explains how this works:
Navigation is an important functionality of any metro based application. While frameworks like PRISM offer robust Navigation functionalities, it is not an elephant task to make a small one for yourself. Mahapps metro offers a navigation functionality, but I did not like it because it provided a forward button too and I did not want to make a browser like application. I preferred writing a small Navigation functionality for myself. Well, let's see how the INavigator
interface works. Shall we? Being an Interface, it can be accessed from anywhere (read it as any ViewModel
) by IOC/DI. Here is the interface.
public interface INavigator:INotifyPropertyChanged
{
void NavigateBack();
void NavigateToHome();
WorkspaceViewModelBase CurrentView { get; set; }
IEnumerable<workspaceviewmodelbase> GetAllView();
void AddView(WorkspaceViewModelBase workspaceView);
void AddHomeView(WorkspaceViewModelBase workspaceView);
void NavigateToView(string viewKey);
}
The Navigator works in three very simple steps:
- It contains the collection of all
WorkspaceViewModels
. - It holds a current view that is in turn, is listened to for any change, by the
ApplicationViewModel
. - It offers the method
NavigateToView(string viewKey)
that takes the registered name of the WorksaceViewModelBase
and sets it as the current view.
It gives special attention to the HomeView
and offers a special NavigateToHome()
method. This is because from almost every place "navigate to home" may be demanded. I think, the implementation for the above mentioned functionality is pretty simple and straight forward and so I am not providing the implementation. Should you want to see that, you can see the internal class Navigator
in the source code.
A ViewModel
class is the major driving component of an MVVM application. In fact, to run a pure MVVM application, you don't need a View
component at all. A mere test project or a console project will do. It's no wonder then, that since ViewModel
classes do all the heavy loading, they may have time consuming and memory intensive code for initialization. If you see the ViewModelBase
class in the project (I am not pasting the code here), you will find an Initialize
method that in turn calls an overridable OnInitialize
method. Therefore, any derived ViewModel
class can place their initialization code in the overriden OnInitialize
method. Fair enough? But when to call the Initialize
method of a ViewModel
? Is it fine to call it at the application start up? Well, exactly that's the place! But how about this: I initialize the ViewModel
that caters to the Manage View and the user doesn't event care to load the Manage screen? I, for nothing, have got all the data that I need in my Manage screen and holding that in memory. Isn't it a better idea to initialize a ViewModel
only when the associated view is loaded? But we can't have code behind to do this. Attached behaviour comes to our rescue. If you want to understand what attached behaviors are, I suggest this article by Josh Smith. The InitializeDataContextWhenLoadedProperty
does the task of calling the ViewModel
's Initialize
method whenever an associated view is loaded. Here is the code:
public static class ElementLoadingBehavior
{
#region InitializeDataContextWhenLoaded
public static bool GetInitializeDataContextWhenLoaded(FrameworkElement element)
{
return (bool)element.GetValue(InitializeDataContextWhenLoadedProperty);
}
public static void SetInitializeDataContextWhenLoaded(
FrameworkElement element, bool value)
{
element.SetValue(InitializeDataContextWhenLoadedProperty, value);
}
public static readonly DependencyProperty
InitializeDataContextWhenLoadedProperty =
DependencyProperty.RegisterAttached(
"InitializeDataContextWhenLoaded",
typeof(bool),
typeof(ElementLoadingBehavior),
new UIPropertyMetadata(false, OnInitializeDataContextWhenLoadedChanged));
static void OnInitializeDataContextWhenLoadedChanged
(DependencyObject depObj, DependencyPropertyChangedEventArgs e)
{
var item = depObj as FrameworkElement;
if (item == null)
return;
if (e.NewValue is bool == false)
return;
if ((bool)e.NewValue)
item.Loaded += OnElementLoaded;
else
item.Loaded -= OnElementLoaded;
}
static void OnElementLoaded(object sender, RoutedEventArgs e)
{
if (!ReferenceEquals(sender, e.OriginalSource))
return;
var item = e.OriginalSource as FrameworkElement;
if (item != null)
{
var dataContext = item.DataContext as ViewModelBase;
if(null!=dataContext && !dataContext.IsInitialized)
{
dataContext.Initialize();
}
}
}
#endregion
}
Thus, with this behavior, our ViewModel
s are actually initialized on demand.
Although most of the styling in XPence is taken care of by MahApps metro, yet I preferred overriding a few styles. Also styles are provided for the custom controls that are native to XPence.
I always prefer XAML figures over PNG/JPEG images, because of their crisp and clean visual. It is worthwhile to have a look at how MetroButton
style is designed in XPence. Here is how the style looks:
<Style x:Key="MetroButtonStyle" TargetType="{x:Type Button}">
<Setter Property="Foreground" Value="{DynamicResource AccentColorBrush}"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Button}">
<grid x:name="LayoutRoot" background="Transparent"
rendertransformorigin="0.5,0.5">
<grid.rendertransform>
<transformgroup>
<scaletransform>
<skewtransform>
<rotatetransform>
<translatetransform>
</translatetransform></rotatetransform></skewtransform>
<viewbox>
<grid x:name="backgroundGrid" width="48"
height="48" visibility="Visible">
<path x:name="arrow" data="{TemplateBinding Content}"
stretch="Uniform" fill="{TemplateBinding Foreground}"
stroke="{TemplateBinding Foreground}" width="26"
height="26" strokethickness="0.1">
<ellipse x:name="circle" fill="Transparent"
stroke="{TemplateBinding Foreground}"
width="40" height="40" strokethickness="2">
<controltemplate.triggers>
<trigger property="IsFocused" value="True">
<trigger property="IsDefaulted" value="True">
<trigger property="IsMouseOver" value="True">
<setter property="RenderTransform" targetname="LayoutRoot">
<setter.value>
<transformgroup>
<scaletransform scalex="1.1" scaley="1.1">
<skewtransform>
<rotatetransform>
<translatetransform>
<trigger property="IsPressed" value="True">
<setter property="Opacity"
targetname="LayoutRoot" value="0.7">
</setter>
<trigger property="IsEnabled" value="False">
<setter property="Fill"
value="#8B8B8B" targetname="arrow">
<setter property="Stroke"
value="#8B8B8B" targetname="circle">
This style makes the button expect a Geometry
for its content. And the Path
in place of ContentPresenter
draws the geometry inside the ellipse. The Geometries
are stored as keyed resources in a separate ResourceFile
. This style though precarious, is smart.
Mahapps metro provides a static ThemeManager
class that provides methods for changing the theme and accent of your application. XPence uses a wrapper class over the ThemeManager
class called AppearanceManager
. A number of reasons are there for it:
- The theme manager of Mahapps metro expects parameters that are native to Mahapps metro. In the future, there might be a need to replace Mahapps with some other library. The change will be limited to the wrapper class.
AppearanceManager
class provides the themes and accents in the form of string
s to the ViewModel
layer which is good from "separation of concerns" perspective. - There are other components too that need theme and accent changes (like the charting component).
AppearanceManager
takes care of their needs too.
Here is the code of AppearanceManager
:
public class AppearanceManager
{
internal static readonly ResourceDictionary
LightChartResource = new ResourceDictionary { Source = new Uri
("pack://application:,,,/XPence.Infrastructure;component/Resources/ChartLight.xaml") };
internal static readonly ResourceDictionary DarkChartResource =
new ResourceDictionary { Source = new Uri
("pack://application:,,,/XPence.Infrastructure;component/Resources/ChartDark.xaml") };
private static readonly string LightThemeText;
private static readonly string DarkThemeText;
#region Constructors
static AppearanceManager()
{
LightThemeText = "Light";
DarkThemeText = "Dark";
}
#endregion
#region Public Static Methods
public static IEnumerable<string> GetAccentNames()
{
return ThemeManager.DefaultAccents.Select(a => a.Name).ToList();
}
public static IEnumerable<string> GetThemeNames()
{
var themes = new[] { LightThemeText, DarkThemeText };
return themes;
}
public static string GetApplicationAccent()
{
var theme = ThemeManager.DetectTheme(Application.Current);
return theme.Item2.Name;
}
public static string GetApplicationTheme()
{
var theme = ThemeManager.DetectTheme(Application.Current);
if (theme.Item1 == Theme.Dark)
return DarkThemeText;
if (theme.Item1 == Theme.Light)
return LightThemeText;
throw new Exception("Undetected theme.");
}
public static void ChangeAccent(string accentName)
{
var theme = ThemeManager.DetectTheme(Application.Current);
var accent = ThemeManager.DefaultAccents.First(x => x.Name == accentName);
ThemeManager.ChangeTheme(Application.Current, accent, theme.Item1);
}
public static void ChangeTheme(string themeName)
{
ChangeThemeForGraph(Application.Current.Resources, themeName);
if (string.CompareOrdinal(LightThemeText, themeName) == 0)
{
var theme = ThemeManager.DetectTheme(Application.Current);
ThemeManager.ChangeTheme(Application.Current, theme.Item2, Theme.Light);
}
else if (string.CompareOrdinal(DarkThemeText, themeName) == 0)
{
var theme = ThemeManager.DetectTheme(Application.Current);
ThemeManager.ChangeTheme(Application.Current, theme.Item2, Theme.Dark);
}
else
{
throw new ValueUnavailableException("Theme name not known.");
}
}
private static void ChangeThemeForGraph
(ResourceDictionary resources, string themeName)
{
if (resources == null)
return;
ResourceDictionary oldChartThemeResource;
ResourceDictionary newChartThemeResource;
if (string.CompareOrdinal(LightThemeText, themeName) == 0)
{
oldChartThemeResource = DarkChartResource;
newChartThemeResource = LightChartResource;
}
else if (string.CompareOrdinal(DarkThemeText, themeName) == 0)
{
oldChartThemeResource = LightChartResource;
newChartThemeResource = DarkChartResource;
}
else
{
throw new ValueUnavailableException
("Theme resource not found for graph.");
}
if (oldChartThemeResource != null)
{
var md = resources.MergedDictionaries.FirstOrDefault
(d => d.Source == oldChartThemeResource.Source);
if(null!=md)
{
resources.MergedDictionaries.Add(newChartThemeResource);
var chartThemeChanged = resources.MergedDictionaries.Remove(md);
if(!chartThemeChanged)
{
throw new Exception("Theme for chart could not be changed");
}
}
}
}
#endregion
}
I have been a Winforms programmer before. When I was introduced to WPF, I was amazed by the immense power it put in the hands of the UI developer. No matter how high your imagination runs as a UI developer, WPF lets you attain that. I believe, and I believe most of you would, that writing custom controls in WPF is far easier than in Winforms. The very new things that were introduced with WPF viz: DependencyProperties
, ContentTemplate
, DataTemplate
let you do virtually anything you would want to do to show the data your way. There is no more need to delve into the complex GDI drawing code. To put in my two cents: a serious WPF developer, though not a pro in writing custom controls, should be able to write simple custom controls if he wants to have the independence to have wild imagination (no naughty meaning intended!).
When it comes to writing custom controls, it is always a rewarding decision to put it in an entirely new library (until and unless it is very specific to your application and contains its logic). Expense uses a number of custom control, but I put the simplest of them: NavigationButtonControl
in the custom controls library. This is done to highlight the idea of WPF control libraries as reusable components.
How It Happened?
I needed a nice template for my buttons that would navigate the user to my Linkedin and google+ profile page. I took it as an overambitious opportunity to write a custom control. While some of you may suggest a better way of doing, I start by choosing the base class for my custom control. This is how I start:
- Choose an existing control that is as close in functionality to your control as possible. In my case, it is button.
- Using a disassembler (or even better, the object browser in Visual Studio), have a look at the base types it implements.
- Figure out the base class that gives you the least common functionalities you want.
I was very clear about the functionalities I wanted in my custom control:
- It would let me have a content.
- It would let me override its template
- It would be intelligent enough to understand when it is mouse pressed. (Why? Will come to that later.)
- It would give me a Command dependency property to which I could bind an
ICommand
property of my view model.
I did not want:
- Any exposed click event. (I wanted to force the user to use command binding. MVVM haters would hate me for this!)
- Any properties like
IsDefault
, IsCancelled
, etc.
Here is what the object browser shows me:
It is definitely not difficult to guess that I had to write a replacement of ButtonBase
class. The code for NavigationButtonControl
is given below:
public class NavigationButtonControl : ContentControl, ICommandSource
{
#region Dependency Properties.
public static readonly DependencyProperty CommandProperty =
DependencyProperty.Register("Command", typeof(ICommand),
typeof(NavigationButtonControl), new PropertyMetadata(null, CommandChanged));
public static readonly DependencyProperty CommandParameterProperty =
DependencyProperty.Register("CommandParameter", typeof(object),
typeof(NavigationButtonControl), new PropertyMetadata(null));
public static readonly DependencyProperty IsPressedProperty =
DependencyProperty.Register("IsPressed", typeof(bool),
typeof(NavigationButtonControl), new PropertyMetadata(false));
#endregion
#region Public properties
public bool IsPressed
{
get { return (bool)GetValue(IsPressedProperty); }
set { SetValue(IsPressedProperty, value); }
}
#endregion
#region ICommandSource Members
public ICommand Command
{
get
{
return (ICommand)GetValue(CommandProperty);
}
set
{
SetValue(CommandProperty, value);
}
}
public object CommandParameter
{
get
{
return GetValue(CommandParameterProperty);
}
set
{
SetValue(CommandParameterProperty, value);
}
}
public IInputElement CommandTarget
{
get { return this; }
}
#endregion
#region Private Static Event handlers
private static void CommandChanged
(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var control = d as NavigationButtonControl;
if (null != control)
{
control.HookUpCommand((ICommand)e.OldValue, (ICommand)e.NewValue);
}
}
#endregion
#region Private Methods
private void HookUpCommand(ICommand oldCommand, ICommand newCommand)
{
if (oldCommand != null)
{
RemoveCommand(oldCommand);
}
AddCommand(newCommand);
}
private void RemoveCommand(ICommand oldCommand)
{
EventHandler handler = CanExecuteChanged;
oldCommand.CanExecuteChanged -= handler;
}
private void AddCommand(ICommand newCommand)
{
var handler = new EventHandler(CanExecuteChanged);
var canExecuteChangedHandler = handler;
if (newCommand != null)
{
newCommand.CanExecuteChanged += canExecuteChangedHandler;
}
}
private void CanExecuteChanged(object sender, EventArgs e)
{
if (Command == null)
return;
var command = Command as RoutedCommand;
IsEnabled = command != null ? command.CanExecute
(CommandParameter, CommandTarget) : Command.CanExecute(CommandParameter);
}
private void FireAtWill()
{
if (Command == null)
return;
var command = Command as RoutedCommand;
if (command != null)
{
command.Execute(CommandParameter, CommandTarget);
}
else
{
Command.Execute(CommandParameter);
}
}
#endregion
#region Overriden Methods
protected override void OnMouseDown(MouseButtonEventArgs e)
{
base.OnMouseDown(e);
IsPressed = true;
FireAtWill();
}
protected override void OnMouseLeave(MouseEventArgs e)
{
base.OnMouseLeave(e);
IsPressed = false;
}
#endregion
#region Static Constructor
static NavigationButtonControl()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(NavigationButtonControl),
new FrameworkPropertyMetadata
(typeof(NavigationButtonControl)));
}
#endregion
}
The implementation of the interface ICommandSource
and the property IsPressed
is all it provides. Now while your control is ready, what about its look? If you leave it just like that, it will have the look of its base class which is the lookless ContentControl
. I want to give the consumer of my control a default look for my control which he may override. This can be achieved in three simple steps:
- Tell the custom control library where is the resource file containing default style for the control is located in the project by placing this in the Assembly.cs of the custom control project.
[assembly: ThemeInfo(
ResourceDictionaryLocation.None, // where theme specific resource
// dictionaries are located
// (used if a resource is not found in the page,
// or application resource dictionaries)
ResourceDictionaryLocation.SourceAssembly //where the generic
// resource dictionary is located
// (used if a resource is not found in the page,
// app, or any theme specific resource dictionaries)
)]
The comments present with the code explains it all. You are telling WPF where to look for the resources for the control if it is not found anywhere in the application that is consuming it, including the App.xaml. This path is fixed at your project\Themes\Generic.xaml.
- Tell the control that you want it to take up the default style. You override the
DefaultStyleKey
dependency property in the static
constructor of the control.
DefaultStyleKeyProperty.OverrideMetadata(typeof (NavigationButtonControl),
new FrameworkPropertyMetadata(typeof (NavigationButtonControl)));
- And now, supply the default style of the control in the \Themes\Generic.xaml file:
<Style TargetType="{x:Type ControlsLib:NavigationButtonControl}">
<Setter Property="MaxWidth" Value="50"/>
<Setter Property="MaxHeight" Value="23"/>
<setter property="Padding" value="2">
<setter property="Margin" value="3">
<setter property="SnapsToDevicePixels" value="True">
<setter property="Template">
<setter.value>
<controltemplate targettype=
"{x:Type ControlsLib:NavigationButtonControl}">
<grid x:name="backgroundGrid"
rendertransformorigin="0.5,0.5"
background="{DynamicResource
{x:Static SystemColors.ControlBrush}}">
<contentpresenter snapstodevicepixels=
"{TemplateBinding SnapsToDevicePixels}">
</contentpresenter></grid>
<controltemplate.triggers>
<trigger property="IsPressed" value="True">
<setter targetname="backgroundGrid"
property="Background"
value="{DynamicResource
{x:Static SystemColors.ControlDarkBrush}}">
</setter></trigger>
<trigger property="IsMouseOver" value="True">
<setter targetname="backgroundGrid"
property="RenderTransform">
<setter.value>
<scaletransform scalex="1.1" scaley="1.1">
<setter targetname="backgroundGrid"
property="Background"
value="{DynamicResource
{x:Static SystemColors.InactiveBorderBrush}}">
</setter>
Since most of the code for this control is taken from this article by darrellp, I am not going into the working of the control. However, packaging of the work in a custom control has been done in XPence. Image cropper control provides a readonly
DependencyProperty
to give the cropped image that the user produces by adjusting the cropper rectangle. The control also supports drag drop of JPEG images on it (additionally, picking an image using an open file dialog is also provided).
It was during the writing of this control that I came across the WPF bug that prevents binding a readonly
Dependency Property to a ViewModel
property using the OnWayToSource
binding mode. PushBinding was used to overcome the problem.
The image cropper is considerate enough to give a constant image size of 75 by 75 irrespective what image you drop into it. This is because the output image is going into the db and there has to be a limitation on the size.
The RenderTargetBitmap
that is the output of the Image cropper control is converted to byte[]
using ImageToBinaryConverter
. And this byte array is actually saved in the db. Here is the code for ImageToBinaryConverter
:
public class ImageToBinaryConverter : IValueConverter
{
#region Implementation of IValueConverter
public object Convert(object value, Type targetType,
object parameter, CultureInfo culture)
{
var bytes = value as byte[];
if (bytes != null && bytes.Length > 0)
{
var stream = new MemoryStream(bytes);
var image = new BitmapImage();
image.BeginInit();
image.StreamSource = stream;
image.EndInit();
return image;
}
return null;
}
public object ConvertBack(object value, Type targetType,
object parameter, CultureInfo culture)
{
var renderTargetBitmap = value as RenderTargetBitmap;
if (null != renderTargetBitmap)
{
var bitmapImage = new BitmapImage();
var bitmapEncoder = new BmpBitmapEncoder();
bitmapEncoder.Frames.Add(BitmapFrame.Create(renderTargetBitmap));
using (var stream = new MemoryStream())
{
bitmapEncoder.Save(stream);
stream.Seek(0, SeekOrigin.Begin);
bitmapImage.BeginInit();
bitmapImage.CacheOption = BitmapCacheOption.OnLoad;
bitmapImage.StreamSource = stream;
bitmapImage.EndInit();
}
var encoder = new JpegBitmapEncoder();
encoder.Frames.Add(BitmapFrame.Create(bitmapImage));
byte[] data = null;
using (var ms = new MemoryStream())
{
encoder.Save(ms);
data = ms.ToArray();
}
return data;
}
return null;
}
#endregion
}
I am sure there is a better way of doing this. I was too hard pressed for time to delve in the imaging details of WPF. If someone suggests a better code for ConvertBack
, I will be more than happy to update the code with an acknowledgement.
Here is a screenshot of the control in action:
Many times, I have come across the question: Does MVVM mean no code behind? The straight forward answer is: No. The View is a component in a MVVM application and I don't think there is any harm if the code in code behind is strictly keeping its scope within view, e.g., there are two instances within XPence where you will find code behind:
- Windows within messaging services
UserEmblemView
In the former, the messaging service is getting injected inside any ViewModel
against an interface and hence the ViewModel
in no way, is aware of any view specific code or logic going inside the messaging service. The consumer ViewModel
s only deal with string
or enum
s. Which in no way hampers their test ability.
In the latter, the user control is used at two instances: first as an emblem in the top right corner of the application view and second as a DataTemplate
in the ListBox
's ItemTemplate
showing users. There is a slight change in the control's appearance in the two instances (visibility of a path). The logic to hide a user component for one case and show for another is entirely scoped within the View
component. Therefore, it is perfectly fine to write code behind in this case without breaking the MVVM guidelines. The code behind can be easily avoided by repeating the .xaml of the control in the ItemTemplate
of the ListBox
. But isn't that spoiling the re usability? I will call that over engineering!
Procrastination is a lazy man's way of keeping up with yesterday! And therefore, I have made sure that XPence never becomes yesterday for me! There are several points that may stop XPence from becoming a "near-perfect" application. Some of them are attributed to my laziness and the remainder to my "hard pressed for time" schedule. While I am initiating the list with a few points, I am sure you will add to the list:
- Bad data models
The data models in XPence is pretty straight forward: A model to carry transaction data and another one to carry the user date. While a transaction model should have a reference of the associated user model, it just has a string
property called ModifiedUser
.
- Redundant dependency property in ModalCustomMessageDialog
I wanted to override the control template of the Mahapps metro's MetroWindow
that is serving as ModalCustomMessageDialog
. The right way would be to override the content template of the window. But I found it easier to create another dependency property called ActualContent
, thus bypassing the task of delving into the template of MetroWindow
.
- NavigationButtonControl is inaccurate
I created this control in a separate plain WPF application that was not using Mahapps metro and it worked perfect. But when I used it in XPence, it stopped working. I realized any control that is placed in Mahapps MetroWindow has some mouse events getting compromised. This is because Mahapps does serious Mouse event manipulation to make the MetroWindow the MetroWindow. I took the shortcut of firing a command on MouseDown
, unlike a real button that fires command on mouse up. The accurate code still lies dead at the bottom of the code file.
I am sorry but I believe honest confession is a better policy than pleading "not guilty" when actually I am "guilty"!
Well, that's all I have to share. I am most eager to hear your comments, suggestions, remarks. Even with all my laziness, it has taken considerable effort to bring this up! I am not a seasoned writer like most cool guys out here, yet I have tried to give my best to explain the key ideas that XPence communicates. If you are voting below 5, please at least mention the reason. I would also like to ask, if you liked the article, please vote for it, Also, a comment would be nice as it would let me know if the article offers what people want to know. That would be my take-away from the article.
If you feel certain sections need more explanation, feel free to leave a comment and I will be happy to update the article with an acknowledgement. However, please consider the fact that since there are a lot of concepts that have been touched, every point can't have a very detailed explanation without the length of article becoming insanely large.
- Version 1.0 (23 April 2014): Initial draft
- Version 1.1 (1 May 2014): Namely the following updates:
- Fixes for bugs reported by LOKImotive. v.i.z.:
- The bug was that the amount entered for a transaction would not be reflected in the grid even after saving.
- A better message if the amount field is left empty
- Another bug fix that prevented the header text for the selected transaction from changing when a transaction was saved
- Reduced the image sizes in the article as suggested by DaveAuld
- Added accent color support to the pie chart
- Changed content