Cross Platform C#

Realities of Cross-Platform Development: How Platform-Specific Can You Go?

The goal of one tool for every platform isn't quite a reality yet, but you can get close. Here's what you can accomplish so far with Xamarin tools.

Also read:

My personal beliefs on cross-platform development were formed in November 1993. I worked at The Coca-Cola Company at the time, and a few colleagues and I were discussing how to provide Mac users with the same set of applications that we were building on Windows 3.1 with PowerBuilder.

The discussion centered around the UI. The concern was with providing a Windows-centric interface to Mac users. I remember one of the great lines from the meeting: "If you provide Windows help to a user expecting Mac balloon help, you are going to have users that hate you." After this and some personal experiments, I came to agree with this viewpoint. If developers want to make users as happy as possible, they need to call the native APIs of the platform.

Fast-forward to 2009, when I fell in love with the Xamarin iOS product from the first announcement. Xamarin had created a way for C# developers to use the Microsoft .NET Framework and to call into the native platform APIs. Xamarin Android operated the same way. While I would often discuss the need for a cross-platform UI with Xamarin management, I never forgot the lessons of cross-platform from many years ago. At the same time, I knew that there was a need for a cross-platform toolset to create an application. I had talked to enough people to understand the pain and agony. Quite honestly, I was a fence sitter in this area. I liked the idea of XF, but I was never quite ready to make the jump and, honestly, try XF for anything more than some personal experiments and helping a couple of customers.

The problem with cross-platform tools is that they tend to target the lowest common denominator. This commoditizes the UI. This isn't necessarily a bad thing. Time to market can be improved. There are lots of applications that don't need a fully optimized platform. There are a number of applications where getting something out quickly and solving a business problem can be the most important factor.

There's no "one size fits all" solution no matter what some faction wants to preach. Some applications have very device-specific needs -- some applications don't -- it very much depends on the situation. Users, the ones who pay for development, tend to want applications that look, smell and taste like every other application that they already have. The worst possible situation to get yourself into is the one where you start down the road of using a cross-platform framework, and the users demand so many features that are platform-specific that it would've been easier and cheaper to have just built two or more platform-specific applications. That is a special type of hell. I've been there, and it's a bad conversation to have with a paying customer. The bottom line is that you need to have a real-world discussion with your users and the people that sign the checks. No one will quite understand it, but morally, you really need to have this conversation in the consulting world.

To help solve the cross-platform problem, Xamarin has produced Xamarin.Forms and added this product to the list of tools for developers. XF is a cross-platform API that maps to device-specific APIs. This article will delve into an examination of Xamarin.Forms with an application that I had to write for our startup and how well it meets several requirements:

  • How well does it display on a platform? How much platform-specific code is necessary? Does the application "look" like a native platform?
  • When a developer has to resort to native API calls, how easy is it? Can it be done? Does it work?
  • When something else has to happen, how much third-party support is there?

What Apps Will Work XF? There's lots of discussion regarding
what types of apps work with XF. What types of apps should a developer target with XF? There's a set of apps that work well, and there's a set of apps that I'd be concerned about using with XF. Apps that work well with XF include:

  • Internal company applications. Internal apps tend to be heavily data-bound. Solving the business problem is the most important thing.
  • Applications that don't need to customize the UI. The more customization of the UI, the less an XF app makes sense.

What Did I Build?
I work with startups and am currently working on a startup golf app. We wanted to be able to take pictures of teams at a charity event and be able to add them to the system at a moment's notice. The needs of the application were to:

  • Log in to the server remotely and validate that the user connected has the appropriate security level to add images to the system. Logging into the service is easy. It involves running over HTTPS and calling a REST/JSON endpoint. This can easily be done in any .NET flavor using the HttpClient.
  • Use the built-in camera to take pictures. For XF, this can be problematic. XF has been designed to provide easy data binding and not necessarily to integrate with device features, such as the camera.
  • Minimize the images to effectively upload to the server. iOS and Android provide native mechanisms to compress images. How does an XF application call these native device APIs?
  • Save the images on the server to the appropriate storage location. The application needs to call Web services. In our situation, we needed to be able to use a REST-based Web service and use JSON to perform the necessary communication.
  • Display the images for a team and delete the images for a team as necessary. Images would be displayed in a list. The list needed to handle deleting the images when a user directs the image to be deleted. If multiple users uploaded images, each one needed to see the other's additions. How easy is it to implement pull to refresh, as well as swiping to delete?

