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.. 

23 Comments

  1. i get an error if in runtime once it try to update ,i placed it in a cellEditTemplate instead of just a celltemplate .. i haven’t found the reason my self but it’s something to do with the trigger action i guess ..
    can u help ?

  2. Hi, thanks for sharing this as it was just what I was looking for!

    I do have the same issues as commenters above though where it throws an exception when the combobox is in a CellEditingTemplate and the editing is completed. Any idea how to fix this is highly appreciated

  3. In case of edit template, the problem was apparently caused by resetting the binding for SelectedItem. Just comment out the following:
    combo.SetBinding(ComboBox.SelectedItemProperty, combo.GetBindingExpression(ComboBox.SelectedItemProperty).ParentBinding);

  4. I can’t thank you enough for this behavior. It worked fantastic for me. I was beating my head against a wall trying to get ElementName binding to work within a DataForm EditTemplate.

    Let’s hope this ElementName sillyness gets fixed for good within Silverlight 5.

  5. Pingback: Steve Konves » Bind to DataContext property from within a DataGrid

  6. There is no limit that I know of, although I never tried that many rows. You probably want to put some trace statements to find out what is going on. In any case, it is usually not a good idea to put that much data in the UI, as performance will likely not going to be good.

  7. Doesn’t update on property changed.
    I have a property on the VM that exposes a collection, the combobox lazily binds to that property so when the getter is accessed, loads collection from server, sets the backing fields and raises property changed for the collection on load completed.

  8. Great job Sergey, thanks for sharing! I’ve been doing something similar (just not using combobox). But I’m struggling with another issue here, perhaps you could help – I want to change eg. Status on another row, another ‘element’… Do you have a suggestion?

  9. You could, just follow visual tree up to parent, such as grid, then back down to sibling rows. Exact code would depend on the controls you use. If you are trying to enforce business rules though or access data on each row, you should consider doing it at the object level instead of UI in my case.

  10. Ah, sorry, I forgot to mention that other limitation in my project – yes, I suppose I could ‘iterate’ through the source of the grid and change it there, but it means that I would have to reload the whole grid every time some value within is changed (since the change is not propagated to UI itself) and in some cases the amount of data could be huge. Not mentioning that, in that case, I have to handle some ‘side’ effects of user actions such as scrolling the huge grid, so I have to scroll back to the position where he was, after reloading and such things…

    Thank you for reply, I’ll keep searching for the best approach here and inform about progress…

  11. …did some more analysis around this and finally turned back to object level as your second suggestion was. No luck doing it on UI since I additionally had an issue with hidden columns which was also one of the constraints I had to tackle with. Tnx once again!

Leave a Reply to Shimmy Cancel reply

Your email address will not be published. Required fields are marked *