APIMASH Starter Kits: Adding Sharing Capabilities

I’ve been putting the finishing touches on an app that I’m writing based off of the Bing Maps/TomTom Starter Kit I published a few weeks ago. In that app you can view the current image from any of 13,000 traffic cams across the US and Canada, but the Starter Kit itself is designed to allow you to display just about any type of data that has a latitude and longitude component.

As I started thinking about how I would enhance the capabilities of that Starter Kit, the idea to integrate the Share contract seemed like a no-brainer: got a friend, spouse, or significant other about to hit the road? Do a search for the location (the Starter Kit already implements the Search contract), find the camera or cameras in their route and send a quick e-mail with a snapshot of the traffic they’re likely to encounter – something like this:

Starter Kit with sharing

Sharing is a quite powerful way to give your application more exposure (and thereby gain more users), and it's actually quite straightforward to integrate it into any application. Here’s what I had to do to the Starter Kit (see the commit details on GitHub for the nitty gritty details):

  1. Define a callback to respond to the share request.
  2. Connect the callback to the DataRequested event that Windows 8 initiates when there’s a share request for the app.
  3. Test! (here’s where I’ll provide a few tips to save you some time – hopefully!)

Define a callback to respond to the share request

I added the code that follows into LeftPanel.xaml.cs since that control is what’s housing the data I want to share – the image. That said, the data is actually part of the ViewModel for the entire page, so I could have included it there.

