MVVM

Writing a Cross-Platform Presentation Layer with MVVM

Brent Edwards

Download the Code Sample

With the release of Windows 8 and Windows Phone 8, Microsoft took a big step toward true cross-platform development. Both run on the same kernel now, which means with a little bit of planning, much of your application code can be reused in both. By leveraging the Model-View-ViewModel (MVVM) pattern, some other common design patterns, and a few tricks, you can write a cross-platform presentation layer that will work on both Windows 8 and Windows Phone 8.

In this article, I’ll take a look at some specific cross-platform challenges I faced and talk about solutions that can be applied that allow my app to maintain clean separation of concerns without sacrificing the ability to write good unit tests for it.

About the Sample App

In the July 2013 issue of MSDN Magazine, I presented code from a sample Windows Store application, and the start of an open source, cross-platform framework I developed, called Charmed (“Leveraging Windows 8 Features with MVVM,” msdn.microsoft.com/magazine/dn296512). In this article, I’ll show you how I took that sample application and framework and evolved them to be more cross-­platform. I also developed a companion Windows Phone 8 app with the same base functionality, leveraging the same framework. The framework and sample apps are available on GitHub at github.com/brentedwards/Charmed. The code will continue to evolve as I move toward the final article in my MVVM series, which will delve into actually testing the presentation layer and additional considerations around testable code.

The app is a simple blog reader, called Charmed Reader. Each platform’s version of the app has just enough functionality to illustrate some key concepts relating to cross-platform development. Both versions are similar in the UX they provide, but still fit the look and feel of their respective OS.

Solution Structure

Any proper discussion of cross-platform development with Visual Studio 2012 must start at the beginning: solution structure. While Windows 8 and Windows Phone 8 do run on the same kernel, their apps still compile differently and have different project types. There are various ways to approach creating a solution with different project types, but I prefer to have one solution with all my platform-specific projects included in it. Figure 1 shows the solution structure for the sample apps I’ll be discussing.

Cross-Platform Charmed Reader Solution Structure
Figure 1 Cross-Platform Charmed Reader Solution Structure

Visual Studio lets you have more than one project file reference a single physical class file, allowing you to  add an existing class file by selecting Add As Link, as shown in Figure 2.

Adding an Existing Item with Add As Link
Figure 2 Adding an Existing Item with Add As Link

By leveraging the Add As Link function­ality, I can write much of my code one time and use it for both Windows 8 and Windows Phone 8. However, I don’t want to do this for every class file. As I’ll demonstrate, there are situations where each platform will have its own implementation.

View Differences

Although I’ll be able to reuse much of the code containing my presentation logic, which is written in C#, I won’t be able to reuse the actual presentation code, which is written in XAML. This is because Windows 8 and Windows Phone 8 have slightly different flavors of XAML that don’t play well enough with each other to be interchangeable. Part of the problem is syntax, particularly the namespace declarations, but most of it is due to different controls being available for the platforms as well as different styling concepts being implemented. For example, Windows 8 uses GridView and ListView quite heavily, but these controls aren’t available for Windows Phone 8. On the flip side, Windows Phone 8 has the Pivot control and the LongListSelector, which aren’t available in Windows 8.

Despite this lack of reusability of the XAML code, it’s still possible to use some design assets in both platforms, particularly the Windows Phone UI design. This is because Windows 8 has the concept of Snap View, which has a fixed width of 320 pixels. Snap View uses 320 pixels because mobile designers have been designing for screen widths of 320 pixels for years. In cross-platform development, this works in my favor because I don’t have to come up with a brand-new design for Snap View—I can just adapt my Windows Phone design. Of course, I have to keep in mind that each platform does have its own unique design principles, so I may have to vary a little to make each app feel natural to its platform.

As Figure 3 shows, I implemented the UI for Windows 8 Snap View to be very similar but not quite identical to the UI for Windows Phone 8. Of course, I’m not a designer, and it shows in my thoroughly uninteresting UI design. But I hope this illustrates how similar Windows 8 Snap View and Windows Phone 8 can be in their UI designs.

Sample App UI for Windows 8 Snap View (left) and Windows Phone 8 (right)
Figure 3 Sample App UI for Windows 8 Snap View (left) and Windows Phone 8 (right)

Code Differences

One of the interesting challenges I faced as I embarked on cross-­platform development with Windows 8 and Windows Phone 8 is that each platform handles certain tasks differently. For example, while both platforms follow a URI-based navigation scheme, they vary in the parameters they take. Creating secondary tiles is also different. Though both platforms support them, what happens on each platform when a secondary tile is tapped is fundamentally different. Each platform also has its own way of dealing with application settings, and they have different classes for interacting with these settings.

