Graeme Hill's Dev Blog

Specifying expected DataContext type in WPF

Star date: 2009.297

Josh Smith just made a blog post about XAML DataContext comments when using the MVVM pattern. He makes a great point, which is that in many cases it is not immediately obvious what the DataContext of a view is intended to be. A simple comment as Josh suggests will go a long way, but the downside is that comments create a maintainability issue. If you rename a model view, or refactor code so that a page, window or user control expects a different DataContext you also need to update the comment. Here's an example:

<!-- DataContext = SampleModelView -->
<UserControl x:Class="SampleUserControl"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Width="300" Height="300">
    <Grid>

    </Grid>
</UserControl>

But now what if I change my mind and decide that instead of SampleModelView, this UserControl will have AlternateModelView as its DataContext? If I forget to update the comment then it is now a source of misinformation. What I would really like to do is somehow specify the expected data context type for a given UI element, so I created an attached property called ExpectedDataContextType. When the element is loaded, it will fail at runtime if the DataContext is not of the desired type. It looks like this:

<UserControl x:Class="TestControl"
    xmlns:local="clr-namespace:ExpectedDataContextType"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Width="300" Height="300"
    local:DataContextHelper.ExpectedDataContextType="{x:Type local:TestModelView}">
    <Grid>
        <TextBlock>Hello</TextBlock>
    </Grid>
</UserControl>

The code is extremely simple. It just attaches a handler to the Loaded event and then checks the type. When the types do not match up it gives you a warning via a message box if you are debugging. I chose not to throw an exception because the exception gets covered up and just ends up as a tiny message on Visual Studio immediate window. When I am debugging and a control has the wrong DataContext I want to make sure I find out about it, hence the message box. You can always replace the message box code with something else though.

Option Strict On

Public Class DataContextHelper

    Public Shared Function GetExpectedDataContextType(
        ByVal element As DependencyObject) As Type

        If element Is Nothing Then
            Throw New ArgumentNullException("element")
        End If

        Return DirectCast(element.GetValue(ExpectedDataContextTypeProperty), Type)
    End Function

    Public Shared Sub SetExpectedDataContextType(
        ByVal element As DependencyObject, ByVal value As Type)

        If element Is Nothing Then
            Throw New ArgumentNullException("element")
        End If

        element.SetValue(ExpectedDataContextTypeProperty, value)
    End Sub

    Public Shared ReadOnly ExpectedDataContextTypeProperty As _
        DependencyProperty = DependencyProperty.RegisterAttached("ExpectedDataContextType", _
        GetType(Type), GetType(DataContextHelper), _
        New FrameworkPropertyMetadata(Nothing, AddressOf OnExpectedDataContextTypeChanged))

    Private Shared Sub OnExpectedDataContextTypeChanged(
        ByVal obj As DependencyObject, ByVal args As DependencyPropertyChangedEventArgs)

        Dim element = DirectCast(obj, FrameworkElement)
        AddHandler element.Loaded, AddressOf OnElementLoaded
    End Sub

    Private Shared Sub OnElementLoaded(
        ByVal sender As Object, ByVal args As RoutedEventArgs)

        Dim element = DirectCast(sender, FrameworkElement)

        RemoveHandler element.Loaded, AddressOf OnElementLoaded

        ' Compare the expected type to the actual type
        Dim expectedDataContextType = GetExpectedDataContextType(element)
        Dim actualDataContextType = element.DataContext.GetType
        If expectedDataContextType IsNot actualDataContextType Then

#If DEBUG Then
            ' The types don't match and debug mode is on so notify the developer
            ' that the element has the wrong data context
            MessageBox.Show(String.Format("DataContext has type {0}. Expected {1}.", _
                                          actualDataContextType.ToString, _
                                          expectedDataContextType.ToString))
#End If

        End If
    End Sub

End Class

The end result is that you are still specifying what the type of your DataContext should be, but now it is actually enforced. The best part is that if the you rename your model view without updating the ExpectedDataContextType property you will get a compile error because the type no longer exists. If the type still exists then you have to settle for a runtime error.