Binding Entities to Selector Derived Controls

Binding an entity (business object) to a selector derived control such as a ComboBox is quite a common task in WPF.  A quick google would lead you to believe that you either have to implement IEquatable on all your entities so you can bind on SelectedItem, or you have to bind on SelectedValue.  Neither if these approaches quite tick all the boxes for us: we want to write minimal code, bind the entity not just the value, and also have the process automatically deal with active and inactive entities…

Standard Approaches

In a lot of scenarios, having a list of entities and simply performing the binding via the value is fine.  However, other scenarios require that we have more than the value of an entity returned; we require other properties, so we want the entire entity. Clearly SelectedValue is not going to provide this.

So we try to use SelectedItem, but quickly discover that when you set the binding between your source entity and the entities in your list, it will not find a match as it will simply perform an object reference comparison…and your source object and those in the list are likely to differ.  So you determine you need to implement IEquatable on any entities you may wish to bind in this way.

So following the recommendations you should also override the base class implementations of Object.Equals(Object) and GetHashCode.  Hum, not rocket science but somewhat tedious…especially if you follow the consistency argument and decide to implement for every single entity in your model…

Alternate Approach

So what’s an alternative that allows us to bind on the entity and avoid writing lots of tedious code?  Let’s take a look at the end usage of this approach:

Entity Binding
<ComboBox Grid.Row="1" Grid.Column="1"
        ItemsSource="{Binding Path=Topics}"
        SelectedValuePath="TopicId"
        planetControls:SelectorBindingHelper.SelectedEntity="{Binding SelectedPosition.Topic,
            Mode=TwoWay, TargetNullValue={x:Static sys:String.Empty}}"
        DisplayMemberPath="Name"/>

There you go! That’s it. Simple enough? So you can see that the above shows we have a list of Topic entities, and will use TopicId as the value and Name for display purposes…all standard stuff.  The single change here is that rather than using SelectedItem to bind our source entity, we have used our own attached property.

How Does it Work?!

The concept here is very simple: we are simply trying to ensure that the provided source entity is selected in the list of entities.  Here are the rough steps:-

  1. Check to see if SelectedEntity exists as object reference already
  2. If it does not, then try to locate it using the SelectedValuePath property on both the SelectedEntity and ItemsSource and perform an equality check
  3. If we do find it, then we remove the listed entity and insert our SelectedEntity in its place
  4. If we do not find it, we add the SelectedEntity anyway

Then as our SelectedEntity now exists in the list, the standard selection mechanism works as normal.

Active and Inactive Entities

It is quite common to only list active options.  So when you view an existing record that has an inactive option, you normally have to write some extra code to add that inactive option to your list so that it can be displayed and selected accordingly.

As you can see, we accomplish this goal as a side benefit of the alternate approach in Step 4 above.

SelectorBindingHelper
// Copyright 2010 (c) Planet Software Pty Ltd. This work is
// licenced under the Creative Commons Attribution 2.5 Australia License. To view
// a copy of this licence, visit http://creativecommons.org/licenses/by/2.5/au/
using System;
using System.Collections;
using System.ComponentModel;
using System.Diagnostics;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using Planet.Common.Linq;

namespace Planet.Windows.Controls
{
    public static class SelectorBindingHelper
    {
        #region Public DPs    

        private static readonly DependencyProperty IsInitialisedProperty =
           DependencyProperty.RegisterAttached("IsInitialised", typeof(bool),
           typeof(SelectorBindingHelper),
           new UIPropertyMetadata(false));
                        
        private static readonly DependencyProperty SelectedEntityProperty =
            DependencyProperty.RegisterAttached("SelectedEntity", typeof(object),
            typeof(SelectorBindingHelper),
                new PropertyMetadata(
                    (d, e) => SetBindings(d, e, EnsureExistingEntityListed())
                    )
                );

        public static void SetSelectedEntity(DependencyObject dependencyObject,
            string value)
        {
            if (dependencyObject == null)
                { throw new ArgumentNullException("dependencyObject"); }

            dependencyObject.SetValue(SelectedEntityProperty, value);
        }

        public static object GetSelectedEntity(DependencyObject dependencyObject)
        {
            if (dependencyObject == null)
                { throw new ArgumentNullException("dependencyObject"); }

            return dependencyObject.GetValue(SelectedEntityProperty);
        }       
        #endregion