Integrating with the Camera
The first and biggest issue to determine was how the application would integrate with the camera. If there's no way to take pictures, there's no XF version of the application. I would use iOS or Android and just roll with it. Thankfully, there is a solution to this problem. There are at least two options (and perhaps more) to taking pictures that I have found:

  • Xamarin.Forms Labs. XF Labs provides a large number of controls and device-specific properties.
  • Media Plugin by James Montemagno. The Media Plugin provides a simple set of APIs to integrate with audio and video. The API is modeled after Xamarin.Mobile. For this application, I used James Montemagno's Media Plugin.

Let's jump into some code to take pictures by taking a look at Listing 1.

Listing 1: Method for Taking Pictures
public class TeamPicPage : ContentPage
{
Int64 _TeamId, _TournamentId, _ClubId;
public TeamPicPage(Int64 TeamId)
{
  _TeamId = TeamId;
  this.Title = "Team Pic";
  Button btnTakePicture, btnPickPicture;
  if (!CrossMedia.Current.IsCameraAvailable || !CrossMedia.Current.IsTakePhotoSupported)
  {
    DisplayAlert("No Camera", ":( No camera available.", "OK");
    return;
  }
  btnTakePicture = new Button();
  btnPickPicture = new Button();
  btnTakePicture.Text = "Take Picture with Camera";
  btnPickPicture.Text = "Select Picture from Gallery";
  btnTakePicture.Clicked += BtnTakePicture_Clicked;
  btnPickPicture.Clicked += BtnPickPicture_Clicked;
  Content = new StackLayout
  {
    Children = {
      btnTakePicture, btnPickPicture
    }
    };
  var app = (GolfGameClubApp.App)App.Current;
  var tourns = app.Tournaments;
  _ClubId = app.ClubId.Value;
  _TournamentId = app.TournamentId.Value;
}
async private void BtnPickPicture_Clicked(object sender, EventArgs e)
{
  var file = await CrossMedia.Current.PickPhotoAsync();
  if (file == null)
  {
    return;
  }
  byte[] bytes;
  var strm = file.GetStream();
  using (MemoryStream ms = new MemoryStream())
  {
    strm.CopyTo(ms);
    bytes = ms.ToArray();
  }
  bytes = DependencyService.Get<Interfaces.IImageResize>().ResizeImage(bytes);
  await GolfGameClubApp.WebServices.ws.TeamPictureSave(_ClubId, _TournamentId, _TeamId, bytes);
  await UpdateData();
  MessagingCenter.Send<string>("update", "UpdateImages");
}
async private void BtnTakePicture_Clicked(object sender, EventArgs e)
{
  var file = await CrossMedia.Current.TakePhotoAsync(
    new Plugin.Media.Abstractions.StoreCameraMediaOptions
  {
    Directory = "AutoCard",
    Name = String.Format("{0}.jpg", Guid.NewGuid())
  });

  if (file == null)
    return;

  byte[] bytes;
  var strm = file.GetStream();
  using (MemoryStream ms = new MemoryStream())
  {
    strm.CopyTo(ms);
    bytes = ms.ToArray();
  }
  bytes = DependencyService.Get<Interfaces.IImageResize>().ResizeImage(bytes);
  await GolfGameClubApp.WebServices.ws.TeamPictureSave(_ClubId, _TournamentId, _TeamId, bytes);
  await UpdateData();
  MessagingCenter.Send<string>("update", "UpdateImages");
}
async private Task UpdateData()
{
  var app = (GolfGameClubApp.App)App.Current;
  var ClubId = app.ClubId.Value;
  var token = app.UserToken;
  var data = await WebServices.ws.TournamentsInClub(token, ClubId);
  app.Tournaments = new ObservableCollection<Models.TournamentInfo>(data);
  MessagingCenter.Send<string>("update", "UpdateImages");
}
}

