Home > .NET General, Silverlight > Custom Markup Extension with bindable properties

Custom Markup Extension with bindable properties

Download Source Code

Microsoft added custom markup extensions to SL5, but they did not provide built-in support for binding to the properties of CME’s. If you’re using CME’s to manipulate your UI, trying to make the UI react to changes in your View Model after the UI has loaded will not work.

When the UI loads, the runtime finds your CME’s and calls the ProvideValue method of your CME to get the value that should be applied to the UI element. The problem is that there’s no way to force the runtime to call ProvideValue again when something has changed in your View Model, so the UI will not react to changes in your VM.

Below is an example of how you could use a CME (called ConvertBooleanToVisibility) to toggle the visibility of a UI element based on the state of two properties in your View Model. Doing this without using a CME would require you to create a third “aggregate” property in your View Model to combine the values of the two main properties, bind to that aggregated property and use a ValueConverter to convert from boolean to Visibility.

<Rectangle Visibility="{local:ConvertBooleanToVisibility Value1={Binding Path=IsVisible}, Value2={Binding Path=IsVisible2}}"/>

As you can see, I’ve used a CME with two properties, Value1 and Value2, that binds to two properties in the View Model. When these values change, the Visibility property of the Rectangle is changed.

The trick to make this work is the following:

  • Instead of inheriting from MarkupExtension, you need to inherit from DependencyObject and implement the IMarkupExtension<object> interface
  • Add dependency properties to the CME, so that you can use bindings. The DP’s must have a property changed callback.
  • In ProvideValue, get a reference to the UI element and property that the CME is applied to. You’ll need these references to manually update the UI element when your View Model property changes. You’ll also need to subscribe to the DataContextChanged event for the UI element so you’ll know when a View Model has been attached. This is necessary because at the time the runtime calls ProvideValue the first time, the View Model may not yet be attached, so you need to modify the binding to point to the correct source once you have a View Model in scope.
  • When the View Model is attached, the DataContextChanged event will be raised. This will allow you to get the binding expressions for your dependency properties. To get the binding expressions for your properties instead of the values, use the ReadLocalValue method instead of GetValue(…). Once you have the binding expressions, copy the binding and remap the binding source so that it points to the attached View Model. Then apply the new bindings to the dependency properties of the CME.
  • Once the bindings are in place, this will make the property changed callbacks to get called when the dependency properties are changed. In the callbacks, call a method that calculates the composite value of the the two DPs and use the UI element and property reference obtained in ProvideValue (step 2) to update the UI element.

Below is a complete listing of the implementation of this Custom Markup Extension.

using System;
using System.Reflection;
using System.Windows;
using System.Windows.Data;
using System.Windows.Markup;
using System.Xaml;

namespace CMEWithBinding
{
    public class ConvertBooleanToVisibilityExtension : DependencyObject, IMarkupExtension<object>
    {
        private object _target;
        private object _targetProperty; // Can be PropertyInfo or Binding

        #region Value1
                
        public static readonly DependencyProperty Value1Property =
            DependencyProperty.Register("Value1", typeof(bool), typeof(ConvertBooleanToVisibilityExtension),
                new PropertyMetadata((bool)false,
                    new PropertyChangedCallback(OnValue1Changed)));
        

        public bool Value1
        {
            get { return (bool)GetValue(Value1Property); }
            set { SetValue(Value1Property, value); }
        }
                
        private static void OnValue1Changed(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            ((ConvertBooleanToVisibilityExtension)d).OnValue1Changed(e);
        }

        protected virtual void OnValue1Changed(DependencyPropertyChangedEventArgs e)
        {            
            UpdateTarget();
        }

        #endregion

        #region Value2

        /// <summary>
        /// Value2 Dependency Property
        /// </summary>
        public static readonly DependencyProperty Value2Property =
            DependencyProperty.Register("Value2", typeof(bool), typeof(ConvertBooleanToVisibilityExtension),
                new PropertyMetadata((bool)false,
                    new PropertyChangedCallback(OnValue2Changed)));

        /// <summary>
        /// Gets or sets the Value2 property.  This dependency property 
        /// indicates ....
        /// </summary>
        public bool Value2
        {
            get { return (bool)GetValue(Value2Property); }
            set { SetValue(Value2Property, value); }
        }

        /// <summary>
        /// Handles changes to the Value2 property.
        /// </summary>
        private static void OnValue2Changed(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            ((ConvertBooleanToVisibilityExtension)d).OnValue2Changed(e);
        }

        /// <summary>
        /// Provides derived classes an opportunity to handle changes to the Value2 property.
        /// </summary>
        protected virtual void OnValue2Changed(DependencyPropertyChangedEventArgs e)
        {
            UpdateTarget();
        }

