Freigeben über


Implementing contact bindings in a Windows Phone Silverlight 8.1 app

[ This article is for Windows Phone 8 developers. If you’re developing for Windows 10, see the latest documentation. ]

Example walkthrough

This topic walks you through an example app that implements custom contacts. This example includes a foreground app and a background agent. To make the sample code in this example more concise and readable, the code uses some helper methods for common tasks that are not described in detail. To view the complete, working sample, including all of the helper methods, see Contact Bindings Sample.

Update your app manifest file

To implement contact bindings, your app must declare the required extensions in the WMAppManifest.xml file of your Silverlight 8.1 project.

Add the People_Connect and the People_Connect_Manual extensions to your app manifest. In Solution Explorer, under Properties, right-click WMAppManifest.xml, and then click View Code. If your app manifest doesn’t already contain an Extensions element, create one following the Tokens element. Add the following Extension elements inside the Extensions element.

<Extension ExtensionName="People_Connect" ConsumerID="{bedab396-3404-490c-822e-13309c687e97}" TaskID="_default" />
<Extension ExtensionName="People_Connect_Manual" ConsumerID="{bedab396-3404-490c-822e-13309c687e97}" TaskID="_default" />

If you don’t include the People_Connect extension in your app, the tile for your contact bindings will not appear on the connect pivot in the People Hub. If you don’t include the People_Connect_Manual extension, your app name will not appear when the user taps add apps on the connect pivot to manually add a contact binding.

Add URI mapping to App.xaml.cs

When your app implements contact bindings, the system launches your app when the user taps your app’s tile in the connect pivot for a contact in the People Hub. When this occurs, the launch URI contains the query string parameter action=Show_Contact and the parameter contact_ids, which indicate which contact binding your app should display content for. If your app registered as supporting manual connect for contact bindings, the system also launches your app if the user taps add apps in a contact for which a binding doesn’t already exist, selects your app, and then taps your app’s connect tile. In this case, the action query string parameter in the launch URI will be Connect_Contact and the ID of the contact binding that is being manually bound. Add a URI mapping class that derives from UriMapperBase to your app.xaml.cs file to intercept the launch URI, parse it, and then navigate to the relevant page for the action. The following example shows a URI mapper that navigates to ShowContact.xaml and ManualConnect.xaml based on the action query string parameter. If your app doesn’t support manual connect, you can leave out that code block. This example directs all other launch URIs to MainPage.xaml. Your can implement this differently based on the needs of your app.

class AssociationUriMapper : UriMapperBase
{
    public override Uri MapUri(Uri uri)
    {
        System.Diagnostics.Debug.WriteLine("AssociationUriMapper (old) : " + uri.ToString());

        // send all links to MainPage.xaml by default
        Uri newUri = new Uri("/MainPage.xaml", UriKind.Relative);

        string uriAsString = uri.ToString();

        // Before navigating to one of the contact binding pages, check to make sure the user is signed in
        if (!string.IsNullOrEmpty(App.MyAppSettings.AuthTokenSetting))
        {
            // change the deep link URI to land on the right page
            // e.g.: /PeopleExtension?action=Show_Contact&contact_ids=2002
            // becomes /MainPage.xaml?action=Show_Contact&contact_ids=2002

            if (uriAsString.Contains("Show_Contact"))
            {
                // When the system includes "Show_Contact" in the uri, the user tapped the connect tile for
                // one of your app's contact bindings.

                // keep the parameters and send to MainPage.xaml
                string parameters = uriAsString.Substring(uriAsString.IndexOf('?'));
                newUri = new Uri("/ShowContact.xaml" + parameters, UriKind.Relative);
            }
            else if (uriAsString.Contains("Connect_Contact"))
            {
                // This is an optional scenario. See ManualConnect.xaml.cs.
                // When the system includes "Connect_Contact" in the uri, the user is attempting to 
                // manually bind a contact to your app.

                string offsetQueryParams = uri.ToString().Substring(uriAsString.IndexOf('&') + 1);
                string[] args = offsetQueryParams.Split('=');

                if (args[0] == "binding_id")
                {
                    // Map the uri to ShowContact.xaml and include the ID of the contact binding
                    // in the query string params
                    return new Uri("/ManualConnect.xaml?id=" + args[1], UriKind.Relative);
                }
            }

        }

        System.Diagnostics.Debug.WriteLine("AssociationUriMapper (new) : " + newUri.ToString());
        return newUri;
    }
}

