Home > Patterns > The Stateless Sticky ViewModel Pattern

The Stateless Sticky ViewModel Pattern


What do you do when you want some functionality in your application that is too small to justify creating a full ViewModel, or that you want to reuse in different places and applications? You create a Sticky ViewModel. A Sticky ViewModel is a ViewModel that doesn’t replace the DataContext, but attaches itself to it and provides some functionality that closes a gap between what the model can do and what WPF needs.

While I want to introduce Sticky ViewModels in general in a later post, today I had the idea for a special case which is easy to implement and demonstrate: the Stateless Sticky ViewModel.

What is a Stateless Sticky ViewModel?

A Stateless Sticky ViewModel is a ViewModel that provides all its functionality in shared (static in C#) code, and stores all its state in attached properties. This pattern is suitable for cases where you have a simple dependency between a small group of values, all of which could be data bound.

For example, you might have a data model which exposes the name of a person as one property. In the UI, you’d like to edit first name and last name separately.

Implementing the Pattern

The first step in implementing the pattern would be to create a class that provides the code and attached properties. Since the class needs to define attached properties, it has to be based on DependencyObject. Then it would have to provide code in order to determine first name, last name and full name depending on what other information is already known.

Public Class NameViewModel
    Inherits DependencyObject

    Public Shared Function DetermineLastName(ByVal fullname As String) As String
        Dim result As String = fullname
        If result Is Nothing Then
            result = String.Empty
        Else
            result = result.Trim
            Dim pos As Integer = result.LastIndexOf(" "c)
            If pos >= 0 Then
                result = result.Substring(pos).Trim
            Else
                result = String.Empty
            End If
        End If
        Return result

    End Function

    Public Shared Function DetermineFirstName(ByVal fullname As String) As String
        Dim result As String = fullname
        If result Is Nothing Then
            result = String.Empty
        Else
            result = result.Trim
            Dim pos As Integer = result.LastIndexOf(" "c)
            If pos >= 0 Then
                result = fullname.Substring(0, pos).Trim
            End If
        End If
        Return result
    End Function

    Public Shared Function DetermineFullName(ByVal firstname As String, ByVal lastname As String) As String
        If firstname Is Nothing Then
            firstname = String.Empty
        Else
            firstname = firstname.Trim
        End If
        If lastname Is Nothing Then
            lastname = String.Empty
        Else
            lastname = lastname.Trim
        End If
        Dim result As String = String.Empty
        If String.IsNullOrEmpty(firstname) Or String.IsNullOrEmpty(lastname) Then
            result = firstname & lastname
        Else
            result = firstname & " " & lastname
        End If
        Return result
    End Function

 

Notice that all these functions are declared “Shared”. This means that we actually never have to create an instance of our ViewModel.

The next bit would be to declare the attached properties which contain the data and are bound to model and UI. Let’s take the FirstName property as an example:

Public Shared ReadOnly FirstNameProperty As DependencyProperty = _
    DependencyProperty.RegisterAttached("FirstName", GetType(String), GetType(NameViewModel), _
    New FrameworkPropertyMetadata(String.Empty, _
    FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, _
    AddressOf OnFirstNameChanged))
Public Shared Function GetFirstName(ByVal d As DependencyObject) As String
    Return d.GetValue(FirstNameProperty)
End Function
Public Shared Sub SetFirstName(ByVal d As DependencyObject, ByVal value As String)
    d.SetValue(FirstNameProperty, value)
End Sub
Private Shared Sub OnFirstNameChanged(ByVal d As DependencyObject, ByVal e As DependencyPropertyChangedEventArgs)
    If CStr(e.NewValue) <> CStr(e.OldValue) And GetChangeInitiator(d) Is Nothing Then
        SetChangeInitiator(d, FirstNameProperty)
        SetFullName(d, DetermineFullName(e.NewValue, GetLastName(d)))
        SetChangeInitiator(d, Nothing)
    End If
End Sub

 

The first three declarations are the standard declaration of an attached property: shared get and set methods, and the registration of the property with the property system. It contains a flag that causes the property to update any property to which it is bound by default, because this is what is probably supposed to happen if someone binds this property to their data model. But the most important point in this declaration is that it registers the OnFirstNameChanged method to be called every time the value of the property changes.

In the OnFirstNamedChanged method, we need to make the FullName property reflect the new value of the FirstName property. So, we need to call DetermineFullName and set the FullName property to the new value. In order to do that, we need the last name; since NameViewModel itself is stateless, we take this information from another attached property on the same object.

If we change the FullName property, of course its change handler will try to update the FirstName property. In order to avoid endless circular calls, we only set the FullName property if the value of the FirstName has really changed.

Tracking Dependencies Between the ViewModel’s Properties

But what is the meaning of the bit about the ChangeInitiator? Well, what would be the difference if we set “John A.” to the FirstName or to the FullName? In the first case, we’d expect the LastName to remain unchanged, in the second case we’d expect it to be set to “A.”. Since changing the FirstName changes the FullName and changing the FullName in turn changes the LastName, we need a way to determine that the LastName shouldn’t be changed in this special case.

In general, if we have multiple interdependent properties which all could be changed by the user, we need to make sure that the changes are propagated in the right direction. One way which helps in simple cases like this is storing a reference to the property which originated the change. Since we cannot store state in the ViewModel, we need another attached property:

Private Shared ReadOnly ChangeInitiatorProperty As DependencyProperty = DependencyProperty.RegisterAttached("ChangeInitiator", GetType(DependencyProperty), GetType(NameViewModel))
Private Shared Function GetChangeInitiator(ByVal d As DependencyObject) As DependencyProperty
    Return d.GetValue(ChangeInitiatorProperty)
End Function
Private Shared Sub SetChangeInitiator(ByVal d As DependencyObject, ByVal value As DependencyProperty)
    d.SetValue(ChangeInitiatorProperty, value)
End Sub

This property is declared private, since it is intended for internal use, only. Using the ChangeInitiator property, we can make sure that property changes trigger other properties to change only if it is necessary.

Using the NameViewModel

We use the NameViewModel by binding the model’s Name property to the FullName attached property, and the user controls to the other properties.

<Window x:Class="Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation&quot;
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml&quot;
    xmlns:ex="clr-namespace:SharedStickyViewModelExample"
    Title="Shared Sticky ViewModel Example" Height="300" Width="300" x:Name="Window">
    <Grid x:Name="SVMHost" DataContext="{Binding ElementName=Window}"
          ex:NameViewModel.FullName="{Binding Path=MyName}">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="auto"/>
            <ColumnDefinition/>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="auto"/>
            <RowDefinition Height="auto"/>
            <RowDefinition Height="auto"/>
            <RowDefinition/>
        </Grid.RowDefinitions>
        <Label Grid.Row="0" Grid.Column="0">First Name:</Label>
        <TextBox Grid.Row="0" Grid.Column="1"
          Text="{Binding ElementName=SVMHost,
          Path=(ex:NameViewModel.FirstName)}"/>
        <Label Grid.Row="1" Grid.Column="0">Last Name:</Label>
        <TextBox Grid.Row="1" Grid.Column="1"
          Text="{Binding ElementName=SVMHost,
          Path=(ex:NameViewModel.LastName)}"/>
        <Label Grid.Row="2" Grid.Column="0">Full Name:</Label>
        <TextBox Grid.Row="2" Grid.Column="1"
          Text="{Binding ElementName=SVMHost,
          Path=(ex:NameViewModel.FullName)}"/>
        <Label Grid.Row="3" Grid.Column="0">Model Value:</Label>
        <Label Grid.Row="3" Grid.Column="1" Content="{Binding Path=MyName}"/>
    </Grid>
</Window>

 

Notice that since the ViewModel needs an object to which it can attach its properties, we name the Grid that hosts the other controls “SVMHost”, and refer to when binding to the Stateless Sticky ViewModel’s properties.

Conclusion

The approach introduced here has the advantage that it is very lightweight and has no dependencies whatsoever except those imposed by WPF itself. However, because the Stateless Sticky ViewModel is kind of scattered on the XAML elements that make up the view, it is prone to become confusing when the properties become many and their relationships become complicated. If you need a more structured approach, you could try a full Sticky Component or a Mini-ViewModel.

Coming Soon…

Shortly, I want to introduce Sticky Components and the WPFGlue Sticky Component Framework. However, before I do that, I want to get the Navigation Command off my desk, which I promised in one of the first posts…

Advertisements
  1. No comments yet.
  1. December 16, 2009 at 20:16

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: