Introduction
This article highlights three common, useful purposes for including code-behind in your view in MVVM applications.
Background
After writing my first article I definitely wasn't feeling the love with an initial whopping two votes of 1-star and 3-stars. After reflecting on the article and why I thought perhaps a contributing factor was my statement about how the view should include relative code-behind. I reviewed some other articles and found out how prevalent the purist mentality of zero code-behind is so I wanted to present some defense for this...
The Purist
The purist mentality is that any and all code-behind should be avoided. The thought is code-behind inhibits testing and muddles logic. Both of these are incorrect if the view code-behind is kept to view logic.
The purist approach can actually cause problems when view logic is misplaced in view models. The view models then become coupled with the view which is one of the first things MVVM is trying to avoid. If a control specific property isn't hooked up to the view model then the view model may not be reusable in other controls.
Case 1 - CollectionViewSources
Despite WPF being around for seven years or so now it still surprises me how many WPF developers don't know the ins and outs of the CollectionViewSource. It's essentially a wrapper around various enumerables that is what WPF actually binds to. It provides support for enumeration, selection, filtering, and sorting.
The major benefit to a CollectionViewSource is the abilter to filter and sort without having to change the physical list. In the case of very large lists or view models that may be expensive to load it's provides significant performance benefit.
Here's an example of how to use it:
namespace TestProject
{
public class WidgetViewModel
{
public string Name { get; set; }
public bool IsActive { get; set; }
}
public class WidgetCollectionViewModel
{
private List<WidgetViewModel> _allWidgets;
public List<WidgetViewModel> AllWidgets
{
get
{
if (_allWidgets == null)
{
_allWidgets = new List<WidgetViewModel>()
{
new WidgetViewModel() { Name = "Active Widget", IsActive = true },
new WidgetViewModel() { Name = "Inactive Widget", IsActive = false }
};
}
return _allWidgets;
}
}
}
}
<Window x:Class="TestProject.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:TestProject"
Title="MainWindow" Height="350" Width="525">
<StackPanel>
<ListBox x:Name="WidgetListBox"
DisplayMemberPath="Name"
ItemsSource="{Binding AllWidgets}"/>
<CheckBox x:Name="IsActiveOnlyCheckBox"
Content="Active Only:"/>
</StackPanel>
</Window>
namespace TestProject
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
var vm = new WidgetCollectionViewModel();
DataContext = vm;
CollectionViewSource.GetDefaultView(vm.AllWidgets).Filter += item =>
{
var widget = item as WidgetViewModel;
if (widget != null)
{
return !(IsActiveOnlyCheckBox.IsChecked ?? false) || widget.IsActive;
}
return true;
};
IsActiveOnlyCheckBox.Click +=
(o, e) => { CollectionViewSource.GetDefaultView(vm.AllWidgets).Refresh(); };
}
}
}
When the filter is first set it is ran, but as you might be able to see there's no hook up to a notification when the filter conditions change. Therefore when a filter condition changes, like the IsActiveCheckBox, you need to call Refresh(). Every time the Refresh() is called it will run the Filter to determine what should be displayed.
If I created a filter property on the view model or modified the list based on UI selection I could be impacting other business processes. The filtering and sorting in this case is a view only responsibility that has absolutely zero business logic involved. It's also a very common strategy to reduce the number of items displayed based on an easily selectable field such as check boxes for recent activity.
If my WidgetViewModel has a save command or any other business logic in no way is the code above impacting the ability to develop automated testing or unit tests for it. Same thing if a user wants to sort the list differently or the program auto-selects particular widgets via user controls - all functionality provided by a CollectionViewSource.
Case 2 - Exporting UI Data
Exporting UI data can take a substantial amout of time. This can be exercised through several avenues such as printing charts, exporting grids to Excel, and saving files. Typically speaking during these operations you want to block access to the control while showing some type of progress bar.
The example below demonstrates the design on how that can be accomplished:
<Window x:Class="TestProject.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:TestProject"
Title="MainWindow" Height="350" Width="525">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<DataGrid x:Name="MyDataGrid">
<DataGrid.Columns>
<DataGridTextColumn Header="Name" />
</DataGrid.Columns>
</DataGrid>
<Rectangle x:Name="WorkingRectangle"
Fill="Gray"
Opacity=".3"
Visibility="Collapsed">
</Rectangle>
<Border x:Name="WorkingMarqueeBorder"
BorderBrush="SteelBlue"
Background="LightBlue"
BorderThickness="5"
CornerRadius="5"
Height="50"
Width="170"
Visibility="Collapsed">
<StackPanel Orientation="Vertical"
HorizontalAlignment="Center"
VerticalAlignment="Center">
<TextBlock Text="Working..."
Foreground="White"
HorizontalAlignment="Center"/>
<ProgressBar IsIndeterminate="True"
Height="20"
Width="150"/>
</StackPanel>
</Border>
</Grid>
<Button x:Name="ExportButton"
Grid.Row="1"
Content="Export"
Click="ExportButton_Click"/>
</Grid>
</Window>
namespace TestProject
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void ExportButton_Click(object sender, RoutedEventArgs e)
{
ExportButton.IsEnabled = false;
StartWorking();
new TaskFactory().StartNew(() =>
{
Thread.Sleep(3000);
Application.Current.Dispatcher.Invoke(() =>
{
StopWorking();
ExportButton.IsEnabled = true;
});
}, TaskCreationOptions.LongRunning);
}
private void StartWorking()
{
WorkingRectangle.Visibility = System.Windows.Visibility.Visible;
WorkingMarqueeBorder.Visibility = System.Windows.Visibility.Visible;
}
private void StopWorking()
{
WorkingRectangle.Visibility = System.Windows.Visibility.Collapsed;
WorkingMarqueeBorder.Visibility = System.Windows.Visibility.Collapsed;
}
}
}
The rectangle serves to add a visual indicator the DataGrid is disabled in addition to intercepting any clicks that could influence the control unpredictably during export.
It is viable to hook up controls through properties that indicate work (i.e. IsLoading) to view models when calling business logic operations like saving to a database. In this case though the work is particular to view with view only information such as groupings, column ordering, etc. so it should only be done in the view.
If the grid export code wasn't in the view where in the world would it belong? How would you transfer that information? Not to mention that numerous third party controls contain all the code to do that already so you'd be reinventing the wheel!
Case 3 - User Settings
For advanced applications customizable parts to the UI are often saved as user settings. While the management of the user setting is view model and business logic responsibilty, the setting data itself is particular to view elements such as layout, placement, and general data presentation.
Grid controls are also good to discuss with this functionality because grids are complex (groupings, sorting, column ordering, column spacing, etc.) and layout serialization is often supported out of the box with third party controls. You also have docking locations, default folder paths, window sizes, and more. Point is out of all of these examples nothing is determined by business logic.
Inevitably the data for the setting must be extracted from the UI itself and not all of it will be available via bindings. In many instances the sole way to extract data is by calling methods on view objects. While it's possible to pass around a control through commands for the sole purpose of calling a method outside of it's code behind that's just getting ridiculous...
Summary
The big pitfall with the zero-code behind approach isn't that there is some code that clearly doesn't belong in the business logic, but it puts you in a mentally to shove everything into the view model logic regardless of scope. Outside of the distinct functionality of code-behind you also have commands, converters, and template selectors which serve distinct and very useful purposes. If you ignore these tools and their intended usages it's going to potentially make development a lot harder than it needs to be - both now and in the long run.
Bonus Section: Local Time Converter
It isn't uncommon for view models to convert back and forth from UTC to local time all over the place, but all business logic should be exclusively in UTC. Using this standard reduces confusion, simplifies code, and is less error prone.
Whenever you need to display a DateTime you simply hook up to a local time converter class. Here is one below:
public class LocalTimeConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
var utcTime = value as DateTime?;
if (utcTime != null)
{
return DateTime.SpecifyKind(utcTime.Value, DateTimeKind.Utc).ToLocalTime();
}
return value;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
var localTime = value as DateTime?;
if (localTime != null)
{
return DateTime.SpecifyKind(localTime.Value, DateTimeKind.Local).ToUniversalTime();
}
return value;
}
}
If the time zone is a user preference that may not correspond to the local system time it can also be incorporated here. The conversion makes the assumption the right kind is being passed, but you can of course change the behavior to do validation if desired.
In IValueConverter implementations ConvertBack is commonly not implemented; however, it's very important in this case to do so because you're going to want to be able to select DateTimes from various controls to set view model properties. To do so you'll need to convert back to UTC.
I wanted to mention this particular case because it's a simple example that illustrates even though you can put code into the view model there are much easier ways by leveraging the appropriate tools. Easier does not mean short cut either. The converter is a single responsibility object that uniformly converts time - you can't do it better.