Click here to Skip to main content
15,881,812 members
Articles / Desktop Programming / WPF

Three cases for MVVM view code-behind

Rate me:
Please Sign up or sign in to vote.
1.95/5 (7 votes)
1 Sep 2015CPOL6 min read 36.3K   3   39
Three cases that establish why you need to consider code-behind for views in MVVM.

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:

C#
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;
            }
        }
    }
}

 

XML
<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>

 

C#
namespace TestProject
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    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:

XML
<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>

 

C#
namespace TestProject
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        private void ExportButton_Click(object sender, RoutedEventArgs e)
        {
            ExportButton.IsEnabled = false;
            StartWorking();

            new TaskFactory().StartNew(() =>
            {
                // Normally this would be a process such as exporting the grid data 
                // (potentially thousands of rows) or saving a chart as an image, but 
                // for simplicity this is just sleeping to simulate work.
                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:

C#
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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Architect
United States United States
More than fourteen years of experience programming with six years of WPF application development. Dedicated business application developer with large focus on performance tuning for multithreaded applications. Experience developing for small scale applications and large scale distributed systems at senior and architect levels. Experience developing for a wide range of industry applications to include artificial lift, baggage tracking, distribution, customer relation management, and commodities.

Comments and Discussions

 
Questionmy vote of 5 Pin
Mr.PoorEnglish16-Sep-15 4:16
Mr.PoorEnglish16-Sep-15 4:16 
AnswerRe: my vote of 5 Pin
PureNsanity17-Sep-15 6:11
professionalPureNsanity17-Sep-15 6:11 
GeneralI partially agree and disagree with everyone. Pin
rthorat3-Sep-15 8:36
rthorat3-Sep-15 8:36 
GeneralRe: I partially agree and disagree with everyone. Pin
PureNsanity3-Sep-15 8:51
professionalPureNsanity3-Sep-15 8:51 
I fully agree with all points. I may have come across the wrong way in the examples, but the intention is to "consider" code-behind instead of just out right dismissing it as a blanket policy.

Regarding the LocalDateTimeConverter, you make a valid point for your environment. Ironically though my main reasons for including it is the same you're against it... If your ViewModel is doing the conversions it may need to call the code numerous places. It's not like a ViewModel is limited to one DateTime to display either. If you accidentally miss converting it can be hard to catch too... Additionally it's a cross-cutting concern so numerous ViewModels will have to implement the code to convert.
GeneralRe: I partially agree and disagree with everyone. Pin
Pete O'Hanlon3-Sep-15 12:04
mvePete O'Hanlon3-Sep-15 12:04 
GeneralRe: I partially agree and disagree with everyone. Pin
PureNsanity3-Sep-15 12:17
professionalPureNsanity3-Sep-15 12:17 
QuestionI am a "purist"... Pin
SledgeHammer012-Sep-15 15:40
SledgeHammer012-Sep-15 15:40 
AnswerRe: I am a "purist"... Pin
PureNsanity3-Sep-15 4:24
professionalPureNsanity3-Sep-15 4:24 
GeneralRe: I am a "purist"... Pin
SledgeHammer013-Sep-15 4:59
SledgeHammer013-Sep-15 4:59 
GeneralRe: I am a "purist"... Pin
PureNsanity3-Sep-15 5:28
professionalPureNsanity3-Sep-15 5:28 
GeneralRe: I am a "purist"... Pin
SledgeHammer013-Sep-15 5:41
SledgeHammer013-Sep-15 5:41 
GeneralRe: I am a "purist"... Pin
PureNsanity3-Sep-15 5:57
professionalPureNsanity3-Sep-15 5:57 
GeneralRe: I am a "purist"... Pin
SledgeHammer013-Sep-15 7:46
SledgeHammer013-Sep-15 7:46 
GeneralRe: I am a "purist"... Pin
PureNsanity3-Sep-15 8:22
professionalPureNsanity3-Sep-15 8:22 
GeneralRe: I am a "purist"... Pin
SledgeHammer013-Sep-15 11:22
SledgeHammer013-Sep-15 11:22 
GeneralRe: I am a "purist"... Pin
PureNsanity3-Sep-15 11:34
professionalPureNsanity3-Sep-15 11:34 
GeneralRe: I am a "purist"... Pin
SledgeHammer013-Sep-15 12:00
SledgeHammer013-Sep-15 12:00 
GeneralRe: I am a "purist"... Pin
PureNsanity3-Sep-15 12:24
professionalPureNsanity3-Sep-15 12:24 
GeneralRe: I am a "purist"... Pin
SledgeHammer013-Sep-15 13:41
SledgeHammer013-Sep-15 13:41 
GeneralRe: I am a "purist"... Pin
PureNsanity3-Sep-15 14:02
professionalPureNsanity3-Sep-15 14:02 
GeneralRe: I am a "purist"... Pin
SledgeHammer013-Sep-15 14:31
SledgeHammer013-Sep-15 14:31 
GeneralRe: I am a "purist"... Pin
PureNsanity3-Sep-15 16:58
professionalPureNsanity3-Sep-15 16:58 
GeneralRe: I am a "purist"... Pin
SledgeHammer013-Sep-15 17:21
SledgeHammer013-Sep-15 17:21 
GeneralRe: I am a "purist"... Pin
PureNsanity3-Sep-15 17:24
professionalPureNsanity3-Sep-15 17:24 
GeneralRe: I am a "purist"... Pin
SledgeHammer013-Sep-15 18:02
SledgeHammer013-Sep-15 18:02 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.