[WPF] Markup extensions and templates

Very poorPoorAverageGoodExcellent (2 votes) 
Loading...Loading...

Note : This post follows the one about a a markup extension that can update its target, and reuses the same code.

You may have noticed that using a custom markup extension in a template sometimes lead to unexpected results… In this post I’ll explain what the problem is, and how to create a markup extensions that behaves correctly in a template.

The problem

Let’s take the example from the previous post : a markup extension which gives the state of network connectivity, and updates its target when the network is connected or disconnected :

<CheckBox IsChecked="{my:NetworkAvailable}" Content="Network is available" />

Now let’s put the same CheckBox in a ControlTemplate :

<ControlTemplate x:Key="test">
  <CheckBox IsChecked="{my:NetworkAvailable}" Content="Network is available" />
</ControlTemplate>

And let’s create a control which uses this template :

<Control Template="{StaticResource test}" />

If we disconnect from the network, we notice that the CheckBox is not automatically updated by the NetworkAvailableExtension, whereas it was working fine when we used it outside the template…

Explanation and solution

The markup expression is evaluated when it is encountered by the XAML parser : in that case, when the template is parsed. But at this time, the CheckBox control is not created yet, so the ProvideValue method can’t access it… When a markup extension is evaluated inside a template, the TargetObject is actually an instance of System.Windows.SharedDp, an internal WPF class.

For the markup extension to be able to access its target, it has to be evaluated when the template is applied : we need to defer its evaluation until this time. It’s actually pretty simple, we just need to return the markup extension itself from ProvideValue : this way, it will be evaluated again when the actual target control is created.

To check if the extension is evaluated for the template or for a “real” control, we just need to test whether the type of the TargetObject is System.Windows.SharedDp. So the code of the ProvideValue method becomes :

        public sealed override object ProvideValue(IServiceProvider serviceProvider)
        {
            IProvideValueTarget target = serviceProvider.GetService(typeof(IProvideValueTarget)) as IProvideValueTarget;
            if (target != null)
            {
                if (target.TargetObject.GetType().FullName == "System.Windows.SharedDp")
                    return this;
                _targetObject = target.TargetObject;
                _targetProperty = target.TargetProperty;
            }

            return ProvideValueInternal(serviceProvider);
        }

Cool, it’s now fixed, the CheckBox is updated when the network connectivity changes :)

Last, but not least

OK, we have a solution that apparently works fine, but let’s not count our chickens before they’re hatched… What if we now want to use our ControlTemplate on several controls ?

<Control Template="{StaticResource test}" />
<Control Template="{StaticResource test}" />

Now let’s run the application and unplug the network cable : the second CheckBox is updated, but the first one is not…

The reason for this is simple : there are two CheckBox controls, but only one instance of NetworkAvailableExtension, shared between all instances of the template. Now, NetworkAvailableExtension can only reference one target object, so only the last one for which ProvideValue has been called is kept…

So we need to keep track of not one target object, but a collection of target objects, which will all be update by the UpdateValue method. Here’s the final code of the UpdatableMarkupExtension base class :

    public abstract class UpdatableMarkupExtension : MarkupExtension
    {
        private List<object> _targetObjects = new List<object>();
        private object _targetProperty;

        protected IEnumerable<object> TargetObjects
        {
            get { return _targetObjects; }
        }

        protected object TargetProperty
        {
            get { return _targetProperty; }
        }

        public sealed override object ProvideValue(IServiceProvider serviceProvider)
        {
            // Retrieve target information
            IProvideValueTarget target = serviceProvider.GetService(typeof(IProvideValueTarget)) as IProvideValueTarget;

            if (target != null && target.TargetObject != null)
            {
                // In a template the TargetObject is a SharedDp (internal WPF class)
                // In that case, the markup extension itself is returned to be re-evaluated later
                if (target.TargetObject.GetType().FullName == "System.Windows.SharedDp")
                    return this;

                // Save target information for later updates
                _targetObjects.Add(target.TargetObject);
                _targetProperty = target.TargetProperty;
            }

            // Delegate the work to the derived class
            return ProvideValueInternal(serviceProvider);
        }

        protected virtual void UpdateValue(object value)
        {
            if (_targetObjects.Count > 0)
            {
                // Update the target property of each target object
                foreach (var target in _targetObjects)
                {
                    if (_targetProperty is DependencyProperty)
                    {
                        DependencyObject obj = target as DependencyObject;
                        DependencyProperty prop = _targetProperty as DependencyProperty;

                        Action updateAction = () => obj.SetValue(prop, value);

                        // Check whether the target object can be accessed from the
                        // current thread, and use Dispatcher.Invoke if it can't

                        if (obj.CheckAccess())
                            updateAction();
                        else
                            obj.Dispatcher.Invoke(updateAction);
                    }
                    else // _targetProperty is PropertyInfo
                    {
                        PropertyInfo prop = _targetProperty as PropertyInfo;
                        prop.SetValue(target, value, null);
                    }
                }
            }
        }

        protected abstract object ProvideValueInternal(IServiceProvider serviceProvider);
    }

The UpdatableMarkupExtension is now fully functional… until proved otherwise ;). This class makes a good starting point for any markup extension that needs to update its target, without having to worry about the low-level aspects of tracking and updating target objects.

