Monday, May 23, 2011

VisibleBinding – Markup Extension Allowing to Only Enable Binding When Target Is Visible

I tried to solve a number of performance problems while tuning WPF application I work on currently.

One of things I wanted to do is – make bindings effective only when control that uses them becomes visible, and don’t update binding target when target is invisible.

I end up creating the markup extension allowing me to do so.

Just use markup extension in format “{namespace:VisibleBinding SomeProperty"}” instead of standard “{Binding SomeProperty}” to achieve this.

note:

1) Binding target will be updated as normally when target is visible.

2) Binding target will not be updated when target is invisible.

3) once target becomes visible the binding becomes effective and therefore at this moment target will be updated with effective value from binding right away.

 

Here is the code:

public class VisibleBinding : BindingDecoratorBase
    {
        #region Class level variables

        private FrameworkElement mTargetObject;
        private DependencyProperty mTargetProperty;
        private PropertyChangeNotifier mNotifier;

        #endregion

        #region Constructor

        public VisibleBinding()
            : base()
        {
            Mode = BindingMode.OneWay;
        }

        public VisibleBinding(string path)
            : base(path)
        {
            Mode = BindingMode.OneWay;
        }

        private VisibleBinding(Binding binding, FrameworkElement targetObject, DependencyProperty targetProperty)
            : base(binding)
        {
            Mode = BindingMode.OneWay;

            Init(targetObject, targetProperty);
        }

        #endregion

        #region Methods

        public static void SetVisibleBinding(Binding binding, FrameworkElement targetObject, DependencyProperty targetProperty)
        {
            new VisibleBinding(binding, targetObject, targetProperty);
        }

        private void AttachTargetObject()
        {
            SetVisibleBinding(mTargetObject, this);
        }

        private void DetachTargetObject()
        {
            SetVisibleBinding(mTargetObject, null);
        }

        private void AttachTargetProperty()
        {

        }

        private void DetachTargetProperty()
        {

        }

        private void AttachExpression()
        {

        }

        private void DetachExpression()
        {

        }

        private void mNotifier_ValueChanged(object sender, EventArgs e)
        {
            CheckBindings();
        }

        private void CheckBindings()
        {
            if (mNotifier != null && mNotifier.Value is bool)
            {
                if ((bool)mNotifier.Value)
                {
                    SetBinding();
                }
                else
                {
                    ClearBinding();
                }
            }
        }

        private void ClearBinding()
        {
            if (mTargetObject != null && mTargetProperty != null)
            {
                mTargetObject.SetValue(mTargetProperty, mTargetObject.GetValue(mTargetProperty));
            }
        }

        private void SetBinding()
        {
            if (mTargetObject != null && mTargetProperty != null)
            {
                mTargetObject.SetBinding(mTargetProperty, mBinding);
            }
        }

        private void Init(DependencyObject targetObject, DependencyProperty targetProperty)
        {
            if (targetObject is FrameworkElement)
            {
                var element = targetObject as FrameworkElement;
                if (mTargetObject == null)
                {
                    if (mTargetObject != null)
                    {
                        DetachTargetObject();
                    }
                    mTargetObject = element;
                    if (mTargetObject != null)
                    {
                        AttachTargetObject();
                    }
                    if (mTargetProperty != null)
                    {
                        DetachTargetProperty();
                    }
                    mTargetProperty = targetProperty;
                    if (mTargetProperty != null)
                    {
                        AttachTargetProperty();
                    }
                    if (mNotifier != null)
                    {
                        mNotifier.ValueChanged -= mNotifier_ValueChanged;
                    }
                    mNotifier = new PropertyChangeNotifier(element, "IsVisible");
                    mNotifier.ValueChanged += mNotifier_ValueChanged;
                    CheckBindings();
                }
            }
        }

        #endregion

        #region Overrides

        public override object ProvideValue(IServiceProvider serviceProvider)
        {
            //delegate binding creation etc. to the base class
            var expression = base.ProvideValue(serviceProvider);

            DependencyObject targetObject = null;
            DependencyProperty targetProperty = null;
           
            if (serviceProvider != null)
            {
                TryGetTargetItems(serviceProvider, out targetObject, out targetProperty);
            }
            Init(targetObject, targetProperty);
            return expression;
        }

        #endregion

        #region DependencyProperties

        #region VisibleBinding

        /// <summary>
        /// Gets the value of the VisibleBinding attached property for a specified UIElement.
        /// </summary>
        /// <param name="element">The UIElement from which the property value is read.</param>
        /// <returns>The VisibleBinding property value for the UIElement.</returns>
        public static VisibleBinding GetVisibleBinding(UIElement element)
        {
            if (element == null)
            {
                throw new ArgumentNullException("element");
            }
            return (VisibleBinding)element.GetValue(VisibleBindingProperty);
        }

        /// <summary>
        /// Sets the value of the VisibleBinding attached property to a specified UIElement.
        /// </summary>
        /// <param name="element">The UIElement to which the attached property is written.</param>
        /// <param name="value">The needed VisibleBinding value.</param>
        public static void SetVisibleBinding(UIElement element, VisibleBinding value)
        {
            if (element == null)
            {
                throw new ArgumentNullException("element");
            }
            element.SetValue(VisibleBindingProperty, value);
        }

        /// <summary>
        /// Identifies the VisibleBinding dependency property.
        /// </summary>
        public static readonly DependencyProperty VisibleBindingProperty =
            DependencyProperty.RegisterAttached(
                "VisibleBinding",
                typeof(VisibleBinding),
                typeof(VisibleBinding),
                new PropertyMetadata(null, OnVisibleBindingPropertyChanged));

        /// <summary>
        /// VisibleBindingProperty property changed handler.
        /// </summary>
        /// <param name="d">VisibleBinding that changed its VisibleBinding.</param>
        /// <param name="e">Event arguments.</param>
        private static void OnVisibleBindingPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
        {
            var control = sender as FrameworkElement;
            if (control != null)
            {
            }
        }

        #endregion

        #endregion
    }

