PhoneHyperlinkButton updated: now supports web, email, text and phone call tasks

3 December 2010

I’ve updated my extended HyperlinkButton control for Windows Phone 7 to now support all the key tasks. This makes it really easy to build up about pages, user profiles, and of course fully supports data binding.

So the core phone tasks work well with this. Same story, you set the Tag to be effectively the Uri. There are a few reasons for this but I admit Tag is the ultimate hack/general property. I’ve also incorporated some earlier feedback from the first version I did; I’m using StartsWith instead of indexing into the Uris.

I’ve also added simple query string support for the tasks, so you can set the basic properties on the messages as well. Note that with the SMS and phone tasks I’ve deviated from the standard RFC 2806 here, so this syntax is specific to this control and supports additional properties that aren’t standard. So the links build on the standard (like you use when building web apps for phones).

Adding the control to your project

Just add the following as a class file in VS or Blend. Then, make sure you setup an xmlns for the namespace, and you use it much like a regular HyperlinkButton at that point, except please set or data bind the Tag property as a string or Uri, instead of NavigateUri.

Example Tags (URIs) & Tasks

Here are some examples and the specific tasks that are used. Remember in the tag that, to make the XAML parser happy, you need to escape the ampersand as “&”.

Phone call

To allow the user the choice of placing a call, use this syntax in the tag. Understand that this won’t actually place the call; the user will be prompted with a allow/don’t allow prompt. It’s up to you to send it a valid phone number though, I’m not error checking this first!

<unofficial:PhoneHyperlinkButton
    Tag="tel:8005551212"
    Content="Call customer support"/>

Optionally you can include a “displayname” property, which will appear to the user along with the phone number that is to be called. Note I’ve encoded the spaces as %20.

<unofficial:PhoneHyperlinkButton
    Tag="tel:8005551212?displayname=Some%20Company%20Support"
    Content="Call customer support"/>

Text message

Just like the “tel” URI syntax, text messaging starts with “sms:” and I’ve added another parameter, body, which will be the initial text message content. The user can add to it, remove that body, or use the back button to cancel the sending of the text.

<unofficial:PhoneHyperlinkButton
    Tag="sms:8005551212"
    Content="Text a friend" />

<unofficial:PhoneHyperlinkButton
    Tag="sms:8005551212?body=Hello"
    Content="Text a friend hello" />

Email composition

Building on the earlier implementation that only supported the “to” address, I’m matching most of the standard mailto: URI expectations by now adding support for body, cc, and subject query string parameters.

<unofficial:PhoneHyperlinkButton
    Tag="mailto:spam@spam.com?subject=Hello%20World&cc=spammer@spam.com&body=Hey%20spam."
    Content="Send an email" />

Default task: Web browser

Anything that doesn’t start with mailto, sms, or tel, will be sent to the web browser.

<unofficial:PhoneHyperlinkButton
    Tag="http://create.msdn.com/"
    Content="Develop Windows Phone Apps" />

Styling note

So the default hyperlink button will show the text as content, underlined, but I actually prefer a slightly different set of styles in my own applications. Here are two I use: “EmptyHyperlinkButton” and “AccentHyperlinkButton”.

These can be used with any hyperlink button, even if they aren’t my PhoneHyperlinkButton, since the control template is simple and shared among all derived types.

EmptyHyperlinkButton

I use this for more complex user interface areas, where I might actually have a stack panel full of a few different things as the content. This is a hyperlink button technically, but hardly looks like it, other than respecting very basic padding/margin values, it has no appearance. I’ve also removed the VSM styles to make it very simple.

I’m also using a content presenter here.

<Style x:Key="EmptyHyperlink" TargetType="HyperlinkButton">
    <Setter Property="Background" Value="Transparent" />
    <Setter Property="Padding" Value="0" />
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="HyperlinkButton">
                <Border Background="{TemplateBinding Background}" Margin="{StaticResource PhoneHorizontalMargin}" Padding="{TemplateBinding Padding}">
                    <ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}" Content="{TemplateBinding Content}" />
                </Border>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

AccentHyperlinkButton

I like this simple style. It gives the text a semi-bold effect, colors them to the user’s personalized accent color.

