ElementName Binding Inside Silverlight DataGrid

If you ever used ComboBox inside a templated columns inside DataGrid in Silverlight, you probably encountered the following issue. 

If you use ElementName to bind ItemsSource for a ComboBox in this situation, you will find that ElementName cannot be resolved.  Typically, you have to put your items source inside Resources collection on your use control, and use binding with StaticResource.  This is quite a pain, especially if you are using MVVM pattern, such as Prism.  This problems has been a sore spot for me for a while, but I did not have time to come up with a good solution.

Today I gave it some thought, and it hit me.  I should use a behavior to solve the problem.  The approach I took is manually look for the bound element one the ComboBox is part of visual tree.  I can do this inside Loaded event.  Here is how I got started:

    public class ResolveElementName : TargetedTriggerAction<FrameworkElement>

    {

        protected override void OnAttached()

        {

            base.OnAttached();

            Target.Loaded += Target_Loaded;

        }

 

        protected override void OnDetaching()

        {

            base.OnDetaching();

            Target.Loaded -= Target_Loaded;

        }

 

        private void Target_Loaded(object sender, RoutedEventArgs e)

        {

I also added a Dependency Property:

        public string PropertyName

        {

            get { return (string)GetValue(PropertyNameProperty); }

            set { SetValue(PropertyNameProperty, value); }

        }

 

        public static readonly DependencyProperty PropertyNameProperty =

            DependencyProperty.Register("PropertyName", typeof(string), typeof(ResolveElementName), new PropertyMetadata(string.Empty));

 

        protected override void Invoke(object parameter)

        {

            // do nothing

        }

 

The next step is to look how I typically setup ComboBox inside data gird.  I would like to keep the code as close to usual setup as possible to minimize the learning of new functionality.  Here is XAML from my test program.  This also shows how behavior is setup.

        <grid:DataGrid

            ItemsSource="{Binding People}"

            x:Name="PoepleBox"

            Grid.Row="3"

            AutoGenerateColumns="False" >

            <grid:DataGrid.Columns>

                <grid:DataGridTemplateColumn>

                    <grid:DataGridTemplateColumn.CellTemplate>

                        <DataTemplate>

                            <TextBlock Text="{Binding Path=Name}" Margin="7"/>

                        </DataTemplate>

                    </grid:DataGridTemplateColumn.CellTemplate>

 

                </grid:DataGridTemplateColumn>

                <grid:DataGridTemplateColumn>

                    <grid:DataGridTemplateColumn.CellTemplate>

                        <DataTemplate>

                            <TextBlock

                                Text="{Binding Path=StatusID}"

                                Margin="7"/>

                        </DataTemplate>

                    </grid:DataGridTemplateColumn.CellTemplate>

 

                </grid:DataGridTemplateColumn>

                <grid:DataGridTemplateColumn>

                    <grid:DataGridTemplateColumn.CellTemplate>

                        <DataTemplate>

                            <ComboBox

                                SelectedValue="{Binding Path=StatusID, Mode=TwoWay}"

                                DisplayMemberPath="StatusName"

                                SelectedValuePath="StatusID"

                                ItemsSource="{Binding ElementName=LayoutRoot, Path=DataContext.Statuses}" 

                                Margin="7"

                                Width="200" >

                                <i:Interaction.Triggers>

                                    <i:EventTrigger>

                                        <behavior:ResolveElementName PropertyName="ItemsSource" />

                                    </i:EventTrigger>

                                </i:Interaction.Triggers>

                            </ComboBox>

                        </DataTemplate>

                    </grid:DataGridTemplateColumn.CellTemplate>

 

                </grid:DataGridTemplateColumn>

            </grid:DataGrid.Columns>

        </grid:DataGrid>

Also, you will need to import name spaces to get this to work as following:

<UserControl x:Class="ResolveElementNameBehaviorDemo.MainPage"

    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"

    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"

    xmlns:local="clr-namespace:ResolveElementNameBehaviorDemo"

    xmlns:grid="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Data"

    xmlns:behavior="clr-namespace:ResolveElementNameBehavior;assembly=ResolveElementNameBehavior"

    xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"  

 

Now, I would like to show you how I am updating ItemsSource binding by replacing element name binding with regular source based binding:

        private void Target_Loaded(object sender, RoutedEventArgs e)

        {

 

            if (Target != null)

            {

                var fields = Target.GetType().GetFields(

                    System.Reflection.BindingFlags.Public |

                    System.Reflection.BindingFlags.FlattenHierarchy |

                    System.Reflection.BindingFlags.Static);

                foreach (var field in fields)

                {

                    if (field.FieldType == typeof(DependencyProperty) &&

                        (field.Name == PropertyName ||

                        field.Name == string.Concat(PropertyName, "Property")))

                    {

                        DependencyProperty dp = field.GetValue(Target) as DependencyProperty;

                        var binding = Target.GetBindingExpression(dp);

                        string elementName = binding.ParentBinding.ElementName;

                        if (!string.IsNullOrEmpty(elementName))

                        {

                            DependencyObject boundToElement = GetElementBasedOnName(Target, elementName);

                            if (boundToElement != null)

                            {

                                Binding newBinding = new Binding();

                                Binding oldBinding = binding.ParentBinding;

                                newBinding.BindsDirectlyToSource = oldBinding.BindsDirectlyToSource;

                                newBinding.Converter = oldBinding.Converter;

                                newBinding.ConverterCulture = oldBinding.ConverterCulture;

                                newBinding.ConverterParameter = oldBinding.ConverterParameter;

                                newBinding.FallbackValue = oldBinding.FallbackValue;

                                newBinding.Mode = oldBinding.Mode;

                                newBinding.NotifyOnValidationError = oldBinding.NotifyOnValidationError;

                                newBinding.Path = oldBinding.Path;

                                newBinding.Source = boundToElement;

                                newBinding.StringFormat = oldBinding.StringFormat;

                                newBinding.TargetNullValue = oldBinding.TargetNullValue;

                                newBinding.UpdateSourceTrigger = oldBinding.UpdateSourceTrigger;

                                newBinding.ValidatesOnDataErrors = oldBinding.ValidatesOnDataErrors;

                                newBinding.ValidatesOnExceptions = oldBinding.ValidatesOnExceptions;

                                newBinding.ValidatesOnNotifyDataErrors = oldBinding.ValidatesOnNotifyDataErrors;

                                if (Target is ComboBox)

                                {

                                    ComboBox combo = Target as ComboBox;

                                    combo.SetBinding(dp, newBinding);

                                    combo.SetBinding(ComboBox.SelectedValueProperty,

                                        combo.GetBindingExpression(ComboBox.SelectedValueProperty).ParentBinding);

                                }

                                else

                                {

                                    Target.SetBinding(dp, newBinding);

                                }

                               

                            }

                        }

                    }

                }

            }

        }

 

Last step, let me show you how to look for an element with a specific name inside visual tree.  I will use a recursive function that will use VisualTreeHelper class:

        private static DependencyObject GetElementBasedOnName(DependencyObject startPoint, string elementName)

        {

            DependencyObject returnValue = null;

            if (startPoint != null)

            {

                DependencyObject parent = VisualTreeHelper.GetParent(startPoint);

                if (parent != null)

                {

                    FrameworkElement fe = parent as FrameworkElement;

                    if (fe != null)

                    {

                        if (fe.Name == elementName)

                        {

                            returnValue = fe;

                        }

                        else

                        {

                            returnValue = GetElementBasedOnName(fe, elementName);

                        }

                    }

                    else

                    {

                        returnValue = GetElementBasedOnName(fe, elementName);

                    }

                }

            }

            return returnValue;

        }

 

This is it.  There as couple of downsides of this approach.  First of all, there is a small flash when combo is bound.  It is hard to notice though.  The other downside is the overhead of looking for a parent.  However, this is not visibly noticeable.  I tested with a data grid with 100 items, and I do not see any hesitation when scrolling.

You can download the entire solution with behavior and test application here.

I also published it to Microsoft Expression Gallery.

Please let me know if you have any questions..