Finally, there are features Windows 8 has that Windows Phone 8 doesn’t. The main concepts my Windows 8 sample app leverages that Windows Phone 8 doesn’t support are contracts and the Charms menu. This means that Windows Phone doesn’t support the Share or Settings charms.

So, how do you deal with these fundamental code differences? There are a number of techniques you can employ to do so.

Compiler Directives When Visual Studio creates Windows 8 and Windows Phone 8 project types, it automatically defines platform-specific compiler directives in the project settings—­NETFX_CORE for Windows 8 and WINDOWS_PHONE for Windows Phone 8. By leveraging these compiler directives, you can tell Visual Studio what to compile for each platform. Of the techniques that can be employed, this is the most basic, but it’s also the messiest. It results in code that’s a little like Swiss cheese: full of holes. While this is sometimes a necessary evil, there are better techniques that work in a lot of cases.

Abstraction This is the cleanest technique I use to deal with platform differences, and it involves abstracting the platform-­specific functionality into an interface or an abstract class. This technique allows you to provide platform-specific implementations for interfaces and, at the same time, a consistent interface for use throughout the codebase. In cases where there’s helper code each platform-specific implementation can utilize, you can implement an abstract class with this common helper code, then provide platform-specific implementations. This technique requires that the interface or abstract class be available in both projects, via the Add As Link functionality mentioned earlier.

Abstraction Plus Compiler Directives The final technique that can be used is a combination of the previous two. You can abstract the platform differences into an interface or an abstract class, then leverage compiler directives in the actual implementation. This is handy for cases where the platform differences are minor enough that it’s not worth separating them for each project type.

In practice, I’ve found that I rarely use compiler directives on their own, especially in my view models. I prefer to keep my view models clean whenever possible. So, when compiler directives are the best solution, I usually also slip in some abstraction to keep the Swiss cheese a little more hidden.

Navigation

One of the first challenges I encountered in my cross-platform travels was navigation. Navigation isn’t the same in Windows 8 and Windows Phone 8, but it’s close. Windows 8 now uses URI-based navigation, which Windows Phone has been using all along. The difference lies in how parameters are passed. Windows 8 takes a single object as a parameter while Windows Phone 8 takes as many parameters as you like, but through the query string. Because Windows Phone uses the query string, all parameters must be serialized to a string. As it turns out, Windows 8 isn’t so different in this respect.

Although Windows 8 takes a single object as a parameter, that object must somehow be serialized when another app takes center stage and my app gets deactivated. The OS takes the easy route and calls ToString on that parameter, which isn’t all that helpful to me when my app gets activated again. Because I want to bring these two platforms together as much as possible in terms of my development effort, it makes sense to serialize my parameter as a string prior to navigation, then deserialize it after navigation is complete. I can even help facilitate this process with the implementation of my navigator.

Note that I want to avoid directly referencing the views from my view models, so I want the navigation to be view model-driven. My solution is to employ a convention in which views are placed in a Views namespace/folder and view models are placed in a ViewModels namespace/folder. I’ll also make sure my views are named {Something}Page and my view models are named {Something}ViewModel. With this convention in place, I can provide some simple logic to resolve an instance of a view, based on the type of view model.

Now I need to decide what other functionality I need for navigation:

  • View model-driven navigation
  • The ability to go back
  • For Windows Phone 8, the ability to remove a back stack entry

The first two are straightforward. I’ll explain the need to remove a back stack entry a little later, but this ability is built in to Windows Phone 8 and not Windows 8.

Both Windows 8 and Windows Phone 8 leverage classes for their navigation that aren’t easily mocked. Because one of my main goals with these apps is to keep them testable, I want to abstract this code behind a mockable interface. Therefore, my navigator will employ a combination of abstraction and compiler directives. This leaves me with the following interface:

public interface INavigator
{
  bool CanGoBack { get; }
  void GoBack();
  void NavigateToViewModel<TViewModel>(object parameter = null);
#if WINDOWS_PHONE
  void RemoveBackEntry();
#endif // WINDOWS_PHONE
}

Note the use of #if WINDOWS_PHONE. This tells the compiler to compile the RemoveBackEntry into the interface definition only when the WINDOWS_PHONE compiler directive is defined, as it will be for Windows Phone 8 projects. Now here’s my implementation, as shown in Figure 4.

Figure 4 Implementing INavigator