For your URI mapper to be called when your app navigates, you must set it to the UriMapper property of the root frame.

RootFrame.UriMapper = new AssociationUriMapper();

Handling user sign-in and sign-out

An app that uses contact bindings should create contact bindings when the user signs in to your web service through your app, and then delete the contact bindings when the user signs out. The code example below shows a helper method, CreateContactBindings, which creates contact bindings based on contact data from the app’s web service. First, the method retrieves a new instance of ContactBindingManager. Next, the app’s proprietary method for retrieving contact information from the server is called. In this example, the server method returns the contact data as a list of MyContactData helper objects. After the server contacts are retrieved, the method loops over each contact and crates a new ContactBinding object with a call to CreateContactBinding. When you call this method, you pass in the remote ID that your service uses to identify the contact. You will use this ID to retrieve data for the bound contact in other contact operations.

Next, populate the contact binding with data from the remote contact, such as the name and email address of the contact. The system uses this data to attempt to match local contacts on the device with remote contacts from your service. The more fields of data you provide, the more likely it is that the system will be able to match a contact. If you save a contact binding for a contact that does not currently exist on the device, the contact binding will be established if that contact is added to the device later. If the supplied data for a remote contact differs from the local contact data and therefore the system fails to associate the contacts, the user can use manual connect to create a binding.

After the ContactBinding is populated, it is saved with a call to SaveContactBindingAsync.

Be sure to add a using directive for the Windows.Phone.PersonalInformation namespace to the file.

private async Task CreateContactBindingsAsync()
{
    ContactBindingManager bindingManager = await ContactBindings.GetAppContactBindingManagerAsync();

    // Simulate call to web service
    List<ServerApi.MyContactData> bindings = ServerApi.GetContactsFromWebServiceAsync();

    foreach (ServerApi.MyContactData binding in bindings)
    {
        ContactBinding myBinding = bindingManager.CreateContactBinding(binding.RemoteId);

        // This information is not displayed on the Contact Page in the People Hub app, but 
        // is used to automatically link the contact binding with existent phone contacts.
        // Add as much information as possible for the ContactBinding to increase the 
        // chances to find a matching Contact on the phone.
        myBinding.FirstName = binding.GivenName;
        myBinding.LastName = binding.FamilyName;
        myBinding.EmailAddress1 = binding.Email;
        myBinding.Name = binding.CodeName;                

        // Don't crash if one binding fails, log the error and continue saving
        try
        {
            await bindingManager.SaveContactBindingAsync(myBinding);
        }
        catch (Exception e)
        {
            Logger.Log("MainPage", "Binding (" + binding.RemoteId + ") failed to save. " + e.Message);
        }
    }

}

The following code example shows the Click handler in MainPage.xaml.cs for a sign-in button. How to implement user authentication in your app is your choice, and is not included in this example. After the user is authenticated, save the user ID and authentication token to IsolatedStorageSettings so other processes in your app can determine if the user is signed in and get access to server resources. Next the sign-in handler calls the CreateContactBindings helper method and then logs the results.

async private void Login_Click(object sender, RoutedEventArgs e)
{
    //// Do sign-in logic here and store your authentication token using the AppSettings class
    App.MyAppSettings.AuthTokenSetting = ServerApi.GetAuthTokenFromWebService();
    App.MyAppSettings.UserIdSetting = ServerApi.GetUserIdFromWebService();


    // Add contact bindings for every contact information that we get from the web service
    // This bindings will be automatically linked to existent phone contacts.
    // The auto-linking is based on name, email or phone numbers match.
    await CreateContactBindingsAsync();

    // log new bindings
    {
        ContactBindingManager manager = await ContactBindings.GetAppContactBindingManagerAsync();
        foreach (ContactBinding binding in await manager.GetContactBindingsAsync())
        {
            Logger.Log("MainPage", "binding = " + binding.RemoteId);
        }
    }
}