In this code, there are two buttons that are displayed to the user on the page. One button will allow the user to take a picture with the camera. The other button will allow the user to grab an image from the local photo gallery on the device. Once the user has picked an image, it is converted into a byte array. The byte array is then passed to the device-specific routines to compress an image. Once the code returns to the XF routines, a call is made to a static method to call a Web service to upload an image. After the image is uploaded, a call is made to update the local copy of golf team images. Finally, a message is sent within the application to request that any other places within the application that need to update their UI with new data do so.

Note: The UI is built programmatically. While XAML has a lot of love from die-hard Windows developers, the XF design surface for XAML is currently in public alpha testing. It was merely easier to create the UI programmatically.

iOS code to compress an image appears in Listing 2. Note that this code is contained within the iOS-specific project that is part of the XF solution.

Listing 2: Compressing an Image for iOS
using System;
using System.Collections.Generic;
using System.Text;

using Foundation;
using UIKit;
using CoreImage;

using Xamarin.Forms;
using Xamarin.Forms.Platform.iOS;

[assembly: Dependency(typeof(GolfGameClubApp.iOS.DependencyServices.ImageResizing))]
namespace GolfGameClubApp.iOS.DependencyServices
{
  public class ImageResizing : GolfGameClubApp.Interfaces.IImageResize
  {
    public byte[] ResizeImage(byte[] imageBytes)
    {
      if(imageBytes.Length > 0)
      {
        var dataIn = NSData.FromArray(imageBytes);
        var imgOriginal = UIKit.UIImage.LoadFromData(dataIn);
        var currentSize = imgOriginal.Size;
        var maxWidth = 1024.0;
        if (currentSize.Width > maxWidth)
        {
          var scaledHeight = currentSize.Height * maxWidth / currentSize.Width;
          var jpgSize = imgOriginal.AsJPEG().Length;
          UIGraphics.BeginImageContext(new CoreGraphics.CGSize(maxWidth, scaledHeight));
          imgOriginal.Draw(new CoreGraphics.CGRect(0, 0, maxWidth, scaledHeight));
          imgOriginal = UIGraphics.GetImageFromCurrentImageContext();
          UIGraphics.EndImageContext();
          return imgOriginal.AsJPEG(new nfloat(.5f)).ToArray();
        }
        return imageBytes;
      }
      else
      {
        return imageBytes;
      }
    }
  }
}
Listing 3 shows Android-specific code to do the same.
Listing 3: Compressing an Image for Android
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using Android.App;
using Android.Content;
using Android.Graphics;
using Android.OS;
using Android.Runtime;
using Android.Views;
using Android.Widget;

using Xamarin.Forms;
using Xamarin.Forms.Platform.Android;

using GolfGameClubApp;

[assembly: Dependency(typeof(GolfGameClubApp.Droid.DependencyServices.ImageResizing))]
namespace GolfGameClubApp.Droid.DependencyServices
{
  public class ImageResizing : Interfaces.IImageResize
  {
    public byte[] ResizeImage(byte[] imageBytes)
    {
      if(imageBytes.Length == 0)
      {
        return imageBytes;
      }
      var bmp = BitmapFactory.DecodeByteArray(imageBytes, 0, imageBytes.Length);
      var maxWidth = 1024.0;
      var imgWidth = Convert.ToDouble(bmp.Width);
      if (imgWidth > maxWidth)
      {
        var newHeight = maxWidth * bmp.Height / imgWidth;
        var scaledBmp = Bitmap.CreateScaledBitmap(bmp,  
          Convert.ToInt32(maxWidth), Convert.ToInt32(newHeight), false);

        byte[] bitmapData;
        using (var stream = new System.IO.MemoryStream())
        {
          scaledBmp.Compress(Bitmap.CompressFormat.Jpeg, 50, stream);
          bitmapData = stream.ToArray();
          return bitmapData;
        }
      }
      return imageBytes;
    }
  }
}