public sealed class Navigator : INavigator
{
  private readonly ISerializer serializer;
  private readonly IContainer container;
#if WINDOWS_PHONE
  private readonly Microsoft.Phone.Controls.PhoneApplicationFrame frame;
#endif // WINDOWS_PHONE
  public Navigator(
    ISerializer serializer,
    IContainer container
#if WINDOWS_PHONE
    , Microsoft.Phone.Controls.PhoneApplicationFrame frame
#endif // WINDOWS_PHONE
    )
  {
    this.serializer = serializer;
    this.container = container;
#if WINDOWS_PHONE
    this.frame = frame;
#endif // WINDOWS_PHONE
  }
  public void NavigateToViewModel<TViewModel>(object parameter = null)
  {
    var viewType = ResolveViewType<TViewModel>();
#if NETFX_CORE
    var frame = (Frame)Window.Current.Content;
#endif // NETFX_CORE
      if (parameter != null)
                             {
#if WINDOWS_PHONE
      this.frame.Navigate(ResolveViewUri(viewType, parameter));
#else
      frame.Navigate(viewType, this.serializer.Serialize(parameter));
#endif // WINDOWS_PHONE
    }
    else
    {
#if WINDOWS_PHONE
      this.frame.Navigate(ResolveViewUri(viewType));
#else
      frame.Navigate(viewType);
#endif // WINDOWS_PHONE
    }
  }
  public void GoBack()
  {
#if WINDOWS_PHONE
    this.frame.GoBack();
#else
    ((Frame)Window.Current.Content).GoBack();
#endif // WINDOWS_PHONE
  }
  public bool CanGoBack
  {
    get
    {
#if WINDOWS_PHONE
      return this.frame.CanGoBack;
#else
      return ((Frame)Window.Current.Content).CanGoBack;
#endif // WINDOWS_PHONE
    }
  }
  private static Type ResolveViewType<TViewModel>()
  {
    var viewModelType = typeof(TViewModel);
    var viewName = viewModelType.AssemblyQualifiedName.Replace(
      viewModelType.Name,
      viewModelType.Name.Replace("ViewModel", "Page"));
    return Type.GetType(viewName.Replace("Model", string.Empty));
  }
  private Uri ResolveViewUri(Type viewType, object parameter = null)
  {
    var queryString = string.Empty;
    if (parameter != null)
    {
      var serializedParameter = this.serializer.Serialize(parameter);
      queryString = string.Format("?parameter={0}", serializedParameter);
    }
    var match = System.Text.RegularExpressions.Regex.Match(
      viewType.FullName, @"\.Views.*");
    if (match == null || match.Captures.Count == 0)
    {
      throw new ArgumentException("Views must exist in Views namespace.");
    }
    var path = match.Captures[0].Value.Replace('.', '/');
    return new Uri(string.Format("{0}.xaml{1}", path, queryString),
      UriKind.Relative);
  }
#if WINDOWS_PHONE
  public void RemoveBackEntry()
  {
    this.frame.RemoveBackEntry();
  }
#endif // WINDOWS_PHONE
}

I need to highlight a couple parts of the Navigator implementation in Figure 4, in particular the use of both the WINDOWS_PHONE and NETFX_CORE compiler directives. This allows me keep the platform-specific code separated within the same code file. I also want to point out the ResolveViewUri method, especially how the query string parameter is defined. To keep things as consistent as possible across the two platforms, I’m allowing only one parameter to be passed. That one parameter will then be serialized and passed along in the platform-specific navigation. In the case of Windows Phone 8, that parameter will be passed via a “parameter” variable in the query string.

Of course, my implementation of navigator is pretty limited, particularly due to the overly simple convention expectation. If you’re using an MVVM library, such as Caliburn.Micro, it can handle the actual navigation for you in a more robust way. However, in your own navigation, you may still want to apply this abstraction-plus-compiler-directives technique to smooth over the platform differences that exist in the libraries themselves.

Application Settings

Application settings are another area where Windows 8 and Windows Phone 8 diverge. Each platform has the ability to save application settings fairly easily, and their implementations are quite similar as well. They differ in the classes they use, but both use classes that aren’t easily mockable, which would break the testability of my view model. So, once again, I’m going to opt for abstraction plus compiler directives. I must first decide what my interface should look like. The interface must:

  • Add or update a setting
  • Try to get a setting, without throwing an exception on failure
  • Remove a setting
  • Determine if a setting exists for a given key

Pretty straightforward stuff, so my interface will be pretty straightforward:

public interface ISettings
{
  void AddOrUpdate(string key, object value);
  bool TryGetValue<T>(string key, out T value);
  bool Remove(string key);
  bool ContainsKey(string key);
}

Because both platforms will have the same functionality, I don’t have to bother with compiler directives in the interface, keeping things nice and clean for my view models. Figure 5 shows my implementation of the ISettings interface.