In the sign-out Click handler, the existing contact binding’s data is deleted from the system with a call to DeleteAllContactBindingsAsync. Then the method clears the user ID and authentication token from IsolatedStorageSettings to properly reflect the user’s signed-out status.

async private void Logout_Click(object sender, RoutedEventArgs e)
{
    ContactBindingManager bindingManager = await ContactBindings.GetAppContactBindingManagerAsync();
    await bindingManager.DeleteAllContactBindingsAsync();

    // Do sign-out logic here and clear the authentication token using the AppSettings class
    App.MyAppSettings.AuthTokenSetting = "";
    App.MyAppSettings.UserIdSetting = "";

}

Implementing the ShowContact action

The URI mapper in app.xaml.cs, shown in a previous section, navigates the app to a ShowContact.xaml page when the app is launched after a user taps your app’s connect tile for a bound contact. On this page, you can obtain the remote ID you used to create the contact binding, and then use that to retrieve and show your app’s content for the specified content. The following shows an example implementation of the OnNavigatedTo(NavigationEventArgs) method from ShowContact.xaml.cs. In this example, the remote ID of the contact is displayed as text.

protected override async void OnNavigatedTo(NavigationEventArgs e)
{
    base.OnNavigatedTo(e);

    string[] uri = e.Uri.ToString().Split('=');

    if (uri.Length > 1)
    {
        string remoteId = Uri.UnescapeDataString(uri[2]);
        contactInformation.Text = "Displaying contact information for contact with remote ID = " + remoteId;
    }
}

Implementing manual connect for contact bindings

Manual connect is an optional feature that gives the app user the option to manually select one of the contact bindings your app has created to associate with a local device contact. This allows the user to “fix” a binding where the remote contact data supplied was not sufficient for the system to match a local contact. As discussed earlier, when the user taps add apps on the connect pivot, selects your app, and then taps the tile for your app, the system launches your app with a launch URI that the app’s URI mapper navigates to ManualConnect.xaml. This page provides a ListBox that displays all contact bindings for the app. The following XAML defines the ListBox and sets up a TextBlock bound to a property called Name for each data item..

<StackPanel Margin="10,0,0,10">
  <TextBlock Text="Pick the contact you would like to link:" Grid.Row="0"/>
  <ListBox Name="EntityList" Grid.Row="1">
    <ListBox.ItemTemplate>
      <DataTemplate>
        <TextBlock Text="{Binding Name}" FontSize="24"/>
      </DataTemplate>
    </ListBox.ItemTemplate>
  </ListBox>
</StackPanel>

In the OnNavigatedTo method in the code-behind page, ManualConnect.xaml.cs, the binding ID is parsed from the query string, and the URI encoding is removed. This ID is stored in a class variable so it can be accessed later. Next, an instance of the ContactBindingManager class is obtained. All contact bindings that have been created by the app are retrieved using GetContactBindingsAsync. The returned list set as the ItemsSource of the ListBox that was defined in XAML. Finally, a handler for the SelectionChanged is registered.

protected override async void OnNavigatedTo(NavigationEventArgs e)
{
    base.OnNavigatedTo(e);

    string[] uri = e.Uri.ToString().Split('=');

    if (uri.Length > 1)
    {
        bindingId = Uri.UnescapeDataString(uri[2]);
    }

    contactBindingManager = await ContactBindings.GetAppContactBindingManagerAsync();

    EntityList.ItemsSource = await contactBindingManager.GetContactBindingsAsync();

    EntityList.SelectionChanged += EntityList_SelectionChanged;
}

When the user selects a contact binding to associate with the specified local contact, the SelectionChanged event is raised. The handler for this event casts the selected item as a ContactBinding and then calls CreateContactBindingTileAsync to create the contact binding association, passing in the binding ID supplied by the system when it launched the app and the selected contact binding. Finally, the handler navigates to a URI that will be mapped to the ShowContact.xaml page.