The goal of each of the platform-specific image resizers is to:

  • Resize the image to the point where it's 1024 pixels wide. This is merely an arbitrary value that was used within the application.
  • Compress the JPEG image a bit more. JPEG is a lossy compression format, so it allows images to be compressed more than just on its size.

The reason to compress the images was to send the smallest reasonable image to the device.

Displaying Team Images
We wanted to display the images for a team and in the application, each team could have more than one image. Therefore, I decided to use the XF ListView. The XF ListView is a grid that's used to display data. It's the same as a ListView in Android (see Figure 1) or a UITableiView in iOS (see Figure 2) and it has several built-in cells. This application uses an inherited version of the ImageCell.

[Click on image for larger view.] Figure 1. Images ListView on Android in Genymotion Simulator
[Click on image for larger view.] Figure 2. ListView on iPhone Simulator

The code in Listing 4 does several things:

  • Displays a list of images currently within the database for a team.
  • In the case of other people having already added images for a team, implements pull to refresh to get the updated images.
  • Users get confused easily, so there was a need to handle the ability to delete an image. Ideally, we wanted to delete images inside the ListView (Figure 3 in Android and Figure 4 in iOS).
[Click on image for larger view.] Figure 3. Deleting an Image in Android Emulator
[Click on image for larger view.] Figure 4. Deleting an Image in iPhone Simulator
Listing 4: Displaying Images for a Team
public class TeamPictures : ContentPage
{
Int64 _TeamId;
ListView lv;
public TeamPictures(Int64 TeamId)
{
  _TeamId = TeamId;
  this.Title = "Team Pics";
  lv = new ListView();
  var cellTemplate = new DataTemplate(typeof(MyTeamPicCell));
  cellTemplate.SetBinding(ImageCell.ImageSourceProperty, "ImageUrl");
  cellTemplate.SetBinding(ImageCell.CommandParameterProperty, "TeamPictureId");
  lv.ItemTemplate = cellTemplate;
  lv.IsPullToRefreshEnabled = true;
  lv.RefreshCommand = new Command ((act) => {
    var app = (GolfGameClubApp.App)App.Current;
    var token = app.UserToken;
    var ClubId = app.ClubId.Value;
    WebServices.ws.TournamentsInClub(token, ClubId).ContinueWith((task1) =>
      {
        var data1 = task1.Result;
        app.Tournaments = new ObservableCollection<Models.TournamentInfo>(data1);
          MessagingCenter.Send<string>("update", "UpdateImages");
          Device.BeginInvokeOnMainThread(() => {
            lv.EndRefresh();
        });
      );
    });
  MessagingCenter.Subscribe<MyTeamPicCell>(
    this, "UpdateImages", (sender) =>
    {
      DisplayTeamImages(_TeamId);
    });
  MessagingCenter.Subscribe<string>(
    this, "UpdateImages", (sender) =>
    {
      DisplayTeamImages(_TeamId);
    });
  DisplayTeamImages(_TeamId);

  Content = new StackLayout
  {
    Children = {
      lv
    }
  };
  this.ToolbarItems.Add(new ToolbarItem("+", String.Empty, () =>
  {
    this.Navigation.PushAsync(new TeamPicPage(_TeamId));
  }));
}
private void DisplayTeamImages(long TeamId)
{
  var app = (GolfGameClubApp.App)App.Current;
  var data = app.Tournaments;
  var pics = (from t in data select t).SelectMany(n => n.Teams).Where(m => m.TeamId == 
    TeamId).Select(o => o.TeamPics).First();
  Device.BeginInvokeOnMainThread(() =>
  {
    lv.ItemsSource = pics;
  });
}
}
public class MyTeamPicCell : ImageCell
{
public MyTeamPicCell()
{

}
protected override void OnAppearing()
{
  base.OnAppearing();   
}

protected override void OnBindingContextChanged()
{
  base.OnBindingContextChanged();

  if (BindingContext != null)
  {
    var moreAction = new MenuItem { Text = "Delete", IsDestructive = true };
    moreAction.SetBinding(MenuItem.CommandParameterProperty, new Binding("."));
    moreAction.Clicked += async (sender, e) => {
      await System.Threading.Tasks.Task.Run(() => {
        var mi = ((MenuItem)sender);
        var cp = (Models.TeamPicture)mi.CommandParameter;
        var teamPicId = cp.TeamPictureId;
        var myapp = (GolfGameClubApp.App)App.Current;
        var ClubId = myapp.ClubId.Value;
        var TournamentId = myapp.TournamentId.Value;
        var TeamId = myapp.TeamId.Value;
        WebServices.ws.TeamPictureDelete(ClubId, TournamentId, TeamId, 
          teamPicId).ContinueWith((task) => {
          var app = (GolfGameClubApp.App)App.Current;
          var token = app.UserToken;
          WebServices.ws.TournamentsInClub(token, ClubId).ContinueWith((task1) =>
          {
            var data1 = task1.Result;
            app.Tournaments = new ObservableCollection<Models.TournamentInfo>(data1);
            MessagingCenter.Send<MyTeamPicCell>(this, "UpdateImages");
          });
        });
      });
    };
    ContextActions.Add(moreAction);
  }
}
}

What's The Verdict?
Let's look back at each issue in creating the app and go through them one by one.

How well does it display on a platform? How much platform-specific code is necessary? Does the application "look" like a native platform? The application looks very much like an iPhone application on the iPhone and an Android application on Android. The iPhone has a UINavigationController that allows a developer to easily push and pop view controllers onto the stack. XF has a similar concept. The XF application looks like just about every other iPhone application out there. Overall, the programming model is very iPhone-centric. Given that the iPhone is what most people think of when they think of mobile, this is probably not too bad.

When a developer has to resort to native API calls, how easy is this? Can this be done? Does it work? Yes, it works, and it works pretty easily for apps such as this example.

And, finally: When something else has to happen, how much third-party support is there? I'm not going to try to pretend that there are as many XF components as components for iOS or Android. There is a big difference in the number of components. However, for the application that I had to build, there were several third-party components that did meet my needs.

Note: Xamarin has recently open sourced a set of components that Xamarin employees have built for Xamarin.Forms.

How well did we do? So far, we've done fairly well. We'll have more on this subject in the future, I'm sure.

comments powered by Disqus

Featured

  • Compare New GitHub Copilot Free Plan for Visual Studio/VS Code to Paid Plans

    The free plan restricts the number of completions, chat requests and access to AI models, being suitable for occasional users and small projects.

  • Diving Deep into .NET MAUI

    Ever since someone figured out that fiddling bits results in source code, developers have sought one codebase for all types of apps on all platforms, with Microsoft's latest attempt to further that effort being .NET MAUI.

  • Copilot AI Boosts Abound in New VS Code v1.96

    Microsoft improved on its new "Copilot Edit" functionality in the latest release of Visual Studio Code, v1.96, its open-source based code editor that has become the most popular in the world according to many surveys.

  • AdaBoost Regression Using C#

    Dr. James McCaffrey from Microsoft Research presents a complete end-to-end demonstration of the AdaBoost.R2 algorithm for regression problems (where the goal is to predict a single numeric value). The implementation follows the original source research paper closely, so you can use it as a guide for customization for specific scenarios.

  • Versioning and Documenting ASP.NET Core Services

    Building an API with ASP.NET Core is only half the job. If your API is going to live more than one release cycle, you're going to need to version it. If you have other people building clients for it, you're going to need to document it.

Subscribe on YouTube