        #endregion



        #region Properties
        
        private bool _value1CompareTo = true;
        public bool Value1CompareTo
        {
            get { return this._value1CompareTo; }
            set { this._value1CompareTo = value; }
        }

        private bool _value2CompareTo = true;

        public bool Value2CompareTo
        {
            get { return this._value2CompareTo; }
            set { this._value2CompareTo = value; }
        }



        #endregion

        public ConvertBooleanToVisibilityExtension()
        {
                        
        }
        
        public object ProvideValue(IServiceProvider serviceProvider)
        {
            IProvideValueTarget ipValueTarget = (IProvideValueTarget)serviceProvider.GetService(typeof(IProvideValueTarget));
                        
            if (this._target == null)
            {
                if (ipValueTarget.TargetObject is FrameworkElement)
                {
                    FrameworkElement target = (FrameworkElement)ipValueTarget.TargetObject;
                    target.DataContextChanged += new DependencyPropertyChangedEventHandler(target_DataContextChanged);
                }

                this._target = ipValueTarget.TargetObject;
                this._targetProperty = ipValueTarget.TargetProperty;
            }

            Visibility vis = CalcCompositeValue();
            return vis;
        }
        

        void target_DataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
        {
            BindingExpression value1BEX = ReadLocalValue(ConvertBooleanToVisibilityExtension.Value1Property) as BindingExpression;
            BindingExpression value2BEX = ReadLocalValue(ConvertBooleanToVisibilityExtension.Value2Property) as BindingExpression;
            
            if (value1BEX != null && BindingSourceNotSet(value1BEX.ParentBinding))
            {
                Binding old = value1BEX.ParentBinding;
                ClearValue(ConvertBooleanToVisibilityExtension.Value1Property);
                Binding newBinding = CopyBinding(ref e, old);
                BindingOperations.SetBinding(this, ConvertBooleanToVisibilityExtension.Value1Property, newBinding);
            }

            if (value2BEX != null && BindingSourceNotSet(value2BEX.ParentBinding))
            {
                Binding old = value2BEX.ParentBinding;
                ClearValue(ConvertBooleanToVisibilityExtension.Value2Property);
                Binding newBinding = CopyBinding(ref e, old);
                BindingOperations.SetBinding(this, ConvertBooleanToVisibilityExtension.Value2Property, newBinding);
            }
        }
        

        private bool BindingSourceNotSet(Binding binding)
        {
            return binding.Source == null && binding.RelativeSource == null;
        }

        private static Binding CopyBinding(ref DependencyPropertyChangedEventArgs e, Binding old)
        {
            Binding b = new Binding();
            b.BindsDirectlyToSource = old.BindsDirectlyToSource;
            b.Converter = old.Converter;
            b.ConverterCulture = old.ConverterCulture;
            b.ConverterParameter = old.ConverterParameter;
            b.FallbackValue = old.FallbackValue;
            b.Mode = old.Mode;
            b.NotifyOnValidationError = old.NotifyOnValidationError;
            b.Path = old.Path;
            if (old.RelativeSource != null)
            {
                b.RelativeSource = old.RelativeSource;
            }

            if (old.ElementName != null)
            {
                b.ElementName = null;
            }
            
            if (old.ElementName == null && b.RelativeSource == null)
            {
                b.Source = e.NewValue;
            }

            b.StringFormat = old.StringFormat;
            b.TargetNullValue = old.TargetNullValue;
            b.UpdateSourceTrigger = old.UpdateSourceTrigger;
            b.ValidatesOnDataErrors = old.ValidatesOnDataErrors;
            b.ValidatesOnExceptions = old.ValidatesOnExceptions;
            b.ValidatesOnNotifyDataErrors = old.ValidatesOnNotifyDataErrors;
            
            return b;
        }
        

        private void UpdateTarget()
        {
            Visibility vis = CalcCompositeValue();
            if (this._target != null && this._targetProperty != null && !(this._target is Binding))
            {
                if (this._targetProperty is PropertyInfo)
                {
                    PropertyInfo pi = (PropertyInfo)this._targetProperty;
                    pi.SetValue(this._target, vis, null);
                }
            }
        }        

        private Visibility CalcCompositeValue()
        {
            bool b = (this.Value1 == this._value1CompareTo && this.Value2 == this._value2CompareTo);
            Visibility vis = b ? Visibility.Visible : Visibility.Collapsed;
            return vis;
        }
    } 

}

 

Download Source Code

Categories: .NET General, Silverlight Tags:
  1. No comments yet.
  1. No trackbacks yet.
You must be logged in to post a comment.