async void EntityList_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
    if (EntityList.SelectedItem != null)
    {
        ContactBinding entity = EntityList.SelectedItem as ContactBinding;
        await contactBindingManager.CreateContactBindingTileAsync(bindingId, entity);

        System.Diagnostics.Debug.WriteLine(string.Format("Bound [{0}] to entity with name [{1}]", bindingId, entity.Name));

        NavigationService.Navigate(new Uri(string.Format("/MainPage.xaml?action=Show_Contact&contact_ids={0}", entity.RemoteId), UriKind.Relative));
    }
}

Syncing rich connect tile data in a background agent

You can provide up to three images for each contact binding. The system requests these images from your app on an as-needed basis when the user is viewing the connect pivot for a contact. Your app must implement a Silverlight ScheduledTaskAgent that is launched by the system to perform this task. Note that you cannot use a Windows Runtime background task for this feature. To add a new background agent, in Solution Explorer, right-click your solution, and then click Add->New Project. In the Add New Project dialog, in the left pane, click Windows Phone Apps, and then click the Scheduled Task Agent (Windows Phone Silverlight) project type. You need to reference the name of the new project when you register the background agent. For this example, the new project is named ContactBindingsAgent. Click OK. In the version selector dialog, be sure to select Windows Phone 8.1.

In your main app, add a reference to the background agent project. In Solution Explorer, right-click References, and then click Add Reference. In the Reference Manager dialog, click Solution, and then in the left pane, click Projects. Select the box next to your background agent project, and then click OK.

Update your app’s manifest file to register the background agent with the system. In WMAppManifest.xml, inside the Tasks element, after the DefaultTask element, add the ExtendedTask element. Note that the attribute names should reflect the name you gave your background agent when you created it.

<ExtendedTask Name="BackgroundTask">
  <BackgroundServiceAgent Specifier="ScheduledTaskAgent" Name="ContactBindingsAgent" Source=" ContactBindingsAgent " Type=" ContactBindingsAgent.ScheduledAgent" />
</ExtendedTask>

The following sections walk through the code for the background agent in the ScheduledAgent.cs file created as part of the background agent project. Be sure to add a using directive for the Windows.Phone.PersonalInformation and Windows.Phone.SocialInformation namespaces to the file. Also, declare a class variable for ContactBindingManager.

using Windows.Phone.PersonalInformation;
using Windows.Phone.SocialInformation;
private ContactBindingManager contactBindingManager;

Every background agent needs to override the OnInvoke(ScheduledTask) method. This is the method that the system calls whenever the background agent is triggered. Because the same background agent can be used for multiple tasks, the first thing you need to do is check the name of the ScheduledTask that is passed in. For an online media agent, the task name will be ExtensibilityTaskAgent.  Next, get an instance of the ContactBindingManager by calling GetAppContactBindingManagerAsync. Then, get the list of operations that the operating system is requesting the agent to perform by calling GetOperationQueueAsync. When you have the list, iterate through each operation and handle each one. This example will implement a ProcessOperation helper method that will handle DownloadRichConnectDataOperation.

Important Note:

The DownloadRichConnectDataOperation is sent to the agent every time the user navigates to the connect pivot for a phone contact. However, there’s a minimum of 15 minutes between two consecutive invocations.