Figure 5 Implementing ISettings

public sealed class Settings : ISettings
{
  public void AddOrUpdate(string key, object value)
  {
#if WINDOWS_PHONE
    IsolatedStorageSettings.ApplicationSettings[key] = value;
    IsolatedStorageSettings.ApplicationSettings.Save();
#else
    ApplicationData.Current.RoamingSettings.Values[key] = value;
#endif // WINDOWS_PHONE
  }
  public bool TryGetValue<T>(string key, out T value)
  {
#if WINDOWS_PHONE
    return IsolatedStorageSettings.ApplicationSettings.TryGetValue<T>(
      key, out value);
#else
    var result = false;
    if (ApplicationData.Current.RoamingSettings.Values.ContainsKey(key))
    {
      value = (T)ApplicationData.Current.RoamingSettings.Values[key];
      result = true;
    }
    else
    {
      value = default(T);
    }
    return result;
#endif // WINDOWS_PHONE
  }
  public bool Remove(string key)
  {
#if WINDOWS_PHONE
    var result = IsolatedStorageSettings.ApplicationSettings.Remove(key);
    IsolatedStorageSettings.ApplicationSettings.Save();
    return result;
#else
    return ApplicationData.Current.RoamingSettings.Values.Remove(key);
#endif // WINDOWS_PHONE
  }
  public bool ContainsKey(string key)
  {
#if WINDOWS_PHONE
    return IsolatedStorageSettings.ApplicationSettings.Contains(key);
#else
    return ApplicationData.Current.RoamingSettings.Values.ContainsKey(key);
#endif // WINDOWS_PHONE
  }
}

The implementation shown in Figure 5 is pretty straightforward, just like the ISettings interface itself. It illustrates how each platform is only slightly different and requires only slightly different code to add, retrieve and remove application settings. The one thing I will point out is that the Windows 8 version of the code leverages roaming settings—Windows 8-specific functionality that lets an app store its settings in the cloud, allowing users to open the app on another Windows 8 device and see the same settings applied.

Secondary Tiles

As I said earlier, both Windows 8 and Windows Phone 8 support the creation of secondary tiles—tiles that are created programmatically and pinned to the user’s home screen. Secondary tiles provide deep-linking functionality, which means the user can tap a secondary tile and jump straight to a specific part of an application. This is valuable for users because they can essentially bookmark parts of your app and jump right to them without wasting time. In the case of my sample apps, I want the user to be able to bookmark a single blog post (FeedItem) and jump right to it from the home screen.

The interesting thing with secondary tiles is that, while both platforms support them, the way I must implement them for each platform is quite different. This is a perfect case for exploring a more complex abstraction example.

If you read my July 2013 article, you may recall that I talked about how to abstract secondary tiles for MVVM development with Windows 8. The solution I presented works perfectly fine for Windows 8, but doesn’t even compile for Windows Phone 8. What I’ll present here is the natural evolution of that Windows 8 solution, which works well on both Windows 8 and Windows Phone 8.

Here’s what the interface looked like in Windows 8:

public interface ISecondaryPinner
{
  Task<bool> Pin(FrameworkElement anchorElement,
    Placement requestPlacement, TileInfo tileInfo);
  Task<bool> Unpin(FrameworkElement anchorElement,
    Placement requestPlacement, string tileId);
  bool IsPinned(string tileId);
}

As I mentioned, this interface doesn’t compile for Windows Phone 8. Particularly problematic is the use of FrameworkElement and the Placement enumeration. Neither of these are the same in Windows Phone 8 as in Windows 8. My goal is to modify this interface a little so it can be used by both platforms without issue. As you can see, the ISecondaryPinner.Pin method takes as a parameter a TileInfo object. TileInfo is a simple data transfer object (DTO) I created containing relevant information needed for creating a secondary tile. The easiest thing for me to do is to move the parameters the Windows 8 version needs into the TileInfo class, then use compiler directives to compile them into the Windows 8 version of the TileInfo class. Doing so changes my ISecondaryPinner interface to the following:

public interface ISecondaryPinner
{
  Task<bool> Pin(TileInfo tileInfo);
  Task<bool> Unpin(TileInfo tileInfo);
  bool IsPinned(string tileId);
}

You can see that the methods are all still the same, but the parameters for Pin and Unpin have changed slightly. As a result, the TileInfo class has also changed from what it was previously and now looks like this:

