Build an App with Xamarin.Forms using Sitecore Mobile SDK for Xamarin – Getting started

StartHero

After the great news that Microsoft acquired Xamarin, I just had to do some lovely Xamarin stuff ๐Ÿ™‚

Sitecore did a great job by making the Sitecore Mobile SDK for Xamarin and the documentation is really good – Mobile SDK for Xamarin.

The Sitecore Mobile SDK for Xamarin is a Portable Class Library (PCL) that provides a high level abstraction for Sitecore items that lets developers work with items and fields rather than HTTP requests and JSON responses.

The cool thing it’s all on the Github at https://github.com/Sitecore/sitecore-xamarin-pcl-sdk and the developer team is really helpful with your requests/issues ๐Ÿ™‚

Working with Xamarin Forms and xaml is indeed a blessing and I’m really happy for Miguel de Icaza and his great Xamarin team to finally join up with Microsoft.
I also want to give Microsoft a big thank you for allowing us .Net developers(with Visual Studio license) to use Xamarin fully free of charge.

I’ve been working on a cross app for some time now and the idea is to present data from a Sitecore website(in this case Habitat), I’ve succeeded to make it work on my iPhone 6 Plus and on my daughters Samsung Galaxy S3. ๐Ÿ™‚
HabitatApp.Start
I wanted to share with you guys on how it was done, it will be a blogpost series where the first post will be about “Getting started”.

I’m a big fan of MonoDevelop(with good old Monotouch) so for me it became natural to continue develop in Xamarin Studio for Mac, anyways lets dive in ๐Ÿ™‚

Setup solution

First we need to set up the solution, it’s quite straightforward. Select the Cross Platform project and that’s it.
XamarinSolution

XamarinSolution2

Now we have have a solution containing three projects:
Xamarin.Forms:
This is where we do business logic and design(layout) using the lovely XAML(eXtensible Application Markup Language) and together with data binding it allows you to work with the very elegant pattern – Model-View-ViewModel(MVVM).
IOS and DROID:
IOS and DROID are platform specific projects. If we need to do some special adjustment for a particular platform, this is where we will put the code. Xamarin did a very smart and easy way on how to cross customize functionality between platforms – DependencyService.

DependencyService allows apps to call into platform-specific functionality from shared code. This functionality enables Xamarin.Forms apps to do anything that a native app can do.

DependencyService is a dependency resolver. In practice, an interface is defined and DependencyService finds the correct implementation of that interface from the various platform projects

Store data locally

