Click here to Skip to main content
15,900,108 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.9K   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 
I think the main argument is sound, though some of the examples are not great. To me, whether something belongs in the View or the ViewModel is not based on purity, but value. Does putting feature X into the ViewModel get me anything for the effort?

I do a lot of mobile development, which I think helps keep you focused on keeping responsibilites clear. I write ViewModels that can be reused cross-platform. The one question I ask when assessing whether something goes into the ViewModel is this: would this make sense with another interface/platform? And by another, I do not mean an entirely different structured UI, I mean a similar UI on another platform, with slightly different concepts and implementation. If the answer to this question is yes, then it goes in the ViewModel, otherwise it goes in the View. Another way I maintain this distinction is by always putting ViewModels in a PCL that targets all potential platforms. This prevents me from putting anything specific to one UI platform in my ViewModels.

Now, for the article examples specifically, this is how I see it...CollectionViewSource is a pretty good example. This is a WPF-specific feature and I would never put it in a ViewModel. But, if sorting and filtering are something I expect to use on multiple platforms, I would roll my own and put it in the ViewModel. Otherwise, if the sorting and filtering is only going to be used on WPF (maybe because other platforms lack support and you do not have the resources to create it), then I would use CollectionViewSource directly in the View. In this case, I gain nothing by putting it in the ViewModel, and I lose portability. I gain nothing because there is no need to test a CollectionViewSource bound to a UI control since those are all part of the framework and Microsoft is testing that. I may or may not care about losing portability. If I do care, then I put it in the View. If I do not care, then you can still put it in the ViewModel. That is a scenario where there really is no right or wrong answer to me.

The Excel export is another good example. As already stated, some UI controls have very rich export capabilities. If you simply want to take advantage of that with little effort, then leaving this functionality in the View is fine. If you want that functionality to be available across platforms and interfaces, then you should put it in the ViewModel. But this will incur a possibly major investment in writing functionality that is already there, so this is an important decision. But writing a custom export in your ViewModel simply because you are a "purist" is madness.

Settings is a bit of a mixed bag. Most likely you will want those in the ViewModel, though I can see some scenarios where some minor settings may not matter (temporary, session-based settings).

Finally, I think the local date/time converter is not a great idea. I would want that to be done in the ViewModel, almost surely. The example makes a big deal out of having the code in one place with the converter, but you can do that with your ViewModel as well. Put a property for "LocalDateTime" on your ViewModel. That property in turn calls a shared utility method to do the conversion. Where the converter breaks down is again in the multi-platform case. Now I put the burden on the user interface writer to convert the dates. If you have interfaces on 4 platforms, you now have 4 converters. And sharing code between them may be harder, unless you have a shared PCL that they all reference.

In talking about these examples, I think it is clear that these are edge cases. You can go round and round about where they belong because there is no right answer. The right answer depends on the context, and even then there may be some contexts where either is appropriate.

In summary: when I decide, I do so based on whether there is any value in putting something in the ViewModel (there usually is). When making this value determination, I consider cross-platform implications.
GeneralRe: I partially agree and disagree with everyone. Pin
PureNsanity3-Sep-15 8:51
professionalPureNsanity3-Sep-15 8:51 
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.