public sealed class TileInfo
{
  public string TileId { get; set; }
  public string ShortName { get; set; }
  public string DisplayName { get; set; }
  public string Arguments { get; set; }
  public Uri LogoUri { get; set; }
  public Uri WideLogoUri { get; set; }
  public string AppName { get; set; }
  public int? Count { get; set; }
#if NETFX_CORE
  public Windows.UI.StartScreen.TileOptions TileOptions { get; set; }
  public Windows.UI.Xaml.FrameworkElement AnchorElement { get; set; }
  public Placement RequestPlacement { get; set; }
#endif // NETFX_CORE
}

In reality, I prefer to provide constructors for each scenario in which helper DTOs like this get used, in order to be more explicit about what parameters are needed at what times. For the sake of brevity, I left the different constructors out of the TileInfo code snippet, but you can see them in all their glory in the sample code.

TileInfo now has all the properties needed by both Windows 8 and Windows Phone 8, so the next thing to do is implement the ISecondaryPinner interface. Because the implementation will be quite different for each platform, I’ll use the same interface in both project types but provide platform-specific implementations in each project. This will cut down on the Swiss-cheese effect that compiler directives would cause in this case. Figure 6 shows the Windows 8 implementation of ISecondaryPinner, now with the updated method signatures.

Figure 6 Implementing ISecondaryPinner for Windows 8

public sealed class Win8SecondaryPinner : ISecondaryPinner
{
  public async Task<bool> Pin(TileInfo tileInfo)
  {
    if (tileInfo == null)
    {
      throw new ArgumentNullException("tileInfo");
    }
    var isPinned = false;
    if (!SecondaryTile.Exists(tileInfo.TileId))
    {
      var secondaryTile = new SecondaryTile(
        tileInfo.TileId,
        tileInfo.ShortName,
        tileInfo.DisplayName,
        tileInfo.Arguments,
        tileInfo.TileOptions,
        tileInfo.LogoUri);
      if (tileInfo.WideLogoUri != null)
      {
        secondaryTile.WideLogo = tileInfo.WideLogoUri;
      }
        isPinned = await secondaryTile.RequestCreateForSelectionAsync(
          GetElementRect(tileInfo.AnchorElement), tileInfo.RequestPlacement);
    }
    return isPinned;
  }
  public async Task<bool> Unpin(TileInfo tileInfo)
  {
    var wasUnpinned = false;
    if (SecondaryTile.Exists(tileInfo.TileId))
    {
      var secondaryTile = new SecondaryTile(tileInfo.TileId);
      wasUnpinned = await secondaryTile.RequestDeleteForSelectionAsync(
        GetElementRect(tileInfo.AnchorElement), tileInfo.RequestPlacement);
    }
    return wasUnpinned;
  }
  public bool IsPinned(string tileId)
  {
    return SecondaryTile.Exists(tileId);
  }
  private static Rect GetElementRect(FrameworkElement element)
  {
    GeneralTransform buttonTransform = element.TransformToVisual(null);
    Point point = buttonTransform.TransformPoint(new Point());
    return new Rect(point, new Size(element.ActualWidth,
      element.ActualHeight));
  }
}

It’s important to note that in Windows 8 you can’t quietly create a secondary tile programmatically without approval from the user. This is different from Windows Phone 8, which does let you do so. So, I must create an instance of SecondaryTile and call RequestCreateForSelectionAsync, which will pop up a dialog in the position I provide, prompting the user to approve the creation (or deletion) of the secondary tile. The helper method GetElementRect takes in a FrameworkElement—which will be the button the user presses to pin the secondary tile—and then calculates its rectangle to be used for positioning the request dialog.

Figure 7 shows the Windows Phone 8 implementation of ISecondaryPinner.

Figure 7 Implementing ISecondaryPinner for Windows Phone 8

public sealed class WP8SecondaryPinner : ISecondaryPinner
{
  public Task<bool> Pin(TileInfo tileInfo)
  {
    var result = false;
    if (!this.IsPinned(tileInfo.TileId))
    {
      var tileData = new StandardTileData
      {
        Title = tileInfo.DisplayName,
        BackgroundImage = tileInfo.LogoUri,
        Count = tileInfo.Count,
        BackTitle = tileInfo.AppName,
        BackBackgroundImage = new Uri("", UriKind.Relative),
        BackContent = tileInfo.DisplayName
      };
      ShellTile.Create(new Uri(tileInfo.TileId, UriKind.Relative), 
        tileData);
      result = true;
    }
  return Task.FromResult<bool>(result);
  }
  public Task<bool> Unpin(TileInfo tileInfo)
  {
    ShellTile tile = this.FindTile(tileInfo.TileId);
    if (tile != null)
    {
      tile.Delete();
    }
    return Task.FromResult<bool>(true);
  }
  public bool IsPinned(string tileId)
  {
    return FindTile(tileId) != null;
  }
  private ShellTile FindTile(string uri)
  {
    return ShellTile.ActiveTiles.FirstOrDefault(
      tile => tile.NavigationUri.ToString() == uri);
  }
}