We will have to have store some “settings” data locally and for that we will use SQLite db. Let’s get them from the Nuget Gallery, this is the ones we need:

  • SQLIte.Net-PCL
  • SQLIte.Net.Core-PCL
  • SQLIte.Net.Async-PCL
  • We will have to add them to all three projects.

    Next will be to implement SQLite in the Xamarin.Forms project, we do that by creating the interface – ISQLiteDb. It will define how to interact with platform-specific functionality.

    namespace HabitatApp.CrossDependencies
    {
    
    	using SQLite.Net;
    	using SQLite.Net.Async;
    
    	public interface ISQLiteDb
    	{
    		SQLiteConnection GetConnection ();
    
    		SQLiteAsyncConnection GetAsyncConnection ();
    	}
    }
    

    Let’s go to the IOS project to set up SQLite. The interesting part is [assembly: Xamarin.Forms.Dependency(typeof(SQLiteDb))], it will register it to the DependencyService.

    using HabitatApp.iOS.CrossDependencies;
    
    [assembly: Xamarin.Forms.Dependency(typeof(SQLiteDb))]
    namespace HabitatApp.iOS.CrossDependencies
    {
    	using System;
    	using HabitatApp.CrossDependencies;
    	using SQLite.Net;
    	using SQLite.Net.Platform.XamarinIOS;
    	using System.IO;
    	using SQLite.Net.Async;
    
    	public class SQLiteDb : ISQLiteDb
    	{
    
    		private const string _fileName = "HabitatLocal.db3";
    
    		public SQLiteDb ()
    		{
    		}
    
    		public SQLiteConnection GetConnection ()
    		{
    			string documentsPath = Environment.GetFolderPath (Environment.SpecialFolder.Personal);
    			string libraryPath = Path.Combine (documentsPath, "..", "Library");
    			string path = Path.Combine (libraryPath, _fileName);
    
    			SQLitePlatformIOS platform = new SQLitePlatformIOS ();
    			SQLiteConnection connection = new SQLiteConnection(platform, path);
    
    			return connection;
    		}
    
    		public SQLiteAsyncConnection GetAsyncConnection ()
    		{
    			string documentsPath = Environment.GetFolderPath (Environment.SpecialFolder.Personal);
    			string libraryPath = Path.Combine (documentsPath, "..", "Library");
    			string path = Path.Combine (libraryPath, _fileName);
    
    			var platform = new SQLitePlatformIOS();
    
    			var param = new SQLiteConnectionString(path, false); 
    			var connection = new SQLiteAsyncConnection(() => new SQLiteConnectionWithLock(platform, param)); 
    
    			return connection;
    
    		}
    	}
    }
    

    And here is the Droid project for setting up SQLite and again notice the attribute [assembly: Xamarin.Forms.Dependency(typeof(SQLiteDb))].

    using HabitatApp.Droid.CrossDependencies;
    
    [assembly: Xamarin.Forms.Dependency(typeof(SQLiteDb))]
    namespace HabitatApp.Droid.CrossDependencies
    {
    	using System;
    	using HabitatApp.CrossDependencies;
    	using SQLite.Net;
    	using SQLite.Net.Platform.XamarinAndroid;
    	using System.IO;
    	using SQLite.Net.Async;
    
    	public class SQLiteDb : ISQLiteDb
    	{
    		private const string _fileName = "HabitatLocal.db3";
    
    		public SQLiteDb ()
    		{
    		}
    
    		public SQLiteConnection GetConnection ()
    		{
    			string documentsPath = Environment.GetFolderPath (Environment.SpecialFolder.Personal);
    			string applicationFolderPath = Path.Combine (documentsPath,"Habitat");
    			string path = Path.Combine (applicationFolderPath, _fileName);
    
    			if(!Directory.Exists(applicationFolderPath))
    				Directory.CreateDirectory(applicationFolderPath);
    
    			SQLitePlatformAndroid platform = new SQLitePlatformAndroid ();
    			SQLiteConnection connection = new SQLiteConnection(platform, path);
    
    			return connection;
    		}
    
    		public SQLiteAsyncConnection GetAsyncConnection ()
    		{
    			string documentsPath = Environment.GetFolderPath (Environment.SpecialFolder.Personal);
    			string applicationFolderPath = Path.Combine (documentsPath,"Habitat");
    			string path = Path.Combine (applicationFolderPath, _fileName);
    
    			if(!Directory.Exists(applicationFolderPath))
    				Directory.CreateDirectory(applicationFolderPath);
    			
    			SQLitePlatformAndroid platform = new SQLitePlatformAndroid();
    
    			var param = new SQLiteConnectionString(path, false); 
    			var connection = new SQLiteAsyncConnection(() => new SQLiteConnectionWithLock(platform, param)); 
    
    			return connection;
    
    		}
    	}
    }
    

    Now we can use the SQLite in the Xamarin.Forms project. I’ve created a service for that – SQLiteConnectionService.

    namespace HabitatApp.Services
    {
    
    	using SQLite.Net.Async;
    	using Xamarin.Forms;
    	using HabitatApp.Services;
    	using HabitatApp.CrossDependencies;
    
    	public class SQLiteConnectionService : ISQLiteConnectionService
    	{
    		
    		private readonly SQLiteAsyncConnection _asyncConnection;
    
    		public SQLiteConnectionService ()
    		{
    			_asyncConnection = DependencyService.Get<ISQLiteDb>().GetAsyncConnection ();
    		}
    
    
    
    		public SQLiteAsyncConnection AsyncConnection {
    
    			get{ 
    				return _asyncConnection;
    			}
    		}
    
    	}
    }
    

    We use DependencyService to get the platform-specific functionality: GetAsyncConnection().

    Settings

    Now we have a working SQLite DB, it’s time to use it. We need to set and store some settings, most of the properties are used by Sitecore Mobile SDK for Xamarin Framework

    namespace HabitatApp.Models
    {
    	using System;
    	using SQLite.Net.Attributes;
    
    	public class Settings
    	{
    		[PrimaryKey]
    		public string RestBaseUrl {
    			get;
    			internal set;
    		}
    
    		public string SitecoreNavigationRootPath {
    			get;
    			internal set;
    		}
    
    		public string SitecoreNavigationRootId {
    			get;
    			internal set;
    		}
    
    		public string SitecoreUserName {
    			get;
    			internal set;
    		}
    
    		public string SitecorePassword {
    			get;
    			internal set;
    		}
    
    		public string SitecoreShellSite {
    			get;
    			internal set;
    		}
    
    		public string SitecoreDefaultDatabase {
    			get;
    			internal set;
    		}
    
    		public string SitecoreDefaultLanguage {
    			get;
    			internal set;
    		}
    
    		public string SitecoreMediaLibraryRoot {
    			get;
    			internal set;
    		}
    
    		public string SitecoreMediaPrefix {
    			get;
    			internal set;
    		}
    
    		public string SitecoreDefaultMediaResourceExtension {
    			get;
    			internal set;
    		}
    	}
    }
    

    In the SettingsRepository we will handle the saving and fetching of Settings. Notice in the constructor we are getting the connection from the SQLiteConnectionService.

    namespace HabitatApp.Repositories
    {
    
    	using System;
    	using HabitatApp.Services;
    	using SQLite.Net.Async;
    	using System.Threading;
    	using System.Threading.Tasks;
    	using HabitatApp.Models;
    
    	public class SettingsRepository : ISettingsRepository
    	{
    		private ILoggingService _loggingService;
    		private readonly SQLiteAsyncConnection _asyncConnection;
    		private readonly SemaphoreSlim _repositoryLock = new SemaphoreSlim (1);
    
    		public SettingsRepository (ILoggingService loggingService, ISQLiteConnectionService sqLiteConnectionService)
    		{
    			_loggingService = loggingService;
    
    			_asyncConnection = sqLiteConnectionService.AsyncConnection;
    
    			_asyncConnection.CreateTableAsync<Settings> ();
    		}
    
    
    		private async Task Create ()
    		{
    
    			Settings settings = new Settings () {
    				RestBaseUrl =  "http://myhabitat.dev",
    				SitecoreNavigationRootId = "{061E42A3-7CD2-48BC-A611-963DF88AFFE0}",
    				SitecoreNavigationRootPath = "/sitecore/content/Habitat/Mobile App",
    				SitecoreUserName = "sitecore\\admin",
    				SitecorePassword = "b",
    				SitecoreShellSite = "/sitecore/shell",
    				SitecoreDefaultDatabase = "master",
    				SitecoreDefaultLanguage = "en",
    				SitecoreMediaLibraryRoot = "/sitecore/media library",
    				SitecoreMediaPrefix = "~/media/",
    				SitecoreDefaultMediaResourceExtension = "ashx"
    
    			};
    
    			await _repositoryLock.WaitAsync ();
    
    			try {
    				await _asyncConnection.InsertAsync(settings);
    			} catch (Exception ex) {
    				_loggingService.Log ("Error in Insert,  SettingsRepository . Error: {0}", ex.Message); 
    				throw ex;
    			} finally { 
    				_repositoryLock.Release ();
    			}
    
    		}		
    
    
    		public async Task<Settings> Get ()
    		{
    			Settings settings = null;
    			try {
    				settings = await _asyncConnection.GetAsync<Settings> (s => s.RestBaseUrl != null);
    			} catch (System.Exception ex) {
    				_loggingService.Log ("Error in reading object from DB,  SettingsRepository . Error: {0}", ex.Message); 
    			}
    
    			return settings;
    		}
    
    
    		public async Task<Models.Settings> GetWithFallback ()
    		{
    			Settings settings = await Get ();
    
    			if (settings != null)
    				return settings;
    
    			await Create ();
    
    			return await Get ();
    		}
    
    
    
    
    		public async Task<Models.Settings> Update (Settings settings)
    		{
    			await _repositoryLock.WaitAsync ();
    
    			try {
    				await _asyncConnection.UpdateAsync (settings);
    			} catch (Exception ex) {
    				_loggingService.Log ("Error in Update,  SettingsRepository . Error: {0}", ex.Message); 
    			} finally { 
    				_repositoryLock.Release ();
    			}
    
    			return await Get ();
    		}
    
    		public async Task<bool> DeleteAll (){
    
    			Int32 result = 0;
    
    			await _repositoryLock.WaitAsync ();
    
    			try {
    
    				result = await _asyncConnection.DeleteAllAsync<Settings> ();
    
    			} finally { 
    				_repositoryLock.Release ();
    			}
    
    			return result > 0;
    
    
    		}
    
    	}
    }
    

    Thats the settings, now we can finally continue to work with Sitecore Mobile SDK for Xamarin Framework.

    Setting up Sitecore Mobile SDK for Xamarin

    Next step will be to add Sitecore Mobile SDK for Xamarin Framework and Password Provider for Sitecore Mobile SDK, we need to add them to all three projects.
    SItecoreXamarin

    The SitecoreService will fetch the sitecore items from the website. The methods GetItemByPath and GetItemById will do the “Sitecore Web Api” calls and retrieve the items. In order to make it work we need to set some settings and credentials on the ISitecoreWebApiSession object. The session object is created from the GetSession() method. The other methods will be explained in the next post.

    namespace HabitatApp.Services
    {
    
    	using System;
    	using Sitecore.MobileSDK.API.Items;
    	using Sitecore.MobileSDK.API;
    	using Sitecore.MobileSDK.API.Session;
    	using Xamarin.Forms;
    	using Sitecore.MobileSDK.PasswordProvider.Interface;
    	using System.Threading.Tasks;
    	using Sitecore.MobileSDK.API.Request;
    	using Sitecore.MobileSDK.API.Request.Parameters;
    	using System.Collections.Generic;
    	using Sitecore.MobileSDK.API.Exceptions;
    	using System.Linq;
    	using HabitatApp.Repositories;
    	using HabitatApp.Models;
    	using HabitatApp.Extensions;
    	using HabitatApp.CrossDependencies;
    
    
    
    	public class SitecoreService : ISitecoreService
    	{
    		
    		private readonly ISettingsRepository _settingsRepository;
    
    		private readonly ILoggingService _loggingService;
    
    	
    		public SitecoreService (ISettingsRepository settingsRepository, ILoggingService loggingService)
    		{
    			_settingsRepository = settingsRepository;
    			_loggingService = loggingService;
    		}
    		
    
    		public async Task<ScItemsResponse> GetItemByPath(string itemPath, PayloadType itemLoadType, List<ScopeType> itemScopeTypes, string itemLanguage = "en"){
    
    			try {
    
    
    				using (ISitecoreWebApiSession session = await SitecoreSession) {
    					IReadItemsByPathRequest request = ItemWebApiRequestBuilder.ReadItemsRequestWithPath (itemPath)
    						.Payload (itemLoadType)
    						.AddScope (itemScopeTypes)
    						.Language(itemLanguage)
    						.Build ();
    
    
    					return await session.ReadItemAsync(request);
    				
    				}
    			} 
    			catch(SitecoreMobileSdkException ex)
    			{
    				this._loggingService.Log ("Error in GetItemByPath,  id {0} . Error: {1}", itemPath, ex.Message); 
    				throw ex;
    			}
    			catch(Exception ex)
    			{
    				this._loggingService.Log ("Error in GetItemByPath,  id {0} . Error: {1}", itemPath, ex.Message); 
    				throw ex;
    			}
    				
    	
    		}
    
    
    		public async Task<ScItemsResponse> GetItemById(string itemId, PayloadType itemLoadType, List<ScopeType> itemScopeTypes, string itemLanguage = "en"){
    
    			try {
    
    				using (ISitecoreWebApiSession session = await SitecoreSession) {
    					IReadItemsByIdRequest request = ItemWebApiRequestBuilder.ReadItemsRequestWithId (itemId)
    						.Payload (itemLoadType)
    						.AddScope (itemScopeTypes)
    						.Language(itemLanguage)
    						.Build ();
    
    
    					return await session.ReadItemAsync(request);
    
    				}
    
    			} 
    			catch(SitecoreMobileSdkException ex)
    			{
    				this._loggingService.Log ("Error in GetItemById,  id {0} . Error: {1}", itemId, ex.Message); 
    				throw ex;
    			}
    			catch(Exception ex)
    			{
    				this._loggingService.Log ("Error in GetItemById,  id {0} . Error: {1}", itemId, ex.Message); 
    				throw ex;
    			}
    
    
    		}
    
    
    		public async Task<PageData> GeneratePageData(string itemid, PayloadType itemLoadType, List<ScopeType> itemScopeTypes, string datasourceFieldName,  string itemLanguage = "en"){
    
    			ScItemsResponse response = await GetItemById(itemid, itemLoadType, itemScopeTypes, itemLanguage);
    
    			if (response == null)
    				return null;
    
    			ISitecoreItem item = response.First ();
    
    			if (item == null)
    				return null;
    
    			PageData pageData = new PageData {
    				PageName = item.DisplayName,
    				ItemContext =  response,
    				NavigationTitle = item.GetValueFromField(Constants.Sitecore.Fields.Navigation.NavigationTitle),
    				PageType = item.GetTemplateName(),
    				DataSourceFromChildren = await GetDatasourceFromChildren(item),
    				DataSourceFromField = await GetDataSourceFromFieldName(item,datasourceFieldName) 
    
    			};
    
    			return pageData;
    		}
    
    
    		private async Task<IList<ScItemsResponse>> GetDataSourceFromFieldName(ISitecoreItem sitecoreItem, string fieldName){
    
    
    			if (sitecoreItem.Fields.All(f => f.Name != fieldName))
    				return null;
    
    			string value = sitecoreItem [fieldName].RawValue;
    
    			if (string.IsNullOrWhiteSpace (value))
    				return null;
    
    			string[] itemIds = value.Split('|');
    
    			if (!itemIds.Any())
    				return null;
    
    
    			IList<ScItemsResponse> sitecoreItems = new List<ScItemsResponse> ();
    
    
    			foreach (string itemId in itemIds) {
    
    
    				ScItemsResponse response = await GetItemById(itemId, PayloadType.Content, new List<ScopeType>(){ScopeType.Self}, sitecoreItem.Source.Language);
    
    				if (response == null)
    					continue;
    
    				sitecoreItems.Add (response);
    			}
    
    			return sitecoreItems;
    
    		}
    
    		private async Task<ScItemsResponse> GetDatasourceFromChildren(ISitecoreItem sitecoreItem){
    
    			return await GetItemById(sitecoreItem.Id, PayloadType.Content, new List<ScopeType>(){ScopeType.Children}, sitecoreItem.Source.Language);
    
    		}
    
    
    		private Task<ISitecoreWebApiSession> SitecoreSession {
    			get{ 
    					return GetSession ();
    				}
    		}
    
    		private async Task<ISitecoreWebApiSession> GetSession()
    		{
    			Settings settings = await _settingsRepository.GetWithFallback ();
    
    			using (IWebApiCredentials credentials = DependencyService.Get<ICustomSecureStringPasswordProvider> ().Login (settings.SitecoreUserName, settings.SitecorePassword)) {
    
    				{
    					ISitecoreWebApiSession session = SitecoreWebApiSessionBuilder.AuthenticatedSessionWithHost (settings.RestBaseUrl)
    						.Credentials (credentials)
    						.Site (settings.SitecoreShellSite)
    						.DefaultDatabase (settings.SitecoreDefaultDatabase)
    						.DefaultLanguage (settings.SitecoreDefaultLanguage)
    						.MediaLibraryRoot (settings.SitecoreMediaLibraryRoot)
    						.MediaPrefix (settings.SitecoreMediaPrefix)
    						.DefaultMediaResourceExtension (settings.SitecoreDefaultMediaResourceExtension)
    						.BuildSession ();
    
    					return session;
    				}
    			}
    
    		}
    
    
    
    
    	}
    
    }
    

    Fix
    SecureStringPasswordProvider is platform specific that means it will not work in Xamarin.Forms, so we need to do a “Cross Dependency” and use DependencyService (Like we did with SQLiteDB)
    We need to create an interface in the Xamarin.Forms project – ICustomSecureStringPasswordProvider.

    namespace HabitatApp.CrossDependencies
    {
    	using Sitecore.MobileSDK.PasswordProvider.Interface;
    
    	public interface ICustomSecureStringPasswordProvider
    	{
    		IWebApiCredentials Login(string userName, string password);
    	}
    }
    

    Let’s go the IOS project and add the platform specific code:

    using HabitatApp.iOS.CrossDependencies;
    
    [assembly: Xamarin.Forms.Dependency(typeof(CustomSecureStringPasswordProvider))]
    namespace HabitatApp.iOS.CrossDependencies
    {
    	using Sitecore.MobileSDK.PasswordProvider.Interface;
    	using Sitecore.MobileSDK.PasswordProvider.iOS;
    	using HabitatApp.CrossDependencies;
    
    	public class CustomSecureStringPasswordProvider : ICustomSecureStringPasswordProvider
    	{
    		public IWebApiCredentials Login(string userName, string password){
    			return new SecureStringPasswordProvider(userName, password);
    		}
    	}
    }
    

    And we will also do it in the DROID project:

    using HabitatApp.Droid.CrossDependencies;
    
    [assembly: Xamarin.Forms.Dependency(typeof(CustomSecureStringPasswordProvider))]
    namespace HabitatApp.Droid.CrossDependencies
    {
    	using Sitecore.MobileSDK.PasswordProvider.Interface;
    	using Sitecore.MobileSDK.PasswordProvider.Android;
    	using HabitatApp.CrossDependencies;
    
    	public class CustomSecureStringPasswordProvider : ICustomSecureStringPasswordProvider
    	{
    		public IWebApiCredentials Login(string userName, string password){
    			return new SecureStringPasswordProvider(userName, password);
    		}
    	}
    }
    

    Now we can finally use SecureStringPasswordProvider in SitecoreService.

    IWebApiCredentials credentials = DependencyService.Get<ICustomSecureStringPasswordProvider> ().Login (settings.SitecoreUserName, settings.SitecorePassword))
    

    Let’s stop there before your head explode ๐Ÿ™‚
    In the next post I will continue on how the “Mobile App” site is done in Sitecore(Habitat) and I will also show you how the navigation is created in the app.

    I have it all on the Github, feel free to take a look at it: Xamarin.Habitat

    Thatโ€™s all for now folks ๐Ÿ™‚


    4 thoughts on “Build an App with Xamarin.Forms using Sitecore Mobile SDK for Xamarin – Getting started

    Leave a comment

    This site uses Akismet to reduce spam. Learn how your comment data is processed.