Using VisualTreeHelper to Perform LightSwitch Wizardry

Normally when you want to teach LightSwitch a fancy new trick, the best approach is to use a Silverlight UserControl. Or if you need easy reuse of the new feature, a LightSwitch extension.  In some cases you may even need to write a custom shell.  But that can be a daunting prospect for a LightSwitch newbie.  Furthermore if the developer doesn’t have access to the full Visual Studio, it may not be possible to create a UserControl or LightSwitch extension (see Extensions Made Easy for a solution to this problem in many cases!).

In situations where you’d like to perform a little LightSwitch wizardry without resorting to writing LightSwitch Extensions, a very useful class is VisualTreeHelper.

VisualTreeHelper allows you to navigate the underlying visual tree of your screens and get references to any control, even those you normally can’t get access to easily from LightSwitch.

To make the class easier to use, I often take advantage of the following helper class:

using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Media;

namespace DotNetLore.Xpf.Core.Extensions
{
    public static class VisualTreeHelpers
    {
        public static IEnumerable<T> GetChildrenByType<T>(this DependencyObject element)
          where T : DependencyObject
        {
            for (int i = 0; i < VisualTreeHelper.GetChildrenCount(element); i++)
            {
                var child = VisualTreeHelper.GetChild(element, i);
                if (child != null)
                {
                    T t = child as T;
                    if (t != null)
                        yield return t;
                    foreach (T item in GetChildrenByType<T>(child))
                    {
                        yield return item;
                    }
                }
            }
        }

        public static IEnumerable<T> GetParentsByType<T>(this DependencyObject element) 
          where T : DependencyObject
        {
            var parent = VisualTreeHelper.GetParent(element);
            while (parent != null)
            {
                T t = parent as T;
                if (t != null)
                    yield return t;
                parent = VisualTreeHelper.GetParent(parent);
            }
        }
    }
}

By using a tool like Silverlight Spy, you can analyze the underlying control tree and XAML of your LightSwitch screens and find the controls that you need to access.

As a simple example, suppose that you want to set a larger font size or a different color for the label that is attached to one of your Text Box controls. If you wanted access to the TextBox control, this would be easy…

this.FindControl("MyTextBox").ControlAvailable += (s,e) =>
{
  (e.Control as TextBox).FontSize = 24;
};

But how do we get access to the attached label instead of the TextBox itself? Using the VisualTreeHelper and VisualTreeHelpers classes, we can write something like this in the screen’s Created method:

this.FindControl("CustomerCode").ControlAvailable += (s,e)=>
{
    var textBox = (e.Control as Control);
    var root = System.Windows.Media.VisualTreeHelper.GetRoot(textBox);
    var label = root.GetChildrenByType<TextBlock>()
        .Where(textBlock => textBlock.DataContext == textBox.DataContext)
        .First();
    label.FontSize = 16;
    label.Foreground = new SolidColorBrush(Colors.Red);
    label.Effect = new System.Windows.Media.Effects.DropShadowEffect();
};

You’ll need these usings:

using DotNetLore.Xpf.Core.Extensions;
using System.Windows.Controls;
using System.Windows.Media;

Here I’m getting the DataContext for the TextBox we’re interested in, then enumerating all the screen’s TextBlock controls and finding the one which has the same DataContext, since I know in this particular case that this will be the attached label that I want. There’s no particular need to take this approach here… one could just do root.GetChildrenByType().Where(textBlock => textBlock.Text == “Customer Code:”).First();

For VB, the VisualTreeHelpers module would look like this:

Module VisualTreeHelpers

    <System.Runtime.CompilerServices.Extension> _
    Public Iterator Function GetChildrenByType(Of T As DependencyObject)(element As DependencyObject) As IEnumerable(Of T)
        For i As Integer = 0 To VisualTreeHelper.GetChildrenCount(element) - 1
            Dim child = VisualTreeHelper.GetChild(element, i)
            If child IsNot Nothing Then
                Dim cast As T = TryCast(child, T)
                If cast IsNot Nothing Then
                    Yield cast
                End If
                For Each item As T In GetChildrenByType(Of T)(child)
                    Yield item
                Next
            End If
        Next
    End Function

    <System.Runtime.CompilerServices.Extension> _
    Public Iterator Function GetParentsByType(Of T As DependencyObject)(element As DependencyObject) As IEnumerable(Of T)
        Dim parent = VisualTreeHelper.GetParent(element)
        While parent IsNot Nothing
            Dim cast As T = TryCast(parent, T)
            If cast IsNot Nothing Then
                Yield cast
            End If
            parent = VisualTreeHelper.GetParent(parent)
        End While
    End Function


End Module

And the code for my screen looks like this:

Imports VisualTreeHelpers

Namespace LightSwitchApplication

    Public Class CreateNewCustomer

        Private Sub CustomerCode_ControlAvailable(ByVal sender As Object, ByVal e As ControlAvailableEventArgs)
            Dim textBox = TryCast(e.Control, Control)
            Dim root = VisualTreeHelper.GetRoot(textBox)
            Dim label = root.GetChildrenByType(Of TextBlock)().Where(Function(textBlock) Equals(textBlock.DataContext, textBox.DataContext)).First()
            label.FontSize = 16
            label.Foreground = New SolidColorBrush(Colors.Red)
            label.Effect = New System.Windows.Media.Effects.DropShadowEffect()
        End Sub

        Private Sub CreateNewCustomer_InitializeDataWorkspace(ByVal saveChangesTo As Global.System.Collections.Generic.List(Of Global.Microsoft.LightSwitch.IDataService))
            ' Write your code here.
            Me.CustomerProperty = New Customer()
            AddHandler Me.FindControl("CustomerCode").ControlAvailable, AddressOf CustomerCode_ControlAvailable
        End Sub

        Private Sub CreateNewCustomer_Saved()
            ' Write your code here.
            Me.Close(False)
            Application.Current.ShowDefaultScreen(Me.CustomerProperty)
        End Sub

    End Class

End Namespace