To achieve the goal I used BindingDecoratorBase class. Thanks to Philipp Sumi for his work!

To make my class work – just download BindingDecoratorBase from his blog and include it into your project.

 

Another helper class which does nothing but listens to INotifyPropertyChanged PropertyChanged event is below:

public sealed class PropertyChangeNotifier : DependencyObject, IDisposable
    {
        #region Class level variables

        private WeakReference mPropertySource;

        #endregion

        #region Constructors

        public PropertyChangeNotifier(DependencyObject propertySource, string path)
            : this(propertySource, new PropertyPath(path))
        {
        }

        public PropertyChangeNotifier(DependencyObject propertySource, DependencyProperty property)
            : this(propertySource, new PropertyPath(property))
        {
        }

        public PropertyChangeNotifier(DependencyObject propertySource, PropertyPath property)
        {
            if (null == propertySource)
                throw new ArgumentNullException("propertySource");
            if (null == property)
                throw new ArgumentNullException("property");
            this.mPropertySource = new WeakReference(propertySource);
            Binding binding = new Binding();
            binding.Path = property;
            binding.Mode = BindingMode.OneWay;
            binding.Source = propertySource;
            BindingOperations.SetBinding(this, ValueProperty, binding);
        }

        #endregion

        #region PropertySource

        public DependencyObject PropertySource
        {
            get
            {
                try
                {
                    return this.mPropertySource.IsAlive
                    ? this.mPropertySource.Target as DependencyObject
                    : null;
                }
                catch
                {
                    return null;
                }
            }
        }

        #endregion

        #region Value

        /// <summary>
        /// Identifies the <see cref="Value"/> dependency property
        /// </summary>
        public static readonly DependencyProperty ValueProperty = DependencyProperty.Register("Value",
            typeof(object), typeof(PropertyChangeNotifier), new FrameworkPropertyMetadata(null, new PropertyChangedCallback(OnPropertyChanged)));

        private static void OnPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            PropertyChangeNotifier notifier = (PropertyChangeNotifier)d;
            if (null != notifier.ValueChanged)
                notifier.ValueChanged(notifier, EventArgs.Empty);
        }

        /// <summary>
        /// Returns/sets the value of the property
        /// </summary>
        /// <seealso cref="ValueProperty"/>
        [Description("Returns/sets the value of the property")]
        [Category("Behavior")]
        [Bindable(true)]
        public object Value
        {
            get
            {
                return (object)this.GetValue(PropertyChangeNotifier.ValueProperty);
            }
            set
            {
                this.SetValue(PropertyChangeNotifier.ValueProperty, value);
            }
        }

        #endregion

        #region Events

        public event EventHandler ValueChanged;

        #endregion

        #region IDisposable Members

        public void Dispose()
        {
            BindingOperations.ClearBinding(this, ValueProperty);
        }

        #endregion
    }

Happy coding!

Kirill