17 Comments

  1. Peter says:

    Nice idea, I had to change the _targetObject and _targetProperty objects to be weak references to avoid leaks.

  2. Ben says:

    I noticed that in the UpdateValue() method, the objects from _targetObjects are required to be DependencyObjects, because if they aren”t, then “DependencyObject obj = target as DependencyObject;” will produce null and cause null reference exceptions later on. Why not simply make _targetObjects (and it”s associated property) a List of DependencyObject instead of object?

    • Actually, they”re required to be DependencyObjects only if the target property is a DependencyProperty. Otherwise, the target property is a PropertyInfo, and the target objects can be of any type.

  3. mouradK says:

    hi

    may i ask you for a code to test this

    i experienced the mentioned problem with datatemplates and your solution doesn”t seem to work in my case

    Thanks in advance

    • Hi,

      What kind of example ? Is there something you don”t understand in the article”s example ? I”ll be glad to explain if I can.

      What exactly doesn”t work for you ? What did you do ?

      Regards,
      Thomas

  4. mourad says:

    hi, here is my problem


    public sealed override object ProvideValue(IServiceProvider serviceProvider) {
    // Retrieve target information
    IProvideValueTarget target = serviceProvider.GetService(typeof(IProvideValueTarget)) as IProvideValueTarget;

    if(target != null && target.TargetObject != null) {

    // In a template the TargetObject is a SharedDp (internal WPF class)
    // In that case, the markup extension itself is returned to be re-evaluated later
    if (target.TargetObject.GetType().FullName == "System.Windows.SharedDp")
    return this;

    // Save target information for later updates
    _targetObjects.Add(target.TargetObject as DependencyObject);
    _targetProperty = target.TargetProperty as DependencyProperty;

    }
    return BindDictionary(serviceProvider);
    }

    private object BindDictionary(IServiceProvider serviceProvider) {
    foreach(var target in _targetObjects) {
    string uid = _uid ?? GetUid(target);
    string vid = TargetProperty.Name;

    Binding binding = new Binding("Dictionary");
    binding.Source = LanguageContext.Instance;
    binding.Mode = BindingMode.TwoWay;

    _shortcuts = _shortcuts ?? ShortcutBehavior.GetShortcuts(target);

    LanguageConverter converter = new LanguageConverter(uid, vid, _default, _shortcuts);

    if(_parameters.Count == 0) {
    binding.Converter = converter;
    object value = binding.ProvideValue(serviceProvider);
    return value;
    } else {
    MultiBinding multiBinding = new MultiBinding();
    multiBinding.Mode = BindingMode.TwoWay;
    multiBinding.Converter = converter;
    multiBinding.Bindings.Add(binding);
    if(string.IsNullOrEmpty(uid)) {
    Binding uidBinding = _parameters[0] as Binding;
    //if(uidBinding == null) {
    // throw new ArgumentException("Uid Binding parameter must be the first, and of type Binding");
    //}
    }
    foreach(Binding parameter in _parameters) {
    multiBinding.Bindings.Add(parameter);
    }
    object value = multiBinding.ProvideValue(serviceProvider);
    return value;
    }
    }
    return null;
    }

    my problem is the following :
    In all buttons, i need to concatenate the tooltip text and the shortcut associated to a button via a behavior. The problem is when the button is generated by datatemplate, the behavior property is not assigning the shortcut as the button is not yet loaded(rendered). So i tried your solution to defer the refresh of this property by waiting til the button gets loaded BUT…..i can”t get it working coz i dont know when exactly to call the updatevalue thing

    hope that i was clear enough

    • Hi,

      I”m not sure I understand what you”re doing here… If you”re using a Binding to provide the value, perhaps you should subscribe to the event”s TargetUpdated value, and call UpdateValue when this event occurs

  5. mourad says:

    hi again,

    I tried with half success your solution, i can get what i expected buuut there”s still one problem : the first item is not refreshed whereas all the following are. Is this only one shared instance problem that you”re talking about the source of my problem ?

    Thanks

    • I”m not sure… which .NET version are you using ? in 4.0, it seems that the behavior described in the article has changed, and the markup extension instance isn”t shared anymore.

  6. mourad says:

    ok then, i”ll give 4.0 a try

  7. mourad says:

    i”m on 3.5

  8. Brad says:

    Hi Thomas,

    Is it possible to get a reference to the object I am attempting to style via a template in the ProvideValueInternal method (rather than the specific target of the MarkupExtension)? Perhaps by using a different IServiceProvider call?

    Cheers & thanks for a great post.

    • Hi Brad, what are you trying to do exactly? Perhaps you could do that using a binding with RelativeSource = TemplatedParent? I don”t think there is a direct way to do it using the IServiceProvider…

      • Brad says:

        I”m not sure if you have used Prism (and what I”m about to say required some knowledge in that area), but I”m basically trying to register a Prism Region to a ScopedRegionManager from inside a template. I am using your UpdatableMarkupExtension as a base class for a custom MarkupExtension that I want to return the scoped region manager for the control I would like to style.

        Hope this makes sense!

  9. lonli says:

    I”m strying to use your solution with styles, but there is an exception – target.TargetObject is Setter, not DependencyObject. Can you assist?

Leave a comment

css.php