async protected override void OnInvoke(ScheduledTask task)
{

    this.contactBindingManager = await ContactBindings.GetAppContactBindingManagerAsync(); 

    // Use the name of the task to differentiate between the ExtensilityTaskAgent 
    // and the ScheduledTaskAgent
    if (task.Name == "ExtensibilityTaskAgent")
    {
        List<Task> inprogressOperations = new List<Task>();

        OperationQueue operationQueue = await SocialManager.GetOperationQueueAsync();
        ISocialOperation socialOperation = await operationQueue.GetNextOperationAsync();

        while (null != socialOperation)
        {
            try
            {
                switch (socialOperation.Type)
                {
                    case SocialOperationType.DownloadRichConnectData:
                        await ProcessOperationAsync(socialOperation as DownloadRichConnectDataOperation);
                        break;

                    default:
                        // This should never happen
                        HandleUnknownOperation(socialOperation);
                        break;
                }

                // The agent can only use up to 20 MB
                // Logging the memory usage information for debugging purposes
                Logger.Log("Agent", string.Format("Completed operation {0}, memusage: {1}kb/{2}kb",
                   socialOperation.ToString(),
                   (int)((long)DeviceExtendedProperties.GetValue("ApplicationCurrentMemoryUsage")) / 1024,
                   (int)((long)DeviceExtendedProperties.GetValue("ApplicationPeakMemoryUsage")) / 1024));


                // This can block for up to 1 minute. Don't expect to run instantly every time.
                socialOperation = await operationQueue.GetNextOperationAsync();
            }
            catch (Exception e)
            {
                Helpers.HandleException(e);
            }
        }

        Logger.Log("Agent", "No more operations in the queue");
    }

    NotifyComplete();
}

Background agents have a memory cap that they must not exceed or they will be terminated. This example uses a helper class to log the memory usage for debugging. When all operations are completed, your agent must let the system know that it is done working by calling NotifyComplete()()().

The ProcessOperationAsync helper method creates a connect tile for each requested contact binding and download up to three images to be displayed on the tile. Because this is a network-intensive operation, a best practice is to perform several network operations in parallel rather than doing them one after another. To help with this, this example uses a helper method, ParallelForEach, which will be shown later in this topic.

The Ids property of the DownloadRichConnectDataOperation object that was passed in by the system contains a list of remote IDs for each contact binding to be updated. For each remote ID, data that will be used to populate the tile for the contact binding is retrieved from the app’s web service. Next, the associated contact binding is loaded by calling GetContactBindingByRemoteIdAsync. A new ConnectTileData object is created and assigned to the TileData property of the contact binding. Next, the title of the tile is set. This is the text that appears below the tile on the connect pivot. For each image that will be assigned to the tile, a new ConnectTileImage is initialized. The image data for the image is set using SetImageAsync, and then the image is added to the Images property of the ConnectTileData object. Then the contact binding, with the new connect tile data, is saved using SaveContactBindingAsync. Finally, the method lets the system know that the operation has been processed by calling NotifyCompletion.

private async Task ProcessOperationAsync(DownloadRichConnectDataOperation operation)
{
    try
    {
        await Helpers.ParallelForEach(operation.Ids, async (string remoteId) =>
            {

                ServerApi.MyTileData myTileData = await ServerApi.GetTileDataFromWebServiceAsync(remoteId);

                ContactBinding binding = await contactBindingManager.GetContactBindingByRemoteIdAsync(remoteId);
                binding.TileData = new ConnectTileData();
                binding.TileData.Title = myTileData.Title;
                foreach (IRandomAccessStream stream in myTileData.Images)
                {
                    ConnectTileImage image = new ConnectTileImage();
                    await image.SetImageAsync(stream);
                    binding.TileData.Images.Add(image);
                }                        
                await contactBindingManager.SaveContactBindingAsync(binding);
                Logger.Log("Agent", "Finish sync for id = " + remoteId);
            });
    }
    catch (Exception e)
    {
        Helpers.HandleException(e);
    }
    finally
    {
        operation.NotifyCompletion();
    }
}

The following code shows the ParallelForEach helper method that executes tasks in parallel up to a default number of six tasks.

public static async Task ParallelForEach<TSource>(IEnumerable<TSource> collection, Func<TSource, Task> work, uint maxTasksRunningInParallel = 6)
{
    List<Task> inprogressTasks = new List<Task>();
    foreach(TSource item in collection)
    {
        // limit the number of simultaneous tasks
        if (inprogressTasks.Count >= maxTasksRunningInParallel)
        {
            Task completed = await Task.WhenAny(inprogressTasks);
            inprogressTasks.Remove(completed);
        }
        inprogressTasks.Add(work(item));
    }

    // wait for all the tasks to complete
    if (inprogressTasks.Count > 0)
    {
        await Task.WhenAll(inprogressTasks);
        inprogressTasks.Clear();
    } 
}