There are a couple of things I’d like to point out in the Windows Phone 8 implementation. The first is how the secondary tile is created using the StandardTileData class and the static ShellTile.Create method. The second is that the Windows Phone 8 implementation for creating secondary tiles is not asynchronous. Because the Windows 8 implementation is asynchronous, I had to make the interface support the async/await pattern. Luckily, it’s very easy to make what would otherwise be a non-asynchronous method support the async/await pattern by leveraging the static, generic method Task.FromResult. The view models that use the ISecondaryPinner interface don’t need to worry about the fact that Windows 8 is inherently asynchronous and Windows Phone 8 is not.

You can now see how Windows 8 (Figure 6) and Windows Phone 8 (Figure 7) vary in their implementation of secondary tiles. That’s not the end of the story for secondary tiles, however. I’ve shown only the implementation of the ISecondaryPinner interface. Because each platform is different and must provide values for different properties of the TileInfo class, I must also provide platform-specific implementations of the view models that use them. For my sample application, I’ll provide the ability to pin a single blog post, or FeedItem, so the view model in question is the FeedItemViewModel.

Some common functionality does exist in both Windows 8 and Windows Phone 8, from the view model’s perspective. When the user pins a FeedItem, I want both platforms to save that FeedItem locally so it can be reloaded when the user taps its secondary tile. On the flip side, when the user unpins a FeedItem, I want both platforms to delete that FeedItem from local storage. Both platforms will need to implement this common functionality, yet expand on it with platform-specific implementations for the secondary tile functionality. So, it makes sense to provide a base class that implements the common functionality and make that base class available on both platforms. Then, each platform can inherit that base class with platform-specific classes that provide the platform’s specific implementations for pinning and unpinning secondary tiles.

Figure 8 shows the FeedItemViewModel, which is the base class to be inherited by both platforms. FeedItemViewModel contains all of the stuff that’s common to both platforms.

Figure 8 FeedItemViewModel Base Class

public abstract class FeedItemViewModel : ViewModelBase<FeedItem>
{
  private readonly IStorage storage;
  protected readonly ISecondaryPinner secondaryPinner;
  public FeedItemViewModel(
    ISerializer serializer,   
    IStorage storage,
    ISecondaryPinner secondaryPinner)
    : base(serializer)
  {
    this.storage = storage;
    this.secondaryPinner = secondaryPinner;
  }
  public override void LoadState(FeedItem navigationParameter,
    Dictionary<string, object> pageState)
  {
    this.FeedItem = navigationParameter;
  }
  protected async Task SavePinnedFeedItem()
  {
    var pinnedFeedItems =
      await this.storage.LoadAsync<List<FeedItem>>(
      Constants.PinnedFeedItemsKey);
    if (pinnedFeedItems == null)
    {
      pinnedFeedItems = new List<FeedItem>();
    }
    pinnedFeedItems.Add(feedItem);
    await this.storage.SaveAsync(Constants.PinnedFeedItemsKey, 
      pinnedFeedItems);
  }
  protected async Task RemovePinnedFeedItem()
  {
    var pinnedFeedItems =
      await this.storage.LoadAsync<List<FeedItem>>(
      Constants.PinnedFeedItemsKey);
    if (pinnedFeedItems != null)
    {
       var pinnedFeedItem = pinnedFeedItems.FirstOrDefault(fi => fi.Id ==
         this.FeedItem.Id);
       if (pinnedFeedItem != null)
       {
         pinnedFeedItems.Remove(pinnedFeedItem);
       }
      await this.storage.SaveAsync(Constants.PinnedFeedItemsKey, 
        pinnedFeedItems);
    }
  }
  private FeedItem feedItem;
  public FeedItem FeedItem
  {
    get { return this.feedItem; }
    set { this.SetProperty(ref this.feedItem, value); }
  }
  private bool isFeedItemPinned;
  public bool IsFeedItemPinned
  {
    get { return this.isFeedItemPinned; }
    set { this.SetProperty(ref this.isFeedItemPinned, value); }
  }
}

With a base class in place to facilitate saving and deleting pinned FeedItems, I can move on to platform-specific implementations. The concrete implementation of FeedItemViewModel for Windows 8 and the use of the TileInfo class with the properties that Windows 8 cares about is shown in Figure 9.

Figure 9 Concrete Implementation of FeedItemViewModel for Windows 8

