In this tutorial, we’re going to explore various ways to integrate our application with the features offered by the Windows Phone platform. We’ll explore launches and choosers, learn how to interact with contacts and appointments, and see how to take advantage of Kid’s Corner, an innovative feature introduced to allow kids to safely use the phone.
Launchers and Choosers
When we discussed storage earlier in this series, we introduced the concept of isolated applications. In the same way that storage is isolated such that you can’t access the data stored by another application, the application itself is isolated from the operating system.
The biggest benefit of this approach is security. Even if a malicious application is able to pass the certification process, it won’t have the chance to do much damage because it doesn’t have direct access to the operating system. But sooner or later, you’ll need to interact with one of the many Windows Phone features, like sending a message, making a phone call, playing a song, etc.
For all these scenarios, the framework has introduced launchers and choosers, which are sets of APIs that demand a specific task from the operating system. Once the task is completed, control is returned to the application.
Launchers are “fire and forget” APIs. You demand the operation and don’t expect anything in return—for example, starting a phone call or playing a video.
Choosers are used to get data from a native application—for example, contacts from the People Hub—and import it into your app.
All the launchers and choosers are available in the Microsoft.Phone.Tasks
namespace and share the same behavior:
- Every launcher and chooser is represented by a specific class.
- If needed, you set some properties that are used to define the launcher or chooser’s settings.
- With a chooser, you’ll need to subscribe to the
Completed
event, which is triggered when the operation is completed. - The
Show()
method is called to execute the task.
Note: Launchers and choosers can’t be used to override the built-in Windows Phone security mechanism, so you won’t be able to execute operations without explicit permission from the user.
In the following sample, you can see a launcher that sends an email using the EmailComposeTask
class:
private void OnComposeMailClicked(object sender, RoutedEventArgs e) { EmailComposeTask mailTask = new EmailComposeTask(); mailTask.To = “mail@domain.com”; mailTask.Cc = “mail2@domain.com”; mailTask.Subject = “Subject”; mailTask.Body = “Body”; mailTask.Show(); }
The following sample demonstrates how to use a chooser. We’re going to save a new contact in the People Hub using the SaveContactTask
class.
private void OnSaveContactClicked(object sender, RoutedEventArgs e) { SaveContactTask task = new SaveContactTask(); task.Completed += task_Completed; task.FirstName = “John”; task.LastName = “Doe”; task.MobilePhone = “1234567890”; task.Show(); } void task_Completed(object sender, SaveContactResult e) { if (e.TaskResult == TaskResult.OK) { MessageBox.Show(“The contact has been saved successfully”); } }
Every chooser returns a TaskResult
property, with the status of the operation. It’s important to verify that the status is TaskResult.OK
before moving on, because the user could have canceled the operation.
The following is a list of all the available launchers:
MapsDirectionTask
is used to open the native Map application and calculate a path between two places.MapsTask
is used to open the native Map application centered on a specific location.MapDownloaderTask
is used to manage the offline maps support new to Windows Phone 8. With this task, you’ll be able to open the Settings page used to manage the downloaded maps.MapUpdaterTask
is used to redirect the user to the specific Settings page to check for offline maps updates.ConnectionSettingsTask
is used to quickly access the different Settings pages to manage the different available connections, like Wi-Fi, cellular, or Bluetooth.EmailComposeTask
is used to prepare an email and send it.MarketplaceDetailTask
is used to display the detail page of an application on the Windows Phone Store. If you don’t provide the application ID, it will the open the detail page of the current application.MarketplaceHubTask
is used to open the Store to a specific category.MarketplaceReviewTask
is used to open the page in the Windows Phone Store where the user can leave a review for the current application.MarketplaceSearchTask
is used to start a search for a specific keyword in the Store.MediaPlayerLauncher
is used to play audio or a video using the internal Windows Phone player. It can play both files embedded in the Visual Studio project and those saved in the local storage.PhoneCallTask
is used to start a phone call.ShareLinkTask
is used to share a link on a social network using the Windows Phone embedded social features.ShareStatusTask
is used to share custom status text on a social network.ShareMediaTask
is used to share one of the pictures from the Photos Hub on a social network.SmsComposeTask
is used to prepare a text message and send it.WebBrowserTask
is used to open a URI in Internet Explorer for Windows Phone.SaveAppointmentTask
is used to save an appointment in the native Calendar app.
The following is a list of available choosers:
AddressChooserTask
is used to import a contact’s address.CameraCaptureTask
is used to take a picture with the integrated camera and import it into the application.EmailAddressChooserTask
is used to import a contact’s email address.PhoneNumberChooserTask
is used to import a contact’s phone number.PhotoChooserTask
is used to import a photo from the Photos Hub.SaveContactTask
is used to save a new contact in the People Hub. The chooser simply returns whether the operation completed successfully.SaveEmailAddressTask
is used to add a new email address to an existing or new contact. The chooser simply returns whether the operation completed successfully.SavePhoneNumberTask
is used to add a new phone number to an existing contact. The chooser simply returns whether the operation completed successfully.SaveRingtoneTask
is used to save a new ringtone (which can be part of the project or stored in the local storage). It returns whether the operation completed successfully.
Getting Contacts and Appointments
Launchers already provide a basic way of interacting with the People Hub, but they always require user interaction. They open the People Hub and the user must choose which contact to import.
However, in certain scenarios you need the ability to programmatically retrieve contacts and appointments for the data. Windows Phone 7.5 introduced some new APIs to satisfy this requirement. You just have to keep in mind that, to respect the Windows Phone security constraints, these APIs only work in read-only mode; you’ll be able to get data, but not save it (later in this article, we’ll see that Windows Phone 8 has introduced a way to override this limitation).
In the following table, you can see which data you can access based on where the contacts are saved.
Provider | Contact Name | Contact Picture | Other Information | Calendar Appointments |
Device | Yes | Yes | Yes | Yes |
Outlook.com | Yes | Yes | Yes | Yes |
Exchange | Yes | Yes | Yes | Yes |
SIM | Yes | Yes | Yes | No |
Yes | Yes | Yes | No | |
Other social networks | No | No | No | No |
To know where the data is coming from, you can use the Accounts
property, which is a collection of the accounts where the information is stored. In fact, you can have information for the same data split across different accounts.
Working With Contacts
Each contact is represented by the Contact
class, which contains all the information about a contact, like DisplayName
, Addresses
, EmailAddresses
, Birthdays
, etc. (basically, all the information that you can edit when you create a new contact in the People Hub).
Note: To access the contacts, you need to enable the ID_CAP_CONTACTS
option in the manifest file.
Interaction with contacts starts with the Contacts
class which can be used to perform a search by using the SearchAsync()
method. The method requires two parameters: the keyword and the filter to apply. There are two ways to start a search:
- A generic search: The keyword is not required since you’ll simply get all the contacts that match the selected filter. This type of search can be achieved with two filter types:
FilterKind.PinnedToStart
which returns only the contacts that the user has pinned on the Start screen, andFilterKind.None
which simply returns all the available contacts. - A search for a specific field: In this case, the search keyword will be applied based on the selected filter. The available filters are
DisplayName
,EmailAddress
, andPhoneNumber
.
The SearchAsync()
method uses a callback approach; when the search is completed, an event called SearchCompleted
is raised.
In the following sample, you can see a search that looks for all contacts whose name is John. The collection of returned contacts is presented to the user with a ListBox
control.
private void OnStartSearchClicked(object sender, RoutedEventArgs e) { Contacts contacts = new Contacts(); contacts.SearchCompleted += new EventHandler<ContactsSearchEventArgs>(contacts_SearchCompleted); contacts.SearchAsync("John", FilterKind.DisplayName, null); } void contacts_SearchCompleted(object sender, ContactsSearchEventArgs e) { Contacts.ItemsSource = e.Results; }
Tip: If you want to start a search for another field that is not included in the available filters, you’ll need to get the list of all available contacts by using the FilterKind.None
option and apply a filter using a LINQ query. The difference is that built-in filters are optimized for better performance, so make sure to use a LINQ approach only if you need to search for a field other than a name, email address, or phone number.
Working With Appointments
Getting data from the calendar works in a very similar way: each appointment is identified by the Appointment
class, which has properties like Subject
, Status
, Location
, StartTime
, and EndTime
.
To interact with the calendar, you’ll have to use the Appointments
class that, like the Contacts
class, uses a method called SearchAsync()
to start a search and an event called SearchCompleted
to return the results.
The only two required parameters to perform a search are the start date and the end date. You’ll get in return all the appointments within this time frame. Optionally, you can also set a maximum number of results to return or limit the search to a specific account.
In the following sample, we retrieve all the appointments that occur between the current date and the day before, and we display them using a ListBox
control.
private void OnStartSearchClicked(object sender, RoutedEventArgs e) { Appointments calendar = new Appointments(); calendar.SearchCompleted += calendar_SearchCompleted; DateTime start = DateTime.Now.AddMonths(-1); DateTime end = DateTime.Now; calendar.SearchAsync(start, end, null); } void calendar_SearchCompleted(object sender, AppointmentsSearchEventArgs e) { Calendar.ItemsSource = e.Results; }
Tip: The only way to filter the results is by start date and end date. If you need to apply additional filters, you’ll have to perform LINQ queries on the results returned by the search operation.
A Private Contact Store for Applications
The biggest limitation of the contacts APIs we’ve seen so far is that we’re only able to read data, not write it. There are some situations in which having the ability to add contacts to the People Hub without asking the user’s permission is a requirement, such as a social network app that wants to add your friends to your contacts list, or a synchronization client that needs to store information from a third-party cloud service in your contact book.
Windows Phone 8 has introduced a new class called ContactStore
that represents a private contact book for the application. From the user’s point of view, it behaves like a regular contacts source (like Outlook.com, Facebook, or Gmail). The user will be able to see the contacts in the People Hub, mixed with all the other regular contacts.
From a developer point of view, the store belongs to the application; you are free to read and write data, but every contact you create will be part of your private contact book, not the phone’s contact list. This means that if the app is uninstalled, all the contacts will be lost.
The ContactStore
class belongs to the Windows.Phone.PersonalInformation
namespace and it offers a method called CreateOrOpenAsync()
. The method has to be called every time you need to interact with the private contacts book. If it doesn’t exist, it will be created; otherwise, it will simply be opened.
When you create a ContactStore
you can set how the operating system should provide access to it:
- The first parameter’s type is
ContactStoreSystemAccessMode
, and it’s used to choose whether the application will only be able to edit contacts that belong to the private store (ReadOnly
), or the user will also be able to edit information using the People Hub (ReadWrite
). - The second parameter’s type is
ContactStoreApplicationAccessMode
, and it’s used to choose whether other third-party applications will be able to access all the information about our contacts (ReadOnly
) or only the most important ones, like name and picture (LimitedReadOnly
).
The following sample shows the code required to create a new private store:
private async void OnCreateStoreClicked(object sender, RoutedEventArgs e) { ContactStore store = await ContactStore.CreateOrOpenAsync(ContactStoreSystemAccessMode.ReadWrite, ContactStoreApplicationAccessMode.ReadOnly); }
Tip: After you’ve created a private store, you can’t change the permissions you’ve defined, so you’ll always have to call the CreateOrOpenAsync()
method with the same parameters.
Creating Contacts
A contact is defined by the StoredContact
class, which is a bit different from the Contact
class we’ve previously seen. In this case, the only properties that are directly exposed are GivenName
and FamilyName
. All the other properties can be accessed by calling the GetPropertiesAsync()
method of the StoredContact
class, which returns a collection of type Dictionary<string, object>
.
Every item of the collection is identified by a key (the name of the contact’s property) and an object (the value). To help developers access the properties, all the available keys are stored in an enum object named KnownContactProperties
. In the following sample, we use the key KnowContactProperties.Email
to store the user’s email address.
private async void OnCreateStoreClicked(object sender, RoutedEventArgs e) { ContactStore store = await ContactStore.CreateOrOpenAsync(ContactStoreSystemAccessMode.ReadWrite, ContactStoreApplicationAccessMode.ReadOnly); StoredContact contact = new StoredContact(store); contact.GivenName = "Matteo"; contact.FamilyName = "Pagani"; IDictionary<string, object> properties = await contact.GetPropertiesAsync(); properties.Add(KnownContactProperties.Email, "info@qmatteoq.com"); await contact.SaveAsync(); }
Tip: Since the ContactStore
is a dictionary, two values cannot have the same key. Before adding a new property to the contact, you’ll have to make sure that it doesn’t exist yet; otherwise, you’ll need to update the existing one.
The StoredContact
class also supports a way to store custom information by accessing the extended properties using the GetExtendedPropertiesAsync()
method. It works like the standard properties, except that the property key is totally custom. These kind of properties won’t be displayed in the People Hub since Windows Phone doesn’t know how to deal with them, but they can be used by your application.
In the following sample, we add new custom information called MVP Category
:
private async void OnCreateStoreClicked(object sender, RoutedEventArgs e) { ContactStore store = await ContactStore.CreateOrOpenAsync(ContactStoreSystemAccessMode.ReadWrite, ContactStoreApplicationAccessMode.ReadOnly); StoredContact contact = new StoredContact(store); contact.GivenName = "Matteo"; contact.FamilyName = "Pagani"; IDictionary<string, object> extendedProperties = await contact.GetExtendedPropertiesAsync(); extendedProperties.Add("MVP Category", "Windows Phone Development"); await contact.SaveAsync(); }
Searching for Contacts
Searching contacts in the private contact book is a little tricky because there’s no direct way to search a contact for a specific field.
Searches are performed using the ContactQueryResult
class, which is created by calling the CreateContactQuery()
method of the ContactStore
object. The only available operations are GetContactsAsync()
,which returns all the contacts, and GetContactCountAsync()
,which returns the number of available contacts.
You can also define in advance which fields you’re going to work with, but you’ll still have to use the GetPropertiesAsync()
method to extract the proper values. Let’s see how it works in the following sample, in which we look for a contact whose email address is info@qmatteoq.com
:
private async void OnSearchContactClicked(object sender, RoutedEventArgs e) { ContactStore store = await ContactStore.CreateOrOpenAsync(ContactStoreSystemAccessMode.ReadWrite, ContactStoreApplicationAccessMode.ReadOnly); ContactQueryOptions options = new ContactQueryOptions(); options.DesiredFields.Add(KnownContactProperties.Email); ContactQueryResult result = store.CreateContactQuery(options); IReadOnlyList<StoredContact> contactList = await result.GetContactsAsync(); foreach (StoredContact contact in contactList) { IDictionary<string, object> properties = await contact.GetPropertiesAsync(); if (properties.ContainsKey(KnownContactProperties.Email) && properties[KnownContactProperties.Email].ToString() == "info@qmatteoq.com") { MessageBox.Show("Contact found!"); } } }
You can define which fields you’re interested in by creating a new ContactQueryOptions
object and adding it to the DesiredFields
collection. Then, you can pass the ContactQueryOptions
object as a parameter when you create the ContactQueryResult
one. As you can see, defining the fields isn’t enough to get the desired result. We still have to query each contact using the GetPropertiesAsync()
method to see if the information value is the one we’re looking for.
The purpose of the ContactQueryOptions
class is to prepare the next query operations so they can be executed faster.
Updating and Deleting Contacts
Updating a contact is achieved in the same way as creating new one: after you’ve retrieved the contact you want to edit, you have to change the required information and call the SaveAsync()
method again, as in the following sample:
private async void OnSearchContactClicked(object sender, RoutedEventArgs e) { ContactStore store = await ContactStore.CreateOrOpenAsync(ContactStoreSystemAccessMode.ReadWrite, ContactStoreApplicationAccessMode.ReadOnly); ContactQueryOptions options = new ContactQueryOptions(); options.DesiredFields.Add(KnownContactProperties.Email); ContactQueryResult result = store.CreateContactQuery(options); IReadOnlyList<StoredContact> contactList = await result.GetContactsAsync(); foreach (StoredContact contact in contactList) { IDictionary<string, object> properties = await contact.GetPropertiesAsync(); if (properties.ContainsKey(KnownContactProperties.Email) && properties[KnownContactProperties.Email].ToString() == "info@qmatteoq.com") { properties[KnownContactProperties.Email] = "mail@domain.com"; await contact.SaveAsync(); } } }
After we’ve retrieved the user whose email address is info@qmatteoq.com
, we change it to mail@domain.com
, and save it.
Deletion works in a similar way, except that you’ll have to deal with the contact’s ID, which is a unique identifier that is automatically assigned by the store (you can’t set it; you can only read it). Once you’ve retrieved the contact you want to delete, you have to call the DeleteContactAsync()
method on the ContactStore
object, passing as parameter the contact ID, which is stored in the Id
property of the StoredContact
class.
private async void OnSearchContactClicked(object sender, RoutedEventArgs e) { ContactStore store = await ContactStore.CreateOrOpenAsync(ContactStoreSystemAccessMode.ReadWrite, ContactStoreApplicationAccessMode.ReadOnly); ContactQueryOptions options = new ContactQueryOptions(); options.DesiredFields.Add(KnownContactProperties.Email); ContactQueryResult result = store.CreateContactQuery(options); IReadOnlyList<StoredContact> contactList = await result.GetContactsAsync(); foreach (StoredContact contact in contactList) { IDictionary<string, object> properties = await contact.GetPropertiesAsync(); if (properties.ContainsKey(KnownContactProperties.Email) && properties[KnownContactProperties.Email].ToString() == "info@qmatteoq.com") { await store.DeleteContactAsync(contact.Id); } } }
In the previous sample, after we’ve retrieved the contact with the email address info@qmatteoq.com
, we delete it using its unique identifier.
Dealing With Remote Synchronization
When working with custom contact sources, we usually don’t simply manage local contacts, but data that is synced with a remote service instead. In this scenario, you have to keep track of the remote identifier of the contact, which will be different from the local one since, as previously mentioned, it’s automatically generated and can’t be set.
For this scenario, the StoredContact
class offers a property called RemoteId
to store such information. Having a RemoteId
also simplifies the search operations we’ve seen before. The ContactStore
class, in fact, offers a method called FindContactByRemoteIdAsync()
, which is able to retrieve a specific contact based on the remote ID as shown in the following sample:
private async void OnFindButtonClicked(object sender, RoutedEventArgs e) { ContactStore store = await ContactStore.CreateOrOpenAsync(ContactStoreSystemAccessMode.ReadWrite, ContactStoreApplicationAccessMode.ReadOnly); string myRemoteId = "2918"; RemoteIdHelper remoteHelper = new RemoteIdHelper(); string taggedRemoteId = await remoteHelper.GetTaggedRemoteId(store, myRemoteId); StoredContact contact = await store.FindContactByRemoteIdAsync(taggedRemoteId); }
There’s one important requirement to keep in mind: the RemoteId
property’s value should be unique across any application installed on the phone that uses a private contact book; otherwise, you’ll get an exception.
In this article published by Microsoft, you can see an implementation of a class called RemoteIdHelper
that offers some methods for adding random information to the remote ID (using a GUID) to make sure it’s unique.
Taking Advantage of Kid's Corner
Kid’s Corner is an interesting and innovative feature introduced in Windows Phone 8 that is especially useful for parents of young children. Basically, it’s a sandbox that we can customize. We can decide which apps, games, pictures, videos, and music can be accessed.
As developers, we are able to know when an app is running in Kid’s Corner mode. This way, we can customize the experience to avoid providing inappropriate content, such as sharing features.
Taking advantage of this feature is easy; we simply check the Modes
property of the ApplicationProfile
class, which belongs to the Windows.Phone.ApplicationModel
namespace. When it is set to Default
, the application is running normally. If it’s set to Alternate
, it’s running in Kid’s Corner mode.
private void OnCheckStatusClicked(object sender, RoutedEventArgs e) { if (ApplicationProfile.Modes == ApplicationProfileModes.Default) { MessageBox.Show("The app is running in normal mode."); } else { MessageBox.Show("The app is running in Kid's Corner mode."); } }
Speech APIs: Let's Talk With the Application
Speech APIs are one of the most interesting new features added in Windows Phone 8. From a user point of view, vocal features are managed in the Settings page. The Speech section lets users configure all the main settings like the voice type, but most of all, it’s used to set up the language they want to use for the speech services. Typically, it’s set with the same language of the user interface, and users have the option to change it by downloading and installing a new voice pack. It’s important to understand how speech services are configured, because in your application, you’ll be able to use speech recognition only for languages that have been installed by the user.
The purpose of speech services is to add vocal recognition support in your applications in the following ways:
- Enable users to speak commands to interact with the application, such as opening it and executing a task.
- Enable text-to-speech features so that the application is able to read text to users.
- Enable text recognition so that users can enter text by dictating it instead of typing it.
In this section, we’ll examine the basic requirements for implementing all three modes in your application.
Voice Commands
Voice commands are a way to start your application and execute a specific task regardless of what the user is doing. They are activated by tapping and holding the Start button. Windows Phone offers native support for many voice commands, such as starting a phone call, dictating email, searching via Bing, and more.
The user simply has to speak a command; if it’s successfully recognized, the application will be opened and, as developers, we’ll get some information to understand which command has been issued so that we can redirect the user to the proper page or perform a specific operation.
Voice commands are based on VCD files, which are XML files that are included in your project. Using a special syntax, you’ll be able to define all the commands you want to support in your application and how the application should behave when they are used. These files are natively supported by Visual Studio. If you right-click on your project and choose Add new item, you’ll find a template called VoiceCommandDefinition
in the Windows Phone section.
The following code sample is what a VCD file looks like:
<?xml version="1.0" encoding="utf-8"?><VoiceCommands xmlns="http://schemas.microsoft.com/voicecommands/1.0"><CommandSet xml:lang="it" Name="NotesCommandSet"><CommandPrefix>My notes</CommandPrefix><Example> Open my notes and add a new note </Example><Command Name="AddNote"><Example> add a new note </Example><ListenFor> [and] add [a] new note </ListenFor><ListenFor> [and] create [a] new note </ListenFor><Feedback> I’m adding a new note... </Feedback><Navigate Target="/AddNote.xaml" /></Command></CommandSet></VoiceCommands>
A VCD file can contain one or more CommandSet
nodes, which are identified by a Name
and a specific language (the xml:lang
attribute). The second attribute is the most important one. Your application will support voice commands only for the languages you’ve included in CommandSet
in the VCD file (the voice commands’ language is defined by users in the Settings page). You can have multiple CommandSet
nodes to support multiple languages.
Each CommandSet
can have a CommandPrefix
, which is the text that should be spoken by users to start sending commands to our application. If one is not specified, the name of the application will automatically be used. This property is useful if you want to localize the command or if your application’s title is too complex to pronounce. You can also add an Example
tag, which contains the text displayed by the Windows Phone dialog to help users understand what kind of commands they can use.
Then, inside a CommandSet
, you can add up to 100 commands identified by the Command
tag. Each command has the following characteristics:
- A unique name, which is set in the
Name
attribute. - The
Example
tag shows users sample text for the current command. ListenFor
contains the text that should be spoken to activate the command. Up to tenListenFor
tags can be specified for a single command to cover variations of the text. You can also add optional words inside square brackets. In the previous sample, theAddNote
command can be activated by pronouncing both “add a new note” or “and add new note.”Feedback
is the text spoken by Windows Phone to notify users that it has understood the command and is processing it.NavigateTarget
can be used to customize the navigation flow of the application. If we don’t set it, the application will be opened to the main page by default. Otherwise, as in the previous sample, we can redirect the user to a specific page. Of course, in both cases we’ll receive the information about the spoken command; we’ll see how to deal with them later.
Once we’ve completed the VCD definition, we are ready to use it in our application.
Note: To use speech services, you’ll need to enable the ID_CAP_SPEECH_RECOGNITION
option in the manifest file.
Commands are embedded in a Windows Phone application by using a class called VoiceCommandService
, which belongs to the Windows.Phone.Speech.VoiceCommands
namespace. This static class exposes a method called InstallCommandSetFromFileAsync()
, which requires the path of the VCD file we’ve just created.
private async void OnInitVoiceClicked(object sender, RoutedEventArgs e) { await VoiceCommandService.InstallCommandSetsFromFileAsync(new Uri("ms-appx:///VoiceCommands.xml")); }
The file path is expressed using a Uri
that should start with the ms-appx:///
prefix. This Uri
refers to the Visual Studio project’s structure, starting from the root.
Phrase Lists
A VCD file can also contain a phrase list, as in the following sample:
<?xml version="1.0" encoding="utf-8"?><VoiceCommands xmlns="http://schemas.microsoft.com/voicecommands/1.0"><CommandSet xml:lang="en" Name="NotesCommandSet"><CommandPrefix>My notes</CommandPrefix><Example> Open my notes and add a new note </Example><Command Name="OpenNote"><Example> open the note </Example><ListenFor> open the note {number} </ListenFor><Feedback> I’m opening the note... </Feedback><Navigate /></Command><PhraseList Label="number"><Item> 1 </Item><Item> 2 </Item><Item> 3 </Item></PhraseList></CommandSet></VoiceCommands>
Phrase lists are used to manage parameters that can be added to a phrase using braces. Each PhraseList
node is identified by a Label
attribute, which is the keyword to include in the braces inside the ListenFor
node. In the previous example, users can say the phrase “open the note” followed by any of the numbers specified with the Item
tag inside the PhraseList
. You can have up to 2,000 items in a single list.
The previous sample is useful for understanding how this feature works, but it’s not very realistic; often the list of parameters is not static, but is dynamically updated during the application execution. Take the previous scenario as an example: in a notes application, the notes list isn’t fixed since users can create an unlimited number of notes.
The APIs offer a way to keep a PhraseList
dynamically updated, as demonstrated in the following sample:
private async void OnUpdatePhraseListClicked(object sender, RoutedEventArgs e) { VoiceCommandSet commandSet = VoiceCommandService.InstalledCommandSets["NotesCommandSet"]; await commandSet.UpdatePhraseListAsync("number", new string[] { "1", "2", "3", "4", "5" }); }
First, you have to get a reference to the current command set by using the VoiceCommandService.InstalledCommandSets
collection. As the index, you have to use the name of the set that you’ve defined in the VCD file (the Name
attribute of the CommandSet
tag). Once you have a reference to the set, you can call the UpdatePhraseListAsync()
to update a list by passing two parameters:
- the name of the
PhraseList
(set using theLabel
attribute) - the collection of new items, as an array of strings
It’s important to keep in mind that the UpdatePhraseListAsync()
method overrides the current items in the PhraseList
, so you will have to add all the available items every time, not just the new ones.
Intercepting the Requested Command
The command invoked by the user is sent to your application with the query string mechanism discussed earlier in this series. When an application is opened by a command, the user is redirected to the page specified in the Navigate
node of the VCD file. The following is a sample URI:
/MainPage.xaml?voiceCommandName=AddNote&reco=My%20notes%20create%20a%20new%20note
The voiceCommandName
parameter contains the spoken command, while the reco
parameter contains the full text that has been recognized by Windows Phone.
If the command supports a phrase list, you’ll get another parameter with the same name of the PhraseList
and the spoken item as a value. The following code is a sample URI based on the previous note sample, where the user can open a specific note by using the OpenNote
command:
/MainPage.xaml?voiceCommandName=OpenNote&reco=My%20notes%20open%20a%20new%20note&number=2
Using the APIs we saw earlier in this series, it’s easy to extract the needed information from the query string parameters and use them for our purposes, like in the following sample:
protected override void OnNavigatedTo(NavigationEventArgs e) { if (NavigationContext.QueryString.ContainsKey("voiceCommandName")) { string commandName = NavigationContext.QueryString["voiceCommandName"]; switch (commandName) { case "AddNote": //Create a new note. break; case "OpenNote": if (NavigationContext.QueryString.ContainsKey("number")) { int selectedNote = int.Parse(NavigationContext.QueryString["number"]); //Load the selected note. } break; } } }
We use a switch
statement to manage the different supported commands that are available in the NavigationContext.QueryString
collection. If the user is trying to open a note, we also get the value of the number
parameter.
Working With Speech Recognition
In the beginning of this section, we talked about how to recognize commands that are spoken by the user to open the application. Now it’s time to see how to do the same within the app itself, to recognize commands and allow users to dictate text instead of typing it (a useful feature in providing a hands-free experience).
There are two ways to start speech recognition: by providing a user interface, or by working silently in the background.
In the first case, you can provide users a visual dialog similar to the one used by the operating system when holding the Start button. It’s the perfect solution to manage vocal commands because you’ll be able to give both visual and voice feedback to users.
This is achieved by using the SpeechRecognizerUI
class, which offers four key properties to customize the visual dialog:
ListenText
is the large, bold text that explains to users what the application is expecting.Example
is additional text that is displayed below theListenText
to help users better understand what kind of speech the application is expecting.ReadoutEnabled
is aBoolean
property; when it’s set to true, Windows Phone will read the recognized text to users as confirmation.ShowConfirmation
is anotherBoolean
property; when it’s set to true, users will be able to cancel the operation after the recognition process is completed.
The following sample shows how this feature is used to allow users to dictate a note. We ask users for the text of the note and then, if the operation succeeded, we display the recognized text.
private async void OnStartRecordingClicked(object sender, RoutedEventArgs e) { SpeechRecognizerUI sr = new SpeechRecognizerUI(); sr.Settings.ListenText = "Start dictating the note"; sr.Settings.ExampleText = "dictate the note"; sr.Settings.ReadoutEnabled = false; sr.Settings.ShowConfirmation = true; SpeechRecognitionUIResult result = await sr.RecognizeWithUIAsync(); if (result.ResultStatus == SpeechRecognitionUIStatus.Succeeded) { RecordedText.Text = result.RecognitionResult.Text; } }
Notice how the recognition process is started by calling the RecognizeWithUIAsync()
method, which returns a SpeechRecognitionUIResult
object that contains all the information about the operation.
To silently recognize text, less code is needed since fewer options are available than were used for the dialog. We just need to start listening for the text and understand it. We can do this by calling the RecognizeAsync()
method of the SpeechRecognizer
class. The recognition result will be stored in a SpeechRecognitionResult
object, which is the same that was returned in the RecognitionResult
property by the RecognizeWithUIAsync()
method we used previously.
Using Custom Grammars
The code we’ve seen so far is used to recognize almost any word in the dictionary. For this reason, speech services will work only if the phone is connected to the Internet since the feature uses online Microsoft services to parse the results.
This approach is useful when users talk to the application to dictate text, as in the previous samples with the note application. But if only a few commands need to be managed, having access to all the words in the dictionary is not required. On the contrary, complete access can cause problems because the application may understand words that aren’t connected to any supported command.
For this scenario, the Speech APIs provide a way to use a custom grammar and limit the number of words that are supported in the recognition process. There are three ways to set a custom grammar:
- using only the available standard sets
- manually adding the list of supported words
- storing the words in an external file
Again, the starting point is the SpeechRecognizer
class, which offers a property called Grammars
.
To load one of the predefined grammars, use the AddGrammarFromPredefinedType()
method, which accepts as parameters a string to identify it (you can choose any value) and the type of grammar to use. There are two sets of grammars: the standard SpeechPredefinedGrammar.Dictation
, and SpeechPredefinedGrammar.WebSearch
, which is optimized for web related tasks.
In the following sample, we recognize speech using the WebSearch
grammar:
private async void OnStartRecordingWithoutUIClicked(object sender, RoutedEventArgs e) { SpeechRecognizer recognizer = new SpeechRecognizer(); recognizer.Grammars.AddGrammarFromPredefinedType("WebSearch", SpeechPredefinedGrammar.WebSearch); SpeechRecognitionResult result = await recognizer.RecognizeAsync(); RecordedText.Text = result.Text; }
Even more useful is the ability to allow the recognition process to understand only a few selected words. We can use the AddGrammarFromList()
method offered by the Grammars
property, which requires the usual identification key followed by a collection of supported words.
In the following sample, we set the SpeechRecognizer
class to understand only the words “save” and “cancel”.
private async void OnStartRecordingClicked(object sender, RoutedEventArgs e) { SpeechRecognizer recognizer = new SpeechRecognizer(); string[] commands = new[] { "save", "cancel" }; recognizer.Grammars.AddGrammarFromList("customCommands", commands); SpeechRecognitionResult result = await recognizer.RecognizeAsync(); if (result.Text == "save") { //Saving } else if (result.Text == "cancel") { //Cancelling the operation } else { MessageBox.Show("Command not recognized"); } }
If the user says a word that is not included in the custom grammar, the Text
property of the SpeechRecognitionResult
object will be empty. The biggest benefit of this approach is that it doesn’t require an Internet connection since the grammar is stored locally.
The third and final way to load a grammar is by using another XML definition called Speech Recognition Grammar Specification (SRGS)
. You can read more about the supported tags in the official documentation by W3C.
The following sample shows a custom grammar file:
<?xml version="1.0" encoding="utf-8" ?><grammar version="1.0" xml:lang="en" root="rootRule" tag-format="semantics/1.0" xmlns="http://www.w3.org/2001/06/grammar" xmlns:sapi="http://schemas.microsoft.com/Speech/2002/06/SRGSExtensions"><rule id="openAction"><one-of><item>open</item><item>load</item></one-of></rule><rule id="fileWords"><one-of><item> note </item><item> reminder </item></one-of></rule><rule id="rootRule"><ruleref uri="#openAction" /><one-of><item>the</item><item>a</item></one-of><ruleref uri="#fileWords" /></rule></grammar>
The file describes both the supported words and the correct order that should be used. The previous sample shows the supported commands to manage notes in an application, like “Open the note” or “Load a reminder,” while a command like “Reminder open the” is not recognized.
Visual Studio 2012 offers built-in support for these files with a specific template called SRGS Grammar
that is available when you right-click your project and choose Add new item.
Once the file is part of your project, you can load it using the AddGrammarFromUri()
method of the SpeechRecognizer
class that accepts as a parameter the file path expressed as a Uri
, exactly as we’ve seen for VCD files. From now on, the recognition process will use the grammar defined in the file instead of the standard one, as shown in the following sample:
private async void OnStartRecordingWithCustomFile(object sender, RoutedEventArgs e) { SpeechRecognizer recognizer = new SpeechRecognizer(); recognizer.Grammars.AddGrammarFromUri("CustomGrammar", new Uri("ms-appx:///CustomGrammar.xml")); SpeechRecognitionResult result = await recognizer.RecognizeAsync(); if (result.Text != string.Empty) { RecordedText.Text = result.Text; } else { MessageBox.Show("Not recognized"); } }
Using Text-to-Speech (TTS)
Text-to-speech is a technology that is able to read text to users in a synthesized voice. It can be used to create a dialogue with users so they won’t have to watch the screen to interact with the application.
The basic usage of this feature is really simple. The base class to interact with TTS services is SpeechSynthesizer
, which offers a method called SpeakTextAsync()
. You simply have to pass to the method the text that you want to read, as shown in the following sample:
private async void OnSpeakClicked(object sender, RoutedEventArgs e) { SpeechSynthesizer synth = new SpeechSynthesizer(); await synth.SpeakTextAsync("This is a sample text"); }
Moreover, it’s possible to customize how the text is pronounced by using a standard language called Synthesis Markup Language (SSML), which is based on the XML standard. This standard provides a series of XML tags that defines how a word or part of the text should be pronounced. For example, the speed, language, voice gender, and more can be changed.
The following sample is an example of an SSML file:
<?xml version="1.0"?><speak xmlns="http://www.w3.org/2001/10/synthesis" xmlns:dc="http://purl.org/dc/elements/1.1/" xml:lang="en" version="1.0"><voice age="5">This text is read by a child</voice><break /><prosody rate="x-slow"> This text is read very slowly</prosody></speak>
This code features three sample SSML tags: voice
for simulating the voice’s age, break
to add a pause, and prosody
to set the reading speed using the rate
attribute.
There are two ways to use an SSML definition in your application. The first is to create an external file by adding a new XML file in your project. Next, you can load it by passing the file path to the SpeakSsmlFromUriAsync()
method of the SpeechSynthesizer
class, similar to how we loaded the VCD file.
private async void OnSpeakClicked(object sender, RoutedEventArgs e) { SpeechSynthesizer synth = new SpeechSynthesizer(); await synth.SpeakSsmlFromUriAsync(new Uri("ms-appx:///SSML.xml")); }
Another way is to define the text to be read directly in the code by creating a string that contains the SSML tags. In this case, we can use the SpeakSsmlAsync()
method which accepts the string to read as a parameter. The following sample shows the same SSML definition we’ve been using, but stored in a string instead of an external file.
private async void OnSpeakClicked(object sender, RoutedEventArgs e) { SpeechSynthesizer synth = new SpeechSynthesizer(); StringBuilder textToRead = new StringBuilder(); textToRead.AppendLine("<speak version=\"1.0\""); textToRead.AppendLine(" xmlns=\"http://www.w3.org/2001/10/synthesis\""); textToRead.AppendLine(" xml:lang=\"en\">"); textToRead.AppendLine(" <voice age=\"5\">This text is read by a child</voice>"); textToRead.AppendLine("<prosody rate=\"x-slow\"> This text is read very slowly</prosody>"); textToRead.AppendLine("</speak>"); await synth.SpeakSsmlAsync(textToRead.ToString()); }
You can learn more about the SSML definition and available tags in the official documentation provided by W3C.
Data Sharing
Data sharing is a new feature introduced in Windows Phone 8 that can be used to share data between different applications, including third-party ones.
There are two ways to manage data sharing:
- File sharing: The application registers an extension such as
.log
. It will be able to manage any file with the registered extension that is opened by another application (for example, a mail attachment). - Protocol sharing: The application registers a protocol such as
log:
. Other applications will be able to use it to send plain data like strings or numbers.
In both cases, the user experience is similar:
- If no application is available on the device to manage the requested extension or protocol, users will be asked if they want to search the Store for one that can.
- If only one application is registered for the requested extension or protocol, it will automatically be opened.
- If multiple applications are registered for the same extension or protocol, users will be able to choose which one to use.
Let’s discuss how to support both scenarios in our application.
Note: There are some file types and protocols that are registered by the system, like Office files, pictures, mail protocols, etc. You can’t override them; only Windows Phone is able to manage them. You can see a complete list of the reserved types in the MSDN documentation.
File Sharing
File sharing is supported by adding a new definition in the manifest file that notifies the operating system which extensions the application can manage. As in many other scenarios we’ve previously seen, this modification is not supported by the visual editor, so we’ll need to right-click the manifest file and choose the View code option.
The extension is added in the Extensions
section, which should be defined under the Token
one:
<Extensions><FileTypeAssociation Name="LogFile" TaskID="_default" NavUriFragment="fileToken=%s"><Logos><Logo Size="small">log-33x33.png</Logo><Logo Size="medium">log-69x69.png</Logo><Logo Size="large">log-176x176.png</Logo></Logos><SupportedFileTypes><FileType ContentType="text/plain">.log</FileType></SupportedFileTypes></FileTypeAssociation></Extensions>
Every supported file type has its own FileTypeAssociation
tag, which is identified by the Name
attribute (which should be unique). Inside this node are two nested sections:
Logos
is optional and is used to support an icon to visually identify the file type. Three different images are required, each with a different resolution: 33 × 33, 69 × 69, and 176 × 176. The icons are used in various contexts, such as when the file is received as an email attachment.SupportedFileTypes
is required because it contains the extensions that are going to be supported for the current file type. Multiple extensions can be added.
The previous sample is used to manage the .log
file extension in our application.
When another application tries to open a file we support, our application is opened using a special URI:
/FileTypeAssociation?fileToken=89819279-4fe0-9f57-d633f0949a19
The fileToken
parameter is a GUID that univocally identifies the file—we’re going to use it later.
To manage the incoming URI, we need to introduce the UriMapper
class we talked about earlier in this series. When we identify this special URI, we’re going to redirect the user to a specific page of the application that is able to interact with the file.
The following sample shows what the UriMapper
looks like:
public class UriMapper: UriMapperBase { public override Uri MapUri(Uri uri) { string tempUri = HttpUtility.UrlDecode(uri.ToString()); if (tempUri.Contains("/FileTypeAssociation")) { int fileIdIndex = tempUri.IndexOf("fileToken=") + 10; string fileId = tempUri.Substring(fileIdIndex); string incomingFileName = SharedStorageAccessManager.GetSharedFileName(fileId); string incomingFileType = System.IO.Path.GetExtension(incomingFileName); switch (incomingFileType) { case ".log": return new Uri("/LogPage.xaml?fileToken=" + fileId, UriKind.Relative); default: return new Uri("/MainPage.xaml", UriKind.Relative); } } return uri; } }
If the starting Uri
contains the FileTypeAssociation
keyword, it means that the application has been opened due to a file sharing request. In this case, we need to identify the opened file’s extension. We extract the fileToken
parameter and, by using the GetSharedFileName()
of the SharedAccessManager
class (which belongs to the Windows.Phone.Storage
namespace), we retrieve the original file name.
By reading the name, we’re able to identify the extension and perform the appropriate redirection. In the previous sample, if the extension is .log
, we redirect the user to a specific page of the application called LogPage.xaml
. It’s important to add to the Uri
the fileToken
parameter as a query string; we’re going to use it in the page to effectively retrieve the file. Remember to register the UriMapper
in the App.xaml.cs
file, as explained earlier in this series.
Tip: The previous UriMapper
shows a full example that works when the application supports multiple file types. If your application supports just one extension, it’s not needed to retrieve the file name and identify the file type. Since the application can be opened with the special URI only in a file sharing scenario, you can immediately redirect the user to the dedicated page.
Now it’s time to interact with the file we received from the other application. We’ll do this in the page that we’ve created for this purpose (in the previous sample code, it’s the one called LogPage.xaml
).
We’ve seen that when another application tries to open a .log
file, the user is redirected to the LogPage.xaml
page with the fileToken
parameter added to the query string. We’re going to use the OnNavigatedTo
event to manage this scenario:
protected override async void OnNavigatedTo(NavigationEventArgs e) { if (NavigationContext.QueryString.ContainsKey(“fileToken”)) { await SharedStorageAccessManager.CopySharedFileAsync(ApplicationData.Current.LocalFolder, “file.log”, NameCollisionOption.ReplaceExisting, NavigationContext.QueryString[“fileToken”]); } }
Again we use the SharedStorageAccessManager
class, this time by invoking the CopySharedFileAsync()
method. Its purpose is to copy the file we received to the local storage so that we can work with it.
The required parameters are:
- A
StorageFolder
object, which represents the local storage folder in which to save the file (in the previous sample, we save it in the root). - The name of the file.
- The behavior to apply in case a file with the same name already exists (by using one of the values of the
NameCollisionOption
enumerator). - The GUID that identifies the file, which we get from the
fileToken
query string parameter.
Once the operation is completed, a new file called file.log
will be available in the local storage of the application, and we can start playing with it. For example, we can display its content in the current page.
How to Open a File
So far we’ve seen how to manage an opened file in our application, but we have yet to discuss how to effectively open a file.
The task is easily accomplished by using the LaunchFileAsync()
method offered by the Launcher
class (which belongs to the Windows.System
namespace). It requires a StorageFile
object as a parameter, which represents the file you would like to open.
In the following sample, you can see how to open a log file that is included in the Visual Studio project:
private async void OnOpenFileClicked(object sender, RoutedEventArgs e) { StorageFile storageFile = await Windows.ApplicationModel.Package.Current.InstalledLocation.GetFileAsync(“file.log”); Windows.System.Launcher.LaunchFileAsync(storageFile); }
Protocol Sharing
Protocol sharing works similarly to file sharing. We’re going to register a new extension in the manifest file, and we’ll deal with the special URI that is used to launch the application.
Let’s start with the manifest. In this case as well, we’ll have to add a new element in the Extensions
section that can be accessed by manually editing the file through the View code
option.
<Extensions><Protocol Name=“log” NavUriFragment=“encodedLaunchUri=%s” TaskID=“_default” /></Extensions>
The most important attribute is Name
, which identifies the protocol we’re going to support. The other two attributes are fixed.
An application that supports protocol sharing is opened with the following URI:
/Protocol?encodedLaunchUri=log:ShowLog?LogId=1
The best way to manage it is to use a UriMapper
class, as we did for file sharing. The difference is that this time, we’ll look for the encodedLaunchUri
parameter. However, the result will be the same: we will redirect the user to the page that is able to manage the incoming information.
public class UriMapper : UriMapperBase { public override Uri MapUri(Uri uri) { string tempUri = System.Net.HttpUtility.UrlDecode(uri.ToString()); if (tempUri.Contains(“Protocol”)) { int logIdIndex = tempUri.IndexOf(“LogId=“) + 6; string logId = tempUri.Substring(logIdIndex); return new Uri(“/LogPage.xaml?LogId=“ + logId, UriKind.Relative); } return uri; } }
In this scenario, the operation is simpler. We extract the value of the parameter LogId
and pass it to the LogPage.xaml
page. Also, we have less work to do in the landing page; we just need to retrieve the parameter’s value using the OnNavigatedTo
event, and use it to load the required data, as shown in the following sample:
protected override void OnNavigatedTo(NavigationEventArgs e) { if (NavigationContext.QueryString.ContainsKey(“LogId”)) { string logId = NavigationContext.QueryString[“LogId”]; MessageBox.Show(logId); } }
How to Open a URI
Similar to file sharing, other applications can interact with ours by using the protocol sharing feature and the Launcher
class that belongs to the Windows.System
namespace.
The difference is that we need to use the LaunchUriAsync()
method, as shown in the following sample:
private async void OnOpenUriClicked(object sender, RoutedEventArgs e) { Uri uri = new Uri(“log:ShowLog?LogId=1”); await Windows.System.Launcher.LaunchUriAsync(uri); }
Conclusion
In this tutorial, we’ve examined various ways to integrate our application with the features offered by the Windows Phone platform:
- We started with the simplest integration available: launchers and choosers, which are used to demand an operation from the operating system and eventually get some data in return.
- We looked at how to interact with user contacts and appointments: first with a read-only mode offered by a new set of APIs introduced in Windows Phone 7.5, and then with the private contacts book, which is a contacts store that belongs to the application but can be integrated with the native People Hub.
- We briefly talked about how to take advantage of Kid’s Corner, an innovative feature introduced to allow kids to safely use the phone without accessing applications that are not suitable for them.
- We learned how to use one of the most powerful new APIs added in Windows Phone 8: Speech APIs, to interact with our application using voice commands.
- We introduced data sharing, which is another new feature used to share data between different applications, and we can manage file extensions and protocols.
This tutorial represents a chapter from Windows Phone 8 Succinctly, a free eBook from the team at Syncfusion.