Here’s the code, followed by a more-or-less line by line description:

    1:  public async void GetSharedData(DataTransferManager sender, DataRequestedEventArgs args)
    2:  {
    3:      try
    4:      {
    5:          var currentCam = MappableListView.SelectedItem as 
                                                         APIMASH_TomTom.TomTomCameraViewModel;
    6:          if (currentCam != null)
    7:          {
    8:   
    9:              DataRequestDeferral deferral = args.Request.GetDeferral();
   10:   
   11:              args.Request.Data.Properties.Title = 
                                      String.Format("TomTom Camera: {0}", currentCam.CameraId);
   12:              args.Request.Data.Properties.Description = currentCam.Name;
   13:   
   14:              // share a file
   15:              var file = await StorageFile.CreateStreamedFileAsync(
   16:                  String.Format("{0}_{1}.jpg", currentCam.CameraId, 
                                                  DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss")),
   17:                  async stream =>
   18:                  {
   19:                      await stream.WriteAsync(currentCam.ImageBytes.AsBuffer());
   20:                      await stream.FlushAsync();
   21:                      stream.Dispose();
   22:                  },
   23:                  null);
   24:              args.Request.Data.SetStorageItems(new List<IStorageItem> { file });
   25:   
   26:              // share as bitmap
   27:              InMemoryRandomAccessStream raStream = new InMemoryRandomAccessStream();
   28:              await raStream.WriteAsync(currentCam.ImageBytes.AsBuffer());
   29:              await raStream.FlushAsync();
   30:              args.Request.Data.SetBitmap(
                                      RandomAccessStreamReference.CreateFromStream(raStream));
   31:   
   32:              deferral.Complete();
   33:          }
   34:          else
   35:          {
   36:              args.Request.FailWithDisplayText("Select a camera to share its image.");
   37:          }
   38:      }
   39:      catch (Exception ex)
   40:      {
   41:          args.Request.FailWithDisplayText(ex.Message);
   42:      }
   43:  }
Line 1 The signature here is that of the DataRequested event which will be wired up in the main page.
Lines 5-6 This app shares only a camera image, so if there isn’t one currently showing, the user will just see a stock message saying there is nothing to share, followed by the text provided in line 36.
Line 9 Since the implementation that follows has some additional asynchronous work to do, a deferral is obtained and then marked complete (Line 32) when the job is done.
Lines 11 - 12 These lines set the text you’ll see in the Share charm when you first bring it up, and below that will be listed the applications that are capable of handling the data formats this app is sharing. The Title is required; if you do not have it specified, you’ll see the rather generic message: “There was a problem with the data from….”
Lines 14 - 23 This section of rather obtuse code actually has little to do with the sharing itself, but is needed to convert the image data from a byte array (in the ViewModel) to an IStorageItem so it can be proffered to other applications that are able to share that type of content.
Line 24 One of the types of data that you can share is a file, and here is the code that provides the file (in this case a bitmap image) to other apps that are capable of receiving a file or files as part of the share contract..
Lines 27 - 29 This code is somewhat analogous to Lines 14-23, except here the goal is to convert the raw image bytes to a RandomAccessStreamReference, which is what the SetBitmap method needs in order to share the same data as an image (versus a file).
Line 30 Here’s where the same image is shared as a bitmap. Sharing in multiple formats gives you the opportunity to reach out to more applications, since some may accept one format but not another.
Line 32 Now that processing is done, complete the deferral.
Line 36 As mentioned before, this is just a bit of a hint to those that attempt to share when there isn’t a selected camera. Without it the search capability is less discoverable.
Line 41 If something really bad happens, you can provide a little bit of diagnostic help or further directions in the text that appears in the flyout.

That covers the actually sharing of the data - the hard part. All that's left is wiring up this callback so that it fires whenever there is a share request.

Connect the callback to DataRequested event

 DataTransferManager dtm = DataTransferManager.GetForCurrentView();
dtm.DataRequested += LeftPanel.GetSharedData;

Yup, that’s it; TWO lines of code. One that grabs a reference to the DataTransferManager and the other to set the callback to use when data is requested. That callback, of course, is the code discussed above.

The tricky part here is not the code, but rather where to put it. The DataTransferManger is application-wide, and there can be only one event handler attached. Now if the app is going to share the same thing regardless of where in the app the user has navigated, you could setup this callback up in the App object or perhaps some page constructor. In this case though, I didn’t think it made sense to try to share from the search result screen, since the content to be shared (the camera image) wasn’t logically available in that context. 

Instead, these two lines of code appear in the OnNavigatedTo event of MainPage.xaml.cs, and there’s an analogous code couplet that detaches the event handler in the OnNavigatedFrom event.  This frees me up to provide different share implementations for the different pages in the app.

In fact, you may want to provide an implementation for the search results page yourself, even if you don’t share anything from it. If you try out the code, you’ll note a share request initiated while you are on the search results page indicates “This app can’t share.”  Well, that’s not entirely true, but it occurs because there’s no DataRequested callback hooked-up. By adding one that simply calls FailWithDisplayText you can provide a more helpful message to the user.

Test! (and a few lessons-learned).

  • I frequently use the Visual Studio simulator to test my applications, and it took me a while to realize that the Windows 8 Mail application doesn’t play nice as a share target in the Simulator. Of course, that’s one of the first apps you’re likely to try! If you do get the oh-so-helpful message “Something went wrong and this app can’t share right now,” try to run the app targeting the Local Machine. For what it’s worth, all seemed to work well for me when sharing with SkyDrive in the simulator.

  • Be sure to request a deferral if you’re doing asynchronous work when pulling together your content. You might actually want to go the route of using SetDataProvider which is appropriate for scenarios where the data to be shared requires a lot more resources or time to assemble.

  • Dealing with files and streams seems really complicated and painful – that’s not really “sharing” related, but if you are sharing image content then you’ll have to manipulate it into appropriate formats like RandomAccessStreamReferences and IStorageFiles. If you’ve got a lot of familiarity with the .NET System.IO package, you may get even more frustrated because WinRT exposes a different set of asynchronous file-based interfaces that don’t always align to what you’re used to.

    The good news is there are helper extension methods to help bridge from the COM-based synchronous file access you know and love to the new asynchronous implementations. To get at those extension methods, like AsInputStream and AsOutputStream, you’ll need to pull in the WindowsRuntimeStreamExtensions which are part of System.IO. I found the AsBuffer method fairly key too.

  • For a ready-made share target test app, download the Sharing content target app sample and install it. Then when you test your app, it will appear as a share target and display the content you’re providing – like the title, description, file name, etc. There’s a companion Sharing content source app which may be useful as a model for your own code.

  • My Starter KIt app doesn’t participate as a share target (only as a source), but I’ve found a number of people understandbly puzzled at how to test and debug applications that are started as a result of actions in other applications, like sharing or protocol activation. There are a few guidelines here, but the key is to delay starting the debug session by:

    • C#/VB: setting “Do not launch but debug my code when it starts” on the Debug tab of the Windows Store project properties dialog.

C# property page for turning off automatic launchng of debugger

    • C++ and JavaScript: selecting No from the Launch Application list on the Debugging property page

C++ property page for turning off automatic launching of application

Hopefully I've given you some ideas on how to make your own app unique and compelling. Keep in mind that the techniques outlined here can be applied to most, if not all, of the other Starter Kits in the APIMASH project.