public sealed class Win8FeedItemViewModel : FeedItemViewModel
{
  private readonly IShareManager shareManager;
  public Win8FeedItemViewModel(
    ISerializer serializer,
    IStorage storage,
    ISecondaryPinner secondaryPinner,
    IShareManager shareManager)
    : base(serializer, storage, secondaryPinner)
  {
  this.shareManager = shareManager;
  }
  public override void LoadState(Models.FeedItem navigationParameter,
    Dictionary<string, object> pageState)
  {
    base.LoadState(navigationParameter, pageState);
    this.IsFeedItemPinned =
      this.secondaryPinner.IsPinned(FormatSecondaryTileId());
  }
  public override void SaveState(Dictionary<string, object> pageState)
  {
    base.SaveState(pageState);
    this.shareManager.Cleanup();
  }
  public async Task Pin(Windows.UI.Xaml.FrameworkElement anchorElement)
  {
    // Pin the feed item, then save it locally to make sure it is still
    // available when they return.
    var tileInfo = new TileInfo(
      this.FormatSecondaryTileId(),
      this.FeedItem.Title,
      this.FeedItem.Title,
      Windows.UI.StartScreen.TileOptions.ShowNameOnLogo |
        Windows.UI.StartScreen.TileOptions.ShowNameOnWideLogo,
      new Uri("ms-appx:///Assets/Logo.png"),
      new Uri("ms-appx:///Assets/WideLogo.png"),
      anchorElement,
      Windows.UI.Popups.Placement.Above,
      this.FeedItem.Id.ToString());
    this.IsFeedItemPinned = await this.secondaryPinner.Pin(tileInfo);
    if (this.IsFeedItemPinned)
    {
       await SavePinnedFeedItem();
    }
  }
  public async Task Unpin(Windows.UI.Xaml.FrameworkElement anchorElement)
  {
    // Unpin, then delete the feed item locally.
  var tileInfo = new TileInfo(this.FormatSecondaryTileId(), anchorElement,
    Windows.UI.Popups.Placement.Above);
    this.IsFeedItemPinned = !await this.secondaryPinner.Unpin(tileInfo);
    if (!this.IsFeedItemPinned)
    {
      await RemovePinnedFeedItem();
    }
  }
  private string FormatSecondaryTileId()
  {
    return string.Format(Constants.SecondaryIdFormat, this.FeedItem.Id);
  }
}

Figure 10 shows the concrete implementation of FeedItemViewModel for Windows Phone 8. Using TileInfo for Windows Phone 8 requires fewer properties than for Windows 8.

Figure 10 Concrete Implementation of FeedItemViewModel for Windows Phone 8

public sealed class WP8FeedItemViewModel : FeedItemViewModel
{
  public WP8FeedItemViewModel(
    ISerializer serializer,
    IStorage storage,
    ISecondaryPinner secondaryPinner)
    : base(serializer, storage, secondaryPinner)
  {
  }
  public async Task Pin()
  {
    // Pin the feed item, then save it locally to make sure it is still
    // available when they return.
    var tileInfo = new TileInfo(
      this.FormatTileIdUrl(),
      this.FeedItem.Title,
      Constants.AppName,
      new Uri("/Assets/ApplicationIcon.png", UriKind.Relative));
    this.IsFeedItemPinned = await this.secondaryPinner.Pin(tileInfo);
    if (this.IsFeedItemPinned)
    {
      await this.SavePinnedFeedItem();
    }
  }
  public async Task Unpin()
  {
    // Unpin, then delete the feed item locally.
    var tileInfo = new TileInfo(this.FormatTileIdUrl());
    this.IsFeedItemPinned = !await this.secondaryPinner.Unpin(tileInfo);
    if (!this.IsFeedItemPinned)
    {
      await this.RemovePinnedFeedItem();
    }
  }
  private string FormatTileIdUrl()
  {
    var queryString = string.Format("parameter={0}", FeedItem.Id);
    return string.Format(Constants.SecondaryUriFormat, queryString);
  }
}

After the Windows 8 (Figure 9) and Windows Phone 8 (Figure 10) implementations for FeedItemViewModel, my apps are all set to pin secondary tiles to their respective home screens. The only thing remaining to close the loop on the functionality is to handle when the user actually taps the pinned secondary tiles. My goal for both of the apps is to launch the app straight into whatever blog post the secondary tile represents, but allow the user to press the back button and be brought to the main page listing all the blogs, rather than back out of the app itself.

From the Windows 8 perspective, nothing changes from what I talked about in the July 2013 article. Figure 11 shows the unchanged Windows 8 code for handling when the app is launched, which is a snippet from the App.xaml.cs class file.

