The blog of dlaa.me

Setting a value to null might be more dangerous than you think [Simple ToolTipServiceExtensions class avoids a runtime exception in Windows Store apps]

I was playing around with a Windows Store app this weekend and ran into a pretty annoying problem. At first, I couldn't tell what was going on; it seemed the app would crash at random times for no apparent reason. But after a bit of debugging to isolate the problem, I figured out what was going on.

Problem: If you have a ToolTipService.ToolTip data binding in a Windows Store app and the value of that binding transitions from non-null to null while it's being displayed, the next time the tooltip is shown, the platform will throw a NullReferenceException from native code and terminate the application.

This is a surprisingly severe consequence for something that's likely to happen with some regularity, so you'd expect someone to have run into it before now. And indeed, someone did: tkrasinger reported this problem in November of last year. It's unclear where things went from there, but I've verified the problem still occurs with fully-patched Windows 8 and also with the Windows 8.1 preview released last week.

At first glance, the situation seems pretty dire because there's no clear way to intercept the exception. And while changing the code to use an empty string instead of null does avoid the crash, it also results in an ugly little white square when you hover. Fortunately, if you know a bit about how tooltips work, you know there's a ToolTip class that gets injected in this scenario to host the bound content. What if we intercepted the binding and made sure to always provide a non-null ToolTip instance? Would that avoid the crash?

Spoiler alert: It does. :)

 

There are various ways you might go about implementing this workaround - I chose to use a custom attached property because the result looks the same in XAML and neatly encapsulates all the code in one simple, standalone class.

Let's say you were using a tooltip like so:

<Border ToolTipService.ToolTip="{Binding BindingThatCanBecomeNull}">
    <TextBlock Text="Watch out, ToolTipService might crash your app..."/>
</Border>

As we've established, if that binding goes null while the user is mouse-ing around, the app is likely to crash soon afterward.

 

So let's use my ToolTipServiceExtensions class to avoid the problem! First, download ToolTipServiceExtensions.cs from the link below and add it to your Windows Store app project. Next, add the corresponding namespace declaration to the top the XAML:

xmlns:delay="using:Delay"

And lastly, tweak the XAML to use ToolTipServiceExtensions instead of ToolTipService:

<Border delay:ToolTipServiceExtensions.ToolTip="{Binding BindingThatCanBecomeNull}">
    <TextBlock Text="ToolTipServiceExtensions saves the day!"/>
</Border>

That's it - you're done! Random crashes from null-going tooltips should be a thing of the past. :)

 

[Click here to open ToolTipServiceExtensions.cs or right-click/save-as to download it to your machine]

 

Aside: If you're using any of the other ToolTipService properties in your code, they are unaffected by this change. All ToolTipServiceExtensions does is wrap the content in a ToolTip before deferring to the existing ToolTipService implementation.

 

For the curious, here's what the code looks like:

namespace Delay
{
    /// <summary>
    /// Class containing a replacement for ToolTipService.SetToolTip that works
    /// around a Windows 8 platform bug where NullReferenceException is thrown
    /// from native code the next time a ToolTip is displayed if its Binding
    /// transitions from non-null to null while on screen.
    /// </summary>
    public static class ToolTipServiceExtensions
    {
        /// <summary>
        /// Gets the value of the ToolTipServiceExtensions.ToolTip XAML attached property for an object.
        /// </summary>
        /// <param name="obj">The object from which the property value is read.</param>
        /// <returns>The object's tooltip content.</returns>
        [SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "0", Justification = "Underlying method will validate.")]
        public static object GetToolTip(DependencyObject obj)
        {
            return (object)obj.GetValue(ToolTipProperty);
        }

        /// <summary>
        /// Sets the value of the ToolTipServiceExtensions.ToolTip XAML attached property.
        /// </summary>
        /// <param name="obj">The object to set tooltip content on.</param>
        /// <param name="value">The value to set for tooltip content.</param>
        [SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "0", Justification = "Underlying method will validate.")]
        public static void SetToolTip(DependencyObject obj, object value)
        {
            obj.SetValue(ToolTipProperty, value);
        }

        /// <summary>
        /// Gets or sets the object or string content of an element's ToolTip.
        /// </summary>
        [SuppressMessage("Microsoft.Security", "CA2104:DoNotDeclareReadOnlyMutableReferenceTypes", Justification = "Stardard attached property implementation.")]
        public static readonly DependencyProperty ToolTipProperty =
            DependencyProperty.RegisterAttached(
                "ToolTip",
                typeof(object),
                typeof(ToolTipServiceExtensions),
                new PropertyMetadata(null, ToolTipPropertyChangedCallback));

        /// <summary>
        /// Method called when the value of an element's ToolTipServiceExtensions.ToolTip XAML attached property changes.
        /// </summary>
        /// <param name="element">Element for which the property changed.</param>
        /// <param name="args">Event arguments.</param>
        private static void ToolTipPropertyChangedCallback(DependencyObject element, DependencyPropertyChangedEventArgs args)
        {
            // Capture the new value
            var newValue = args.NewValue;

            // Create a ToolTip instance to display the new value
            var toolTip = new ToolTip { Content = newValue };

            // Hide the ToolTip instance if the new value is null
            // (Prevents the display of a small white rectangle)
            if (null == newValue)
            {
                toolTip.Visibility = Visibility.Collapsed;
            }

            // Defer to ToolTipService.SetToolTip for the actual implementation
            ToolTipService.SetToolTip(element, toolTip);
        }
    }
}