Share via


Exercise 3: Using sub-tiles and deep toast notifications

This exercise focuses on features that are new to Windows® Phone 7.1. We have mentioned both this features at the beginning of the previous exercise – sub-tiles and deep toast notifications.

The most noticeable change we will perform during this exercise is changing the Windows® Phone client application to allow it to retain weather information received from the server for each location individually. The client application will have a new main screen allowing the user to choose a location for which to display weather information. This new main page will also allow the user to pin sub-tiles for any of the locations to the start area.

We will use sub-tiles to display information about a specific location in the start area. Clicking the sub-tile will open the application and have it display that location’s weather information.

We will also change the way toast notifications sent from the server work. The new toast notifications, when clicked, will lead to the information page belonging to the location that they relate to.

Task 1 – Making MSPN Registration Screen Independent

The Windows® Phone client application is currently written under the assumption that entire application consists of a single screen. Since we intend to add an additional screen to the client, we must make sure that all application level functionality is independent of any specific screen. All MSPN logic is currently contained in the application’s main screen and in the course of this task we will correct this.

  1. Open the Begin.sln starter solution from the Source\Ex3-SecondaryTilesToastInput\Begin folder of this lab.
  2. Open the file LocationInformation.cs
  3. Change the class to implement the INotifyPropertyChanged interface.

    C#

    public class LocationInformation :INotifyPropertyChanged
    FakePre-246bc601261b466cad9c7c65e42e726d-2fe2456b40d5426f999d47dcc243c0c8FakePre-40dc233178934aa3a3dc9c96ffbf32b8-ea8550946eaf45adbad8f7ea66d11fa3FakePre-c44d46757d6c4c2ca5ef7c488e84874f-71c0223ba91a4782b643f4da7d17ccad

    This class encapsulates all the data about a location associated with a sub-tile: whether or not the location’s tile is pinned to the start area, the location’s name and the temperature at it. We add this class now as MSPN state now needs to contain information about sub-tiles.

  4. Open the file Status.cs.
  5. Change the class to implement the INotifyPropertyChanged interface and to contain the following fields and properties:

    C#

    public class Status : INotifyPropertyChanged { private string message; /// <summary> /// A message representing some status. /// </summary> public string Message { get { return message; } set { if (value != message) { message = value; if (PropertyChanged != null) { PropertyChanged(this, new PropertyChangedEventArgs("Message")); } } } } #region INotifyPropertyChanged Members public event PropertyChangedEventHandler PropertyChanged; #endregion }

    This class simply supports a text field that fires change notifications and we will use it in the next steps to represent changing status messages.

  6. Open the file PushHandler.cs.

    The already added fields and properties are used to establish and keep track of the connection to MSPN and we will see how they are to be used in the following steps. Note the “Locations” property in particular. We will use the “Locations” property to map each location name to information about that location.

  7. Add the following constructor to the class:

    C#

    /// <summary> /// Creates a new instance of the class. /// </summary> /// <param name="pushStatus">Status object to update with push /// related status messages.</param> /// <param name="locationsInformation">Dictionary containing /// information about the locations tracked by the /// application</param> /// <param name="uiDispatcher">A dispatcher to use when updating /// the user interface.</param> public PushHandler (Status pushStatus, Dictionary<string, LocationInformation> locationsInformation, Dispatcher uiDispatcher) { PushStatus = pushStatus; Locations = locationsInformation; Dispatcher = uiDispatcher; }

    This constructor is simply used to initialize some of the class’s properties.

  8. Add the following method to the class:

    C#

    /// <summary> /// Connects to the Microsoft Push Service and registers the /// received channel with the application server. /// </summary> public void EstablishConnections() { connectedToMSPN = false; connectedToServer = false; notificationsBound = false; try { //First, try to pick up existing channel httpChannel = HttpNotificationChannel.Find(channelName); if (null != httpChannel) { connectedToMSPN = true; App.Trace("Channel Exists – no need to create a new one"); SubscribeToChannelEvents(); App.Trace("Register the URI with 3rd party web service"); SubscribeToService(); App.Trace("Subscribe to the channel to Tile and Toast notifications"); SubscribeToNotifications(); UpdateStatus("Channel recovered"); } else { App.Trace("Trying to create a new channel..."); //Create the channel httpChannel = new HttpNotificationChannel(channelName, "HOLWeatherService"); App.Trace("New Push Notification channel created successfully"); SubscribeToChannelEvents(); App.Trace("Trying to open the channel"); httpChannel.Open(); UpdateStatus("Channel open requested"); } } catch (Exception ex) { UpdateStatus("Channel error: " + ex.Message); } }

    This method establishes all necessary connections with the MSPN and WPF server application and greatly resembles the “ConnectToMSPN” method created in Exercise 1, Task 3, Step 2.

  9. Add the following method to the class:

    C#

    private void SubscribeToChannelEvents() { //Register to UriUpdated event - occurs when channel //successfully opens httpChannel.ChannelUriUpdated += new EventHandler<NotificationChannelUriEventArgs>( httpChannel_ChannelUriUpdated); //Subscribed to Raw Notification httpChannel.HttpNotificationReceived += new EventHandler<HttpNotificationEventArgs>( httpChannel_HttpNotificationReceived); //general error handling for push channel httpChannel.ErrorOccurred += new EventHandler<NotificationChannelErrorEventArgs>( httpChannel_ExceptionOccurred); //subscribe to toast notification when running app httpChannel.ShellToastNotificationReceived += new EventHandler<NotificationEventArgs>( httpChannel_ShellToastNotificationReceived); }

    The above method simply registers for events related to the MSPN’s notification channel.

  10. Add the following method to the class:

    C#

    private void SubscribeToService() { //Hardcode for solution - need to be updated in case the REST //WCF service address change string baseUri = "https://localhost:8000/RegirstatorService/Register?uri={0}"; string theUri = String.Format(baseUri, httpChannel.ChannelUri.ToString()); WebClient client = new WebClient(); client.DownloadStringCompleted += (s, e) => { if (null == e.Error) { connectedToServer = true; UpdateStatus("Registration succeeded"); } else { UpdateStatus("Registration failed: " + e.Error.Message); } }; client.DownloadStringAsync(new Uri(theUri)); }

    This method registers with the WPF server as we have previously seen in this lab.

  11. Add the following method to the class:

    C#

    private void SubscribeToNotifications() { ////////////////////////////////////////// // Bind to Toast Notification ////////////////////////////////////////// try { if (httpChannel.IsShellToastBound == true) { App.Trace("Already bound to Toast notification"); } else { App.Trace("Registering to Toast Notifications"); httpChannel.BindToShellToast(); } } catch (Exception ex) { // handle error here } ////////////////////////////////////////// // Bind to Tile Notification ////////////////////////////////////////// try { if (httpChannel.IsShellTileBound == true) { App.Trace("Already bound to Tile Notifications"); } else { App.Trace("Registering to Tile Notifications"); // you can register the phone application to receive // tile images from remote servers [this is optional] Collection<Uri> uris = new Collection<Uri>(); uris.Add(new Uri("https://www.larvalabs.com")); httpChannel.BindToShellTile(uris); } } catch (Exception ex) { //handle error here } notificationsBound = true; }

    This method registers to toast and tile notifications as we have previously seen in the lab.

  12. Add the following methods to the class to serve as the event handlers defined in step 11:

    C#

    void httpChannel_ChannelUriUpdated(object sender, NotificationChannelUriEventArgs e) { connectedToMSPN = true; App.Trace("Channel opened. Got Uri:\n" + httpChannel.ChannelUri.ToString()); App.Trace("Subscribing to channel events"); SubscribeToService(); SubscribeToNotifications(); UpdateStatus("Channel created successfully"); } void httpChannel_ExceptionOccurred(object sender, NotificationChannelErrorEventArgs e) { UpdateStatus(e.ErrorType + " occurred: " + e.Message); } void httpChannel_HttpNotificationReceived(object sender, HttpNotificationEventArgs e) { App.Trace("==============================================="); App.Trace("RAW notification arrived"); Dispatcher.BeginInvoke(() => ParseRAWPayload(e.Notification.Body)); App.Trace("==============================================="); } void httpChannel_ShellToastNotificationReceived(object sender, NotificationEventArgs e) { App.Trace("==============================================="); App.Trace("Toast/Tile notification arrived:"); string msg = e.Collection["wp:Text2"]; App.Trace(msg); UpdateStatus("Toast/Tile message: " + msg); App.Trace("==============================================="); }

    These handlers are analogue to the ones we have defined in the MainPage class earlier in the lab.

  13. Add two additional helper methods to the class:

    C#

    private void UpdateStatus(string message) { Dispatcher.BeginInvoke(() => PushStatus.Message = message); } private void ParseRAWPayload(Stream e) { XDocument document; using (var reader = new StreamReader(e)) { string payload = reader.ReadToEnd().Replace('\0', ' '); document = XDocument.Parse(payload); } XElement updateElement = document.Root; string locationName = updateElement.Element("Location"). Value; LocationInformation locationInfo = Locations[locationName]; App.Trace("Got location: " + locationName); string temperature = updateElement.Element("Temperature"). Value; locationInfo.Temperature = temperature; App.Trace("Got temperature: " + temperature); string weather = updateElement.Element("WeatherType").Value; locationInfo.ImageName = weather; App.Trace("Got weather type: " + weather); }

    The first method simply updates the message that describes the client’s connection status, and the second parses the raw push notifications sent by the WPF server in order to extract weather information about a location.

  14. Open App.xaml.
  15. Add a new element as a child of the Application.Resources tag:

    XAML

    <!--Application Resources--> <Application.Resources> <local:Status x:Key="PushStatus"> <local:Status.Message>Not connected</local:Status.Message> </local:Status> ...

    We have referenced this new resource from code in previous step and use it to display the client’s connection status across the application.

  16. Open App.xaml.cs.
  17. Add the following properties to the App class:

    C#

    /// <summary> /// Whether or not it is necessary to refresh the pin status of /// the secondary tiles. /// </summary> public bool TileRefreshNeeded { get; set; } /// <summary> /// Contains information about the locations displayed by the /// application. /// </summary> public Dictionary<string, LocationInformation> Locations { get; set; } /// <summary> /// Returns the application's dispatcher. /// </summary> public Dispatcher Dispatcher { get { return Deployment.Current.Dispatcher; } } private PushHandler PushHandler { get; set; }

  18. Locate the Application_Launching method and change it to the following:

    C#

    private void Application_Launching(object sender, LaunchingEventArgs e) { TileRefreshNeeded = true; InitializeLocations(); RefreshTilesPinState(); PushHandler = new PushHandler(Resources["PushStatus"] as Status, Locations, Dispatcher); PushHandler.EstablishConnections(); }

    The code above will cause the application to initialize location information upon startup, making sure to check which location has a sub-tile present in the start area and to connect to MSPN and the WPF server using the PushHandler class we have created at the beginning of this task.

  19. Locate the Application_Activated method and change it to the following:

    C#

    private void Application_Activated(object sender, ActivatedEventArgs e) { if (!e.IsApplicationInstancePreserved) { // The application was tombstoned, so restore its state foreach (var keyValue in PhoneApplicationService.Current.State) { Locations[keyValue.Key] = keyValue.Value as LocationInformation; } // Reconnect to the MSPN PushHandler = new PushHandler(Resources["PushStatus"] as Status, Locations, Dispatcher); PushHandler.EstablishConnections(); } else if (!PushHandler.ConnectionEstablished) { // Connection was not fully established before fast app // switching occurred PushHandler.EstablishConnections(); } RefreshTilesPinState(); }

    The code above will cause the application to restore location information from the application’s state object to reestablish its connections in case it has been tombstoned. If the application’s instance was preserved but before properly establishing all connections then another attempt to establish them will be made. Finally, as the user may have removed some of the application’s sub-tiles while it was in the background, the application checks which of its tiles are present in the start area.

  20. Locate the Application_Deactivated method and change it to the following:

    C#

    private void Application_Deactivated(object sender, DeactivatedEventArgs e) { foreach (var keyValue in Locations) { PhoneApplicationService.Current.State[keyValue.Key] = keyValue.Value; } }

    The code above simply stores all location information in the application’s state object in case the application is tombstoned.

  21. Add the following helper methods to the App class:

    C#

    /// <summary> /// Writes debug output in debug mode. /// </summary> /// <param name="message">The message to write to debug output. /// </param> internal static void Trace(string message) { #if DEBUG Debug.WriteLine(message); #endif } /// <summary> /// Initializes the contents of the location dictionary. /// </summary> private void InitializeLocations() { List<LocationInformation> locationList = new List<LocationInformation>(new[] { new LocationInformation { Name = "Redmond", TilePinned = false }, new LocationInformation { Name = "Moscow", TilePinned = false }, new LocationInformation { Name = "Paris", TilePinned = false }, new LocationInformation { Name = "London", TilePinned = false }, new LocationInformation { Name = "New York", TilePinned = false } }); Locations = locationList.ToDictionary(l => l.Name); } /// <summary> /// Sees which of the application's sub-tiles are pinned and /// updates the location information accordingly. /// </summary> private void RefreshTilesPinState() { Dictionary<string, LocationInformation> updateDictionary = Locations.Values.ToDictionary(li => li.Name); foreach (ShellTile tile in ShellTile.ActiveTiles) { string[] querySplit = tile.NavigationUri.ToString().Split('='); if (querySplit.Count() != 2) { continue; } string locationName = Uri.UnescapeDataString(querySplit[1]); updateDictionary[locationName].TilePinned = true; updateDictionary.Remove(locationName); } foreach (LocationInformation locationInformation in updateDictionary.Values) { locationInformation.TilePinned = false; } }

    The “Trace” method above simply writes debug information. “InitializeLocations” fills the location information dictionary with the application’s five locations. “RefreshTilesPinState” goes over all of the application’s active tiles (using the new Windows® Phone 7.1 ShellTile class) and updates all locations according to whether or not their sub-tiles is one of the active tiles.

    Note:
    ShellTile.ActiveTiles returns all of the application’s sub-tiles pinned to the start area in addition to the application’s primary tile, whether or not it is pinned to the start area.

Task 2 – Updating the Client’s Main Page

In the previous task we re-implemented application-wide functionality that was only present in the main page. In this task, we will change the main page so that it allows us to view any of the available locations, to see which locations have sub-tiles in the start area and to directly manipulate the application’s main tile.

  1. Under the folder named Converters open the file BoolToVisibilityConverter.cs
  2. Change the BoolToVisibilityConverter class using the following code:

    C#

    public class BoolToVisibilityConverter : IValueConverter { #region IValueConverter Members public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { return ((bool)value) ? Visibility.Visible : Visibility.Collapsed; } public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { throw new NotImplementedException(); } #endregion }

    This class serves as a value converter which turns Boolean values into element visibility values.

  3. There is another converter to pay attention to: BoolToInverseVisibilityConverter.

    This class serves as a value converter which turns Boolean values into element visibility values, where true values represent collapsed elements.

  4. Open MainPage.xaml from the WPPushNotification.TestClient project.
  5. We now need to make changes in the main page’s code to match the new visual changes, and to remove the MSPN related functionality. Open MainPage.xaml.cs and change the OnNavigatedTo function into MainPage class to the following:

    C#

    #region Navigation protected override void OnNavigatedTo( System.Windows.Navigation.NavigationEventArgs e) { Locations = (App.Current as App).Locations; base.OnNavigatedTo(e); } #endregion

    The new version of the main page seen above simply retrieves the location information dictionary from the application object when the page is navigated to.

  6. Replace the following methods in the main page class, to be used as UI handlers:

    C#

    private void UnpinItem_Click(object sender, RoutedEventArgs e) { LocationInformation locationInformation = (sender as MenuItem).DataContext as LocationInformation; ShellTile tile = ShellTile.ActiveTiles.FirstOrDefault( t => t.NavigationUri.ToString().EndsWith( locationInformation.Name)); if (tile == null) { MessageBox.Show("Tile inconsistency detected. It is suggested that you restart the application."); return; } try { tile.Delete(); locationInformation.TilePinned = false; } catch (Exception ex) { MessageBox.Show(ex.Message, "Error deleting tile", MessageBoxButton.OK); return; } } private void PinItem_Click(object sender, RoutedEventArgs e) { LocationInformation locationInformation = (sender as MenuItem).DataContext as LocationInformation; Uri tileUri = MakeTileUri(locationInformation); StandardTileData initialData = new StandardTileData() { BackgroundImage = new Uri("Images/Clear.png", UriKind.Relative), Title = locationInformation.Name }; ((sender as MenuItem).Parent as ContextMenu).IsOpen = false; try { ShellTile.Create(tileUri, initialData); } catch (Exception ex) { MessageBox.Show(ex.Message, "Error creating tile", MessageBoxButton.OK); return; } } private void ListBox_SelectionChanged(object sender,SelectionChangedEventArgs e) { if (e.AddedItems.Count != 0) { (sender as ListBox).SelectedIndex = -1; NavigationService.Navigate(MakeTileUri(e.AddedItems[0] as LocationInformation)); } } private void ChangeMainTile_Click(object sender,RoutedEventArgs e) { // Get the main tile (it will always be available, even if // not pinned) ShellTile mainTile = ShellTile.ActiveTiles.FirstOrDefault( t => t.NavigationUri.ToString() == "/"); StandardTileData newData = new StandardTileData() { BackgroundImage = new Uri(String.Format("Images/MainTile/{0}.png" ,(listMainTileImage.SelectedItem as ListPickerItem).Content), UriKind.Relative), Title = txtMainTileTitle.Text }; mainTile.Update(newData); }

    The first two methods are handlers for the context menu associated with the location entries we have added to the new main page. These allow to pin and un-pin locations to the start area according to their current state. The “ListBox_SelectionChanged” method navigates to a specific location’s page after the user selects it from the main page and the “ChangeMainTile_Click” method changes the application’s main tile according to selections made through the main page.

  7. Press F5 to compile and run the client application and the WPF server.
  8. Pressing and holding over an unpinned location (one without a tick in the checkbox to its left) will open up a menu that will allow pinning it to the start area. This will immediately move the focus to the start area where the new tile can be displayed. These tiles also lead to the “CityPage” page which does not yet exist:

    Figure 18

    Opening a location’s context menu

    Figure 19

    A sub-tile pinned to the start area

  9. Press the back button () to navigate back to the application and use the new controls at the bottom of the screen to alter the application’s main tile:

    Figure 20

    Updating the main tile

    Figure 21

    The start area after updating the application’s main tile

    Note:
    To see the main tile changing, you must first pin it to the start area by pressing and holding this sample application in the installed application list.

  10. Stop running the client application and the server.

Task 3 – Adding a Specific Location Page and Updating the Server

We now need to add a new page to view information about a specific location (much like the old main page) and to update the server and have it send enhanced tile and toast notifications.

  1. Open CityPage.xaml.cs and add the following function to CityPage class:

    C#

    public partial class CityPage : PhoneApplicationPage { ... protected override void OnNavigatedTo( System.Windows.Navigation.NavigationEventArgs e) { DataContext = (App.Current as App).Locations[ NavigationContext.QueryString["location"]]; base.OnNavigatedTo(e); } }

    This will set the page’s data source to the location specified as a query parameter in the navigation URI.

  2. We have finished altering the client application and will now alter the server. Open MainWindow.xaml.cs under the WPPushNotification.ServerSideWeatherSimulator project, locate the sendToast method and change it to the following (altered code yellow-highlighted):

    C#

    private void sendToast() { string msg = txtToastMessage.Text; txtToastMessage.Text = ""; List<Uri> subscribers = RegistrationService.GetSubscribers(); toastPushNotificationMessage.Title = String.Format("WEATHER ALERT ({0})", cmbLocation.SelectedValue); toastPushNotificationMessage.SubTitle = msg; toastPushNotificationMessage.TargetPage = MakeTileUri(cmbLocation.SelectedValue.ToString()).ToString(); subscribers.ForEach(uri => toastPushNotificationMessage. SendAsync(uri, (result) => OnMessageSent(NotificationType.Toast, result), (result) => { })); }

    This altered method will cause toast messages to lead to the page of the location selected when the message was sent from the server.

  3. Find the sendTile method and change it using the following code (altered code yellow-highlighted):

    C#

    private void sendTile() { string weatherType = cmbWeather.SelectedValue as string; int temperature = (int)(sld.Value + 0.5); string location = cmbLocation.SelectedValue as string; List<Uri> subscribers = RegistrationService.GetSubscribers(); tilePushNotificationMessage.BackgroundImageUri = new Uri("/Images/" + weatherType + ".png", UriKind.Relative); tilePushNotificationMessage.Count = temperature; tilePushNotificationMessage.Title = location; tilePushNotificationMessage.SecondaryTile = MakeTileUri(location).ToString(); subscribers.ForEach(uri => tilePushNotificationMessage. SendAsync(uri, (result) => OnMessageSent(NotificationType.Token, result), (result) => { })); }

    This new version of the method will send the tile notification to the sub-tile matching the notification’s associated location.

  4. Locate the sendRemoteTile method and change it to the following (modified code yellow-highlighted):

    C#

    private void sendRemoteTile() { List<Uri> subscribers = RegistrationService.GetSubscribers(); tilePushNotificationMessage.BackgroundImageUri = new Uri( "https://www.larvalabs.com/user_images/screens_thumbs/12555452181.jpg"); tilePushNotificationMessage.SecondaryTile = null; tilePushNotificationMessage.Title = null; tilePushNotificationMessage.Count = 0; subscribers.ForEach(uri => tilePushNotificationMessage.SendAsync(uri, (result) => OnMessageSent(NotificationType.Token, result), (result) => { })); }

    While this method still functions in the same way, there are now more fields that need to be reset when sending a tile message.

  5. Finally, add the following method to the server:

    C#

    private static Uri MakeTileUri(string locationName) { return new Uri(Uri.EscapeUriString(String.Format( "/CityPage.xaml?location={0}", locationName)), UriKind.Relative); }

    This method is a counterpart for the identically named one we have added to the client application and is used to construct URIs in the same manner.

  6. Press F5 to compile and run the client application and the WPF server. In the windows phone emulator, press the Windows key to show the start area, and then try sending tile notifications from the WPF server. When you send a tile notification for a location pinned to the start area its tile will be updated with the relevant information
  7. Try sending a toast notification and pressing it:

    Figure 22

    A toast notification

    Figure 23

    Clicking the toast notification

  8. Send some raw data regarding the displayed location:

    Figure 24

    The specific location page displaying information

    Note:
     You may have expected the specific location page to already show the information displayed by the location’s sub-tile. This is indeed how a real world application should work. However, for simplicity’s sake, this feature was omitted from the client and server created during this lab. In order to support such a feature, the client application must be able to actively request the most up-to-date data from the server upon activation. The lab installation folder contains a folder named Source\Bonus which contains an implementation of this feature, but we will not cover the changes necessary to perform it.