Figure 11 Launching the App from Windows 8

protected override async void OnLaunched(LaunchActivatedEventArgs args)
{
  Frame rootFrame = Window.Current.Content as Frame;
  if (rootFrame.Content == null)
  {
    Ioc.Container.Resolve<INavigator>().
      NavigateToViewModel<MainViewModel>();
  }
  if (!string.IsNullOrWhiteSpace(args.Arguments))
  {
    var storage = Ioc.Container.Resolve<IStorage>();
    List<FeedItem> pinnedFeedItems =
      await storage.LoadAsync<List<FeedItem>>(Constants.PinnedFeedItemsKey);
    if (pinnedFeedItems != null)
    {
      int id;
      if (int.TryParse(args.Arguments, out id))
      {
        var pinnedFeedItem = pinnedFeedItems.FirstOrDefault(fi => fi.Id == id);
        if (pinnedFeedItem != null)
        {
          Ioc.Container.Resolve<INavigator>().           
             NavigateToViewModel<FeedItemViewModel>(pinnedFeedItem);
        }
      }
    }
  }
  Window.Current.Activate();
}

It’s a little trickier with Windows Phone 8. In this case, the secondary tile takes a uri, rather than just parameters. That means that Window Phone 8 can automatically launch my app into whatever page I want, without going through a centralized launching point first, as Windows 8 does. Because I want to provide a consistent experience across both platforms, I created a centralized launching point of my own for Windows Phone 8, which I called SplashViewModel, shown in Figure 12. I set up my project to launch through it whenever the app launches, via a secondary tile or not.

Figure 12 SplashViewModel for Windows Phone 8

public sealed class SplashViewModel : ViewModelBase<int?>
{
  private readonly IStorage storage;
  private readonly INavigator navigator;
  public SplashViewModel(
    IStorage storage,
    INavigator navigator,
    ISerializer serializer)
    : base(serializer)
  {
    this.storage = storage;
    this.navigator = navigator;
  }
  public override async void LoadState(
    int? navigationParameter, Dictionary<string, object> pageState)
  {
    this.navigator.NavigateToViewModel<MainViewModel>();
    this.navigator.RemoveBackEntry();
    if (navigationParameter.HasValue)
    {
      List<FeedItem> pinnedFeedItems =
        await storage.LoadAsync<List<FeedItem>>(Constants.PinnedFeedItemsKey);
      if (pinnedFeedItems != null)
      {
        var pinnedFeedItem =
          pinnedFeedItems.FirstOrDefault(fi => 
            fi.Id == navigationParameter.Value);
        if (pinnedFeedItem != null)
        {
  this.navigator.NavigateToViewModel<FeedItemViewModel>(pinnedFeedItem);
        }
      }
    }
  }
}

The code for SplashViewModel is pretty straightforward. The one thing I will point out is that I want to make sure to remove this page from the back stack—otherwise it would keep users from ever being able to back out of the app. They would constantly be sent back into the app every time they backed up to this page. This is where the valuable addition to INavigator for Windows Phone 8 comes into play: RemoveBackEntry. After navigating to the MainViewModel, I call RemoveBackEntry to take the splash page out of the back stack. It’s now a one-time-use page only for when the app is launched.

Wrapping Up

In this article, I discussed cross-platform development with Windows 8 and Windows Phone 8. I talked about what can be reused between platforms (designs and some code) and what can’t (XAML). I also talked about some of the challenges faced by developers who work on cross-platform apps, and I presented several solutions to those challenges. My hope is that these solutions can be applied to more than just the specific cases of the navigation, application settings and secondary tiles I talked about. These solutions can help keep your view models testable by providing the ability to abstract out some of the OS interactions and to use interfaces to make these interactions mockable.

In my next article, I’ll look more specifically at the actual unit testing of these cross-platform applications, now that they’re all set up to be tested. I’ll talk more about why I made some of the decisions I made, as they relate to testing, as well as how I actually go about unit testing the apps.

By approaching cross-platform development with the intent of providing similar UXs on both platforms, and by doing a little planning in advance, I can write apps that maximize code reuse and promote unit testing. I can leverage platform-specific features, such as the Windows 8 Charms menu, without sacrificing the experience each platform offers.


Brent Edwards is an associate principal consultant for Magenic, a custom application development firm that focuses on the Microsoft stack and mobile application development. He’s also a cofounder of the Twin Cities Windows 8 User Group in Minneapolis, Minn. Reach him at brente@magenic.com.

THANKS to the following technical expert for reviewing this article: Jason Bock (Magenic)

 

Rate: