Adding Tactile Feedback to your app the easy way

About a week ago there was a very good article with tips to make your wp7 app a killer app. Tip 24 was about tactile feedback. Tactile feedback can be something like 30 milliseconds running the VibrateController. That’s very easy, just like this:

VibrateController vibrateController = VibrateController.Default;
vibrateController.Start(new TimeSpan(0, 0, 0, 0, 30));

Because I just want to call one method instead of two lines of code, I created a small static method.

public static void GiveTactileFeedback()
{
    VibrateController vibrateController = VibrateController.Default;
    vibrateController.Start(new TimeSpan(0, 0, 0, 0, 30));
}

Alright, but now I had to add this method call to every Button Click, ListBox SelectionChanged, and ApplicationBar menu or button use. Probably there are some more places where I want to give a little bit of feedback. So I’m thinking about a solution, a generic solution. I remember how easy it was to apply the TiltEffect. So my mind was going the direction of a similar solution. So I started with some Attached Dependency Properties, the same mechanism that’s used to implement the TiltEffect. So enable it for example in the root of your Phone Page and only suppress the effect if you don’t want to apply it a special occasion.

<phone:PhoneApplicationPage ... 
                            WPUI:TactileFeedbackEffect.IsTactileFeedbackEnabled="true">

And suppress it in this way.

<Button Content="Feedback surpressed"
        WPUI:TactileFeedbackEffect.SuppressTactileFeedback="true" />

Basically I want to walk through all the children of the element that have the IsTactileFeedbackEnabled property set to true. So I started walking recursively through the Visual Tree.

public static IEnumerable<FrameworkElement> GetSelfAndChildren(this FrameworkElement node)
{
    var elements = new List<FrameworkElement>();
    int childrenCount = VisualTreeHelper.GetChildrenCount(node);
    elements.Add(node);
    for (int i = 0; i < childrenCount; i++)
    {
        var childFe = VisualTreeHelper.GetChild(node, i) as FrameworkElement;
        if (childFe != null)
        {
            elements.AddRange(GetSelfAndChildren(childFe));
        }
    }
    return elements;
}

Using this Visual Tree of elements to check against controls that should support the Tactile Feedback: ButtonBase (root of all buttons), Selector (root of the different types of ListBoxes), PhoneApplicationPage (only way to get a hold of the  ApplicationBar’s buttons and menu items). For the menu items and buttons, it’s just simply attaching to the Click event. The Selector has a SelectionChanged event to which we can listen. Not that much of rocket science.

The initial idea I had was to make use of the Loaded event of the Element that has  the DependencyProperty applied to start walking though the visual tree. But what happened when the Visual Tree changes? Caused for example by Data Binding changes, or any of the other ways that enable you to manipulate the Visual Tree. So I attached to the LayoutUpdated event as well, I had to hack a bit, because the sender in the LayoutUpdated event is always null.

In het end I came to the conclusion that I didn’t have a way to suppress feedback on the ApplicationBar items.This is mainly because the ApplicationBar items are not inheriting from DependencyObject.

Full code is below for both the VisualTreeHelper and the TactileFeedbackEffect. Please let me know if you have any questions or improvements.

/// <summary>
/// Simple helpers for walking the visual tree
/// </summary>
internal static class TreeHelpers
{
    public static IEnumerable<FrameworkElement> GetSelfAndChildren(this FrameworkElement node)
    {
        var elements = new List<FrameworkElement>();
        int childrenCount = VisualTreeHelper.GetChildrenCount(node);
        elements.Add(node);
        for (int i = 0; i < childrenCount; i++)
        {
            var childFe = VisualTreeHelper.GetChild(node, i) as FrameworkElement;
            if (childFe != null)
            {
                elements.AddRange(GetSelfAndChildren(childFe));
            }
        }
        return elements;
    }
}

public class TactileFeedbackEffect : DependencyObject
{
    /// <summary>
    /// Set this Attached Dependency Property to true to enable tactile feedback on an element and it's children.
    /// </summary>
    public static readonly DependencyProperty IsTactileFeedbackEnabledProperty = DependencyProperty.RegisterAttached
        (
            "IsTactileFeedbackEnabled",
            typeof (bool),
            typeof (TactileFeedbackEffect),
            new PropertyMetadata(OnIsTactileFeedbackEnabledChanged)
        );

    /// <summary>
    /// Set this Attached Dependency Property to true to suppress tactile feedback on the element.
    /// </summary>
    public static readonly DependencyProperty SuppressTactileFeedbackProperty = DependencyProperty.RegisterAttached(
        "SuppressTactileFeedback",
        typeof (bool),
        typeof (TactileFeedbackEffect),
        new PropertyMetadata(false));

    static TactileFeedbackEffect()
    {
        TactileFeedbackItems = new List<Type>
                                    {typeof (ButtonBase), typeof (PhoneApplicationPage), typeof (Selector)};
    }

    private TactileFeedbackEffect()
    {
    }

    public static List<Type> TactileFeedbackItems { get; private set; }

    public static bool GetIsTactileFeedbackEnabled(DependencyObject source)
    {
        return (bool) source.GetValue(IsTactileFeedbackEnabledProperty);
    }


    public static void SetIsTactileFeedbackEnabled(DependencyObject source, bool value)
    {
        source.SetValue(IsTactileFeedbackEnabledProperty, value);
    }

    public static bool GetSuppressTactileFeedback(DependencyObject source)
    {
        return (bool) source.GetValue(SuppressTactileFeedbackProperty);
    }


    public static void SetSuppressTactileFeedback(DependencyObject source, bool value)
    {
        source.SetValue(SuppressTactileFeedbackProperty, value);
    }