<Style
    x:Key="AccentHyperlink"
    TargetType="HyperlinkButton">
    <Setter
        Property="Foreground"
        Value="{StaticResource PhoneAccentBrush}" />
    <Setter
        Property="Background"
        Value="Transparent" />
    <Setter
        Property="FontSize"
        Value="{StaticResource PhoneFontSizeMedium}" />
    <Setter
        Property="FontFamily"
        Value="{StaticResource PhoneFontFamilySemiBold}" />
    <Setter
        Property="Padding"
        Value="0" />
    <Setter
        Property="Template">
        <Setter.Value>
            <ControlTemplate
                TargetType="HyperlinkButton">
                <Border
                    Background="{TemplateBinding Background}"
                    Margin="{StaticResource PhoneHorizontalMargin}"
                    Padding="{TemplateBinding Padding}">
                    <VisualStateManager.VisualStateGroups>
                        <VisualStateGroup
                            x:Name="CommonStates">
                            <VisualState
                                x:Name="Normal" />
                            <VisualState
                                x:Name="MouseOver" />
                            <VisualState
                                x:Name="Pressed">
                                <Storyboard>
                                    <DoubleAnimation
                                        Duration="0"
                                        Storyboard.TargetName="TextElement"
                                        Storyboard.TargetProperty="Opacity"
                                        To="0.5" />
                                </Storyboard>
                            </VisualState>
                            <VisualState
                                x:Name="Disabled">
                                <Storyboard>
                                    <ObjectAnimationUsingKeyFrames
                                        Storyboard.TargetName="TextElement"
                                        Storyboard.TargetProperty="Foreground">
                                        <DiscreteObjectKeyFrame
                                            KeyTime="0"
                                            Value="{StaticResource PhoneDisabledBrush}" />
                                    </ObjectAnimationUsingKeyFrames>
                                </Storyboard>
                            </VisualState>
                        </VisualStateGroup>
                    </VisualStateManager.VisualStateGroups>
                    <TextBlock
                        x:Name="TextElement"
                        Text="{TemplateBinding Content}"
                        HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
                        VerticalAlignment="{TemplateBinding VerticalContentAlignment}" />
                </Border>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

PhoneHyperlinkButton.cs

And the goods to drop in your project:

// (c) Copyright Microsoft Corporation.
// This source is subject to the Microsoft Public License (Ms-PL).
// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
// All other rights reserved.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Net;
using System.Windows.Controls;
using Microsoft.Phone.Tasks;

namespace Microsoft.Phone.Controls.Unofficial
{
    /// <summary>
    /// An extended HyperlinkButton control that uses the Tag property to
    /// open the web browser, compose an e-mail, text message, or make a call.
    /// </summary>
    public class PhoneHyperlinkButton : HyperlinkButton
    {
        /// <summary>
        /// Handles the click event.
        /// </summary>
        protected override void OnClick()
        {
            base.OnClick();

            Debug.Assert(Tag is string, "You need to set the Tag property!");
            string tag = Tag as string;
            if (tag == null)
            {
                // This should support data binding to Uri.
                tag = Tag.ToString();
            }

            IDictionary<string, string> d;

            if (tag.StartsWith("mailto:"))
            {
                Email(tag.Substring(7));
            }
            else if (tag.StartsWith("tel:"))
            {
                // RFC 2806 only defines the basics of a number component.
                // However, since the Windows Phone supports the concept of 
                // sending a name to display as well, I have deviated.
                // tel:8005221212&displayname=Unknown%20Caller
                PhoneCallTask pct = new PhoneCallTask();
                pct.PhoneNumber = GetAddress(tag.Substring(4), out d);

                string name;
                if (d.TryGetValue("displayname", out name))
                {
                    pct.DisplayName = name;
                }

                pct.Show();
            }
            else if (tag.StartsWith("sms:"))
            {
                // Also not really an official syntax for SMS.
                // sms:8005551212&body=Hello%20there!
                SmsComposeTask sct = new SmsComposeTask();
                sct.To = GetAddress(tag.Substring(4), out d);

                string body;
                if (d.TryGetValue("body", out body))
                {
                    sct.Body = body;
                }

                sct.Show();
            }
            else
            {
                // Assume the web.
                WebBrowserTask wbt = new WebBrowserTask
                {
                    URL = (string)Tag,
                };
                wbt.Show();
            }
        }

        private void Email(string s)
        {
            IDictionary<string, string> d;
            string to = GetAddress(s, out d);

            EmailComposeTask ect = new EmailComposeTask
            {
                To = to,
            };

            string cc;
            if (d.TryGetValue("cc", out cc))
            {
                ect.Cc = cc;
            }

            string subject;
            if (d.TryGetValue("subject", out subject))
            {
                ect.Subject = subject;
            }

            string body;
            if (d.TryGetValue("body", out body))
            {
                ect.Body = body;
            }

            ect.Show();
        }

        private static string GetAddress(string input, out IDictionary<string, string> query)
        {
            query = new Dictionary<string, string>(StringComparer.Ordinal);
            int q = input.IndexOf('?');
            string address = input;
            if (q >= 0)
            {
                address = input.Substring(0, q);
                ParseQueryStringToDictionary(input.Substring(q + 1), query);
            }
            return address;
        }

        private static void ParseQueryStringToDictionary(string queryString, IDictionary<string, string> dictionary)
        {
            foreach (string str in queryString.Split("&".ToCharArray(), StringSplitOptions.RemoveEmptyEntries))
            {
                int index = str.IndexOf("=", StringComparison.Ordinal);
                if (index == -1)
                {
                    dictionary.Add(HttpUtility.UrlDecode(str), string.Empty);
                }
                else
                {
                    dictionary.Add(HttpUtility.UrlDecode(str.Substring(0, index)), HttpUtility.UrlDecode(str.Substring(index + 1)));
                }
            }
        }
    }
}

Hope this helps.

Jeff Wilcox is a Software Engineer at Microsoft in the Open Source Programs Office (OSPO), helping Microsoft engineers use, contribute to and release open source at scale.

comments powered by Disqus