        #region Private Methods
        private static void SetBindings<T>(
                DependencyObject d,
                DependencyPropertyChangedEventArgs e,
                Action<T, object> alter) where T : Selector
        {
            var element = d as T;

            if (element != null)
            {
                // Reset index if needed (due to previous binding use)
                if (element.SelectedIndex != -1
                    && (e.NewValue == null
                        || string.IsNullOrEmpty(e.NewValue as string)))
                    { element.SelectedIndex = -1; }

                if (!(e.NewValue == e.OldValue || e.NewValue == null))
                {
                    if (alter != null) { alter(element, e.NewValue); }  
                }

                if (!(bool)element.GetValue(IsInitialisedProperty))
                {
                    element.SetValue(IsInitialisedProperty, true);
                    element.SelectionChanged += Element_SelectionChanged;
                    DependencyPropertyDescriptor desc = DependencyPropertyDescriptor
                        .FromProperty(Selector.ItemsSourceProperty, typeof(T));

                    desc.AddValueChanged(element, delegate
                    {
                        Element_ItemSourceChanged(element, alter);
                    });
                }
            }
        }

        private static void Element_ItemSourceChanged<T>(T element,
            Action<T, object> alter) where T : Selector
        {
            var currentValue = element.GetValue(SelectedEntityProperty);
            alter(element, currentValue);
        }

        private static void Element_SelectionChanged(object sender,
            SelectionChangedEventArgs e)
        {
            var element = sender as Selector;
            var currentValue = element.GetValue(SelectedEntityProperty);

            // If making a selection
            if (e.AddedItems.Count > 0 && element.SelectedIndex > -1)
            {
                // Determine any current selection in ui control
                if (element.SelectedIndex > -1)
                {   // And set back on DP target
                    var ob = element.Items.GetItemAt(element.SelectedIndex);
                    if (ob != currentValue)
                    {
                        element.SetValue(SelectedEntityProperty, ob);
                    }
                }
            }
            else
            {
                if (currentValue != null)
                {
                    Action<Selector, object> alter = EnsureExistingEntityListed();
                    alter(element, currentValue);
                }
            }
        }

        // Need to ensure that is e.NewValue is not in list, then try
        // locating by id...and if found remove existing, add new
        // if not found just add anyway
        private static Action<Selector, object> EnsureExistingEntityListed()
        {
            return (control, newValue) =>
            {
                if (!(newValue is string))
                {
                    // To manipulate we need as list not enumerable
                    IList col = control.ItemsSource as IList;

                    if (col != null)
                    {
                        // 1. Check to see whether newValue exists
                        // as object reference already
                        if (control.HasItems)
                        {
                            if (!control.Items.Contains(newValue))
                            {
                                // We will be adding regardless now...
                                // but if we have a property we can use
                                // to compare try to locate existing
                                // via that first so we can remove
                                int locatedIndex = -1;
                                object newCoreValue = GetPropertyValue(
                                    newValue, control.SelectedValuePath);

                                foreach (var item in col)
                                {
                                    object itemValue = GetPropertyValue(
                                        item, control.SelectedValuePath);

                                    if (itemValue.Equals(newCoreValue))
                                    {
                                        locatedIndex = col.IndexOf(item);
                                        col.Remove(item);
                                        break;
                                    }
                                }

                                if (locatedIndex > -1)
                                {
                                    col.Insert(locatedIndex, newValue);
                                }
                                else
                                {
                                    locatedIndex = col.Add(newValue);
                                }
                            } // Otherwise already exists                       

                            control.SelectedItem = newValue;  // Ensure select
                        }
                        else
                        {
                            // Add new item
                            col.Add(newValue);
                        }
                    }
                    else
                    {
                        Debug.WriteLine("SelectorBindingHelper: IList Required");
                    }
                }
            };
        }

        private static object GetPropertyValue(object item, string propertyName)
        {
            object result = null;

            if (item != null && !string.IsNullOrEmpty(propertyName))
            {
                result = item.GetObjectValue(item.GetType(), propertyName);
            }

            return result;
        }

        #endregion
    }    
}

Note: GetObjectValue is a simple extension method that dynamically gets the property value and is optimised to cache PropertyInfo retrieval.

Print | posted on Sunday, 12 December 2010 12:33 PM

Feedback

# re: Binding Entities to Selector Derived Controls

left by Nasza Klasa at 10/02/2011 4:35 AM Gravatar
Greatly explained, thanks
Title  
Name
Email (never displayed)
Url
Comments   
Please add 4 and 1 and type the answer here: