Summary
This article presents an alternative MVVM implementation that employs a set of proxy control classes to act as a sort of "Platonic Ideal" WPF page for interaction with the view model. The actual WPF pages are registered with the proxy objects, which search them and form the necessary linkages. This technique allows WPF pages to be developed in the standard way (add event handlers and set control properties), then the code-behind is simply cut and pasted into a separate view model class.
Introduction
I've developed a WPF rich client application for CNC machine control. The operator screens need to display quite a lot of process information and must react substantially to changes in machine state. Nothing too exotic - mostly changing labels, colors, visibility and enabled properties - but quite a lot of it. The WPF operator screens follow the standard pattern of a bunch of event handler methods in the code-behind, both control events such as button clicks and "changed" events coming up from the underlying machine state. My main operator screen currently has 2,400 lines of C# in its code-behind!
Much of my work involves knocking out custom operator screens for clients who need to display additional information and/or simply want a unique look for their product's HMI. Obviously, it would be a good idea to switch to the MVVM pattern and move all of that code-behind logic out to a view model.
Here's the thing: Many of my controls would need bindings for foreground and background colors, label text, visibility and enabled properties, and more. My WPF pages would need a lot of bindings - then there are the value converters, data templates, ICommand and CanExecute... yikes! I much prefer the standard pattern of event handler methods that set the properties of my controls...
void MachineMode_Changed(ChangedEventArgs<EMachineMode> e)
{
switch (e.NewValue)
{
case EMachineMode.Auto:
tbMode.Text = "AUTO";
bdrMode.Background = Background;
btnCycleStart.IsEnabled = true;
btnAbort.IsEnabled = true;
tabMonitor.IsSelected = true;
break;
Objectives
I want to be able to develop my WPF pages in the standard way (add event handlers and set control properties). I want to get a page running properly, then I want to simply cut the code-behind and paste it into a separate view model class and have my app operate exactly as before. I'm not done! I also want to be able to create variations on the original WPF page that will also run correctly with this same view model. While I'm at it, I also want to be able to run multiple pages concurrently on a single view model – and I want them to stay synchronized. The only code-behind remaining should implement page-specific functionality such as animations or customer-specific requirements... and I don't want to deal with command and data bindings!
My Solution
Believe it or not, achieving these lofty goals was pretty simple. I've created a set of proxy control classes for the WPF element types that are involved in my code-behind logic (Border
, Button
, TabItem
, TextBlock
, TextBox
, etc). These proxy classes maintain an underlying list of the actual controls, respond to their events, and expose matching properties and events. The roster of supported properties and events need not be extensive - just the ones required by the code-behind (Background
, Foreground
, IsEnabled
, Visibility
, Click
, Text
, TextChanged
, etc).
The set of proxy control objects is created by the view model to match the requirements of the code-behind, and acts as a sort of "Platonic Ideal" WPF page for interaction with the view model logic. The actual WPF pages are then created and registered with the proxy objects - which search them and form the necessary linkages.
Notice how the ProxyButton
class searches registered pages for a Button
control with the matching name. It then adds a Click
handler that forwards the actual button Click
events to the view model…
public class ProxyButton : ProxyFrameworkElement
{
void Register(Page page)
{
object obj = page.FindName(name);
if (obj is Button)
{
elements.Add(new Element(obj, page));
(obj as Button).Click += Click2;
}
}
public event RoutedEventHandler Click;
private void Click2(object sender, RoutedEventArgs e)
{
if (Click != null)
Click(sender, e);
}
}
Notice how the ProxyTextBlock
class forwards new text to all of its TextBlock
elements…
public class ProxyTextBlock : ProxyFrameworkElement
{
public string Text
{
get
{
return (elements.Count > 0) ? (elements[0] as TextBlock).Text : "";
}
set
{
foreach (var element in elements)
(element as TextBlock).Text = value;
}
}
}
The view model creates a proxy object for each WPF element required by the "code-behind" (in quotes because the code has been moved out of the WPF Page
and into the view model).
class ViewModel
{
public ViewModel()
{
btnStart = new ProxyButton("btnStart");
btnStart.Click += btnStart_Click;
btnStop = new ProxyButton("btnStop");
btnStop.Click += btnStop_Click;
tbStatus = new ProxyTextBlock("tbStatus");
}
}
The application creates an instance of the view model and then registers the WPF page or pages with the view model. At registration time, each proxy object uses the WPF FindName()
method to search the page for a control of the matching name and type. There is also the ability to unregister a page before it's closed (to release references so that the page may be garbage collected).
ViewModel viewModel = new ViewModel();
var page = new Page1();
viewModel.Register(page);
About the only fancy feature in the whole scheme is that a Button
may contain one or two TextBlock
labels. Reflection is employed to search the Button
content for the TextBlocks
. If none are found then the Button
content itself is simply set to the new label text.
There is also a proxy brush class that maintains an underlying list of named brush resources for each registered page. This enables you to write...
ProxyBrush brushHighlight = new ProxyBrush("brushHighlight", Brushes.MistyRose);
btnStop.Background = brushHighlight;
One line of code will set the Stop button's background color of each registered page to that page's named brush resource. Note that a default brush can be specified for use if the resource is not found.
WPF Page Requirements
The WPF Page
no longer has responsibility for declarative bindings in the XAML. The sole requirement is that UI elements and brush resources that are intended to interact with the view model must have the proper names. For example, if the page contains a "Stop" button then it must be named "btnStop".
The WPF Page
can still contain code-behind, and it can contain additional controls not used by the view model. In fact, the page could contain a second set of controls for use by another view model (mind=blown). Let's say that I have a CNC machine that could include either a laser or a waterjet cutter. I could create view models for each of the cutters and only instantiate the one that I need.
The Sample App
The sample app hosts two different WPF Pages
that are registered with a single instance of the view model - thus they remain synchronized. Additional pages of each type can be created on the fly - and will also remain synchronized until they are closed. The corresponding controls of both pages have the same names. The right page only differs from the left in its styling and the fact that it does not have a Start button. Missing or extra controls are simply ignored by the view model.
Exercises Left for the Reader
These proxy objects maintain an underlying list of actual controls, respond to their events, and expose identical properties to the view model. There is absolutely nothing about this technique that wouldn't apply to Windows Forms as well. A set of proxy control classes could even be created that works with both WPF and Forms!
My proxy classes are designed to work with WPF Pages
of controls due to the structure of my application. However, they could easily be extended to work with Windows
and User Controls
.
In addition, if you actually use these proxy classes in your app, you will undoubtedly require additional types of controls and additional property and event support. You'll find these classes trivially easy to extend. I will soon apply them to my CNC app. I intend to update this article afterwards if it generates interest from the community - so please leave me your feedback!
Conclusion
Let's recap the advantages of this alternative MVVM technique of proxy control classes:
- Allows development of WPF pages in the standard way (add event handlers and set control properties), then the code-behind is simply cut and pasted into a separate view model class.
- Responsibility for the linkage search is moved to the view model, allowing the WPF pages to be as simple as possible (they only must contain named controls recognizable by the view model).
- Multiple pages may be registered with a single instance of the view model and will stay synchronized.
- Multiple view models may register the same page (the page could contain two sets of named controls - one for each view model).
- Provides separation of the UI from the business logic without bindings, value converters, etc.
- The set of proxy classes may be easily extended and used with any WPF program.
There will be a small performance hit when property setters and events are passed through the proxy objects. However, I suspect that the delay won't be noticeable to the user - even for a complex app with lots of business logic.
I'm pinching myself because I think this solution is really neat - and I’m looking forward to applying it to my CNC app. This technique will enable me to easily split the functionality of a complex page into several simpler pages, or I could include several variations of my main page for different categories of users, or I could load custom versions of the main page from external assemblies.
I wanted to submit this article for peer review. Is there an existing framework that implements this technique? Would this be a good candidate for an open source project? It's often said that there's a fine line between genius and madness. Have I crossed over? Constructive criticism would be welcomed!