    private static void OnIsTactileFeedbackEnabledChanged(DependencyObject target,
                                                            DependencyPropertyChangedEventArgs args)
    {
        if (target is FrameworkElement)
        {
            if ((bool) args.NewValue)
            {
                (target as FrameworkElement).Loaded += (s, e) =>
                                                            {
                                                                var senderDo = s as DependencyObject;
                                                                TryAttachFeedback(senderDo);
                                                                if (senderDo is FrameworkElement)
                                                                    (senderDo as FrameworkElement).LayoutUpdated +=
                                                                        (s2, e2) =>
                                                                            {
                                                                                TryDetachFeedback(senderDo);
                                                                                TryAttachFeedback(senderDo);
                                                                            };
                                                            };
            }
            else
            {
                TryDetachFeedback(target);
            }
        }
    }

    private static void TryAttachFeedback(DependencyObject target)
    {
        foreach (FrameworkElement element in (target as FrameworkElement).GetSelfAndChildren())
        {
            foreach (Type t in TactileFeedbackItems)
            {
                if (t.IsAssignableFrom(element.GetType()))
                {
                    if ((bool) element.GetValue(SuppressTactileFeedbackProperty) != true)
                    {
                        if (element is ButtonBase)
                        {
                            (element as ButtonBase).Click += TactileFeedbackEffectClick;
                        }
                        if (element is PhoneApplicationPage)
                        {
                            var page = element as PhoneApplicationPage;
                            if (page.ApplicationBar != null)
                            {
                                foreach (IApplicationBarMenuItem button in
                                    page.ApplicationBar.Buttons ?? new List<IApplicationBarMenuItem>())
                                {
                                    button.Click += TactileFeedbackEffectApplicationBarClick;
                                }
                                foreach (IApplicationBarMenuItem menuItem in
                                    page.ApplicationBar.MenuItems ?? new List<IApplicationBarMenuItem>())
                                {
                                    menuItem.Click += TactileFeedbackEffectApplicationBarClick;
                                }
                            }
                        }
                        if (element is Selector)
                        {
                            (element as Selector).SelectionChanged += TactileFeedbackEffectSelectionChanged;
                        }
                    }
                }
            }
        }
    }

    private static void TryDetachFeedback(DependencyObject target)
    {
        foreach (FrameworkElement element in (target as FrameworkElement).GetSelfAndChildren())
        {
            foreach (Type t in TactileFeedbackItems)
            {
                if (t.IsAssignableFrom(element.GetType()))
                {
                    if ((bool) element.GetValue(SuppressTactileFeedbackProperty) != true)
                    {
                        if (element is ButtonBase)
                        {
                            (element as ButtonBase).Click -= TactileFeedbackEffectClick;
                        }
                        if (element is PhoneApplicationPage)
                        {
                            var page = element as PhoneApplicationPage;
                            if (page.ApplicationBar != null)
                            {
                                foreach (IApplicationBarMenuItem button in
                                    page.ApplicationBar.Buttons ?? new List<IApplicationBarMenuItem>())
                                {
                                    button.Click -= TactileFeedbackEffectApplicationBarClick;
                                }
                                foreach (IApplicationBarMenuItem menuItem in
                                    page.ApplicationBar.MenuItems ?? new List<IApplicationBarMenuItem>())
                                {
                                    menuItem.Click -= TactileFeedbackEffectApplicationBarClick;
                                }
                            }
                        }
                        if (element is Selector)
                        {
                            (element as Selector).SelectionChanged -= TactileFeedbackEffectSelectionChanged;
                        }
                    }
                }
            }
        }
    }

    private static void TactileFeedbackEffectSelectionChanged(object sender, SelectionChangedEventArgs e)
    {
        GiveTactileFeedback();
    }

    private static void TactileFeedbackEffectApplicationBarClick(object sender, EventArgs e)
    {
        GiveTactileFeedback();
    }

    private static void TactileFeedbackEffectClick(object sender, RoutedEventArgs e)
    {
        GiveTactileFeedback();
    }

    public static void GiveTactileFeedback()
    {
        VibrateController vibrateController = VibrateController.Default;
        vibrateController.Start(new TimeSpan(0, 0, 0, 0, 30));
    }
}

  • Gravatar soldes longchamp 2014 May 5th, 2014 at 03:41
    Betsey Johnson embrayage d'or
  • Gravatar sac cartable longchamp May 5th, 2014 at 03:41
    Ryan Moore semble inopposable pour 锚tre honn锚te. Il prend tellement de man猫ges ext茅rieurs solides pour compl茅ter l'茅quipe de Sir Michael Stoute et il est tout simplement le meilleur jockey de sa g茅n茅ration. Il sera Moore gloire pour Ryan.
  • Gravatar site officiel jordan June 30th, 2014 at 07:04
    Different shoes might make it easier to run with the proper form. It's worth ask asking, you might not need order new shoes.
  • Gravatar talon jordan June 30th, 2014 at 07:05
    Holiday Inn, Salon A
  • Gravatar replicaoakleys.tomantool.us July 13th, 2014 at 16:46
    What's up to all, how is the whole thing, I think every one is getting more from this web site, and your views are good designed for new viewers.
  • Gravatar www.czemar.eu July 13th, 2014 at 16:46
    I am sure this post has touched all the internet people, its really really fastidious paragraph on building up new blog.
  • Gravatar charitel.us July 13th, 2014 at 16:46
    For latest news you have to pay a quick visit internet and on internet I found this web site as a best web page for most recent updates.
  • Gravatar musclesmusic.us July 13th, 2014 at 16:47
    It awesome in support of me to have a website, which is helpful for my experience. thanks admin
  • Gravatar mbt shoes online July 22nd, 2014 at 10:00
    mbt shoes clearance zappos Adding Tactile Feedback to your app the easy way - Silverlight, WP7, .NET, C#, ASP.NET MVC
Gravatar