Xamarin App Configuration: Control Your App Settings


When building Xamarin Apps there is no easy out of the box mechanism to control your enviornmental settings such as web service url as your app migrates through the different environments such as Development, Staging and Production. With a little work we can add a configuration file very similar to how you would update a web.config or appsettings.json in an ASP.NET or ASP.NET Core application

Build Configurations

Having worked on many different Xamarin Apps from Enterprise to Small Business apps I have seen a variety of implementations for handling development and production AppSettings. Since Xamarin doesn't support technologies such as a web.config or appsettings.json out of the box we have typically just used preproccessor directives to handle the different build environments.

namespace App
{
    public class Constants
    {
#if DEBUG
        public const string ServiceEndpoint = "https://localhost/service";
        public const string AppSecret = "1b128dec-fd50-4ee3-91c1-bf9c1b4a8d95";
#else
        public const string ServiceEndpoint = "https://andrewhoefling.com/service";
        public const string AppSecret = "2290863d-0065-46dc-a433-4ab2c717bf41";
#endif
    }
}

This approach of build configurations is an easy quick solution for small projects but it introduces several problems with your Application Settings

  • Secrets get checked into source control
  • As you add more build enviornments it gets more complicated to configure
    • Dev
    • QA
    • Staging
    • Production
    • etc.
  • The code is hard to read and understand what is being used
  • Visual Studio 2018 and older have had a history of prepoccessor directives not displaying correctly

A True App Settings

Instead of having hard-coded values in a constants file we want to use a configuration file that is easy to update and even run transforms at build time. 

This example assumes you are using a .NET Standard Shared Code Library. If you are using a shared library please look for the notes that document how to handle shared code solutions.

Create App Settings

In Visual Studio or Visual Studio for Mac create an appsettings.json in the root directory of your shared code project. 

  1. Right click on the newly created appsettings.json
  2. Select Properties

Update Build Action to Embedded Resource. If you do not set the Build Action correctly your app will not be able to read your appsettings.json file at runtime.

Now we can go ahead and populate our appsettings.json with the content from our original Constants.cs file.

{
  "Service": "https://localhost/service",
  "AppSecret": "1b128dec-fd50-4ee3-91c1-bf9c1b4a8d95"
}

Creat AppSettingsManager

Create a new code file in your shared project called AppSettingsManager.cs. This is going to be a simple Singleton Manager that will allow you to easily access your App Settings anywhere in the shared code of your project.

Dependency

Our solution takes a dependency on https://www.nuget.org/packages/Newtonsoft.Json/. Go ahead and include it in your project before proceeding.

 

Scaffold and Stub out the class

namespace App
{
    public class AppSettingsManager
    {
        // stores the instance of the singleton
        private static AppSettingsManager _instance;

        // variable to store your appsettings in memory for quick and easy access
        private JObject _secrets

        // constants needed to locate and access the App Settings file
        private const string Namespace = "App";
        private const string Filename = "appsettings.json";
        
        // Creates the instance of the singleton
        private AppSettingsManager()
        {
        }

        // Accesses the instance or creates a new instance
        public static AppSettingsManager Settings
        {
            get 
            { 
                 return null; 
            }
        }
        
        // Used to retrieved setting values
        public string this[string name]
        {
            return string.empty;
        }
    }
}

Constructor and File Reading

Once you create the scaffolded class we can implement the constructor, which is where the file access occurs of the embedded json file we created earlier.

namespace App
{
    public class AppSettingsManager
    {
        // .. omitted code

        private AppSettingsManager()
        {
            var assembly = IntrospectionExtensions.GetTypeInfo(typeof(AppSettingsManager)).Assembly;
            var stream = assembly.GetManifestResourceStream($"{Namespace}.{FileName}");
            using (var reader = new StreamReader(stream))
            {
                var json = reader.ReadToEnd();
                _secrets = JObject.Parse(json);
            }
        }

        // .. omitted code
    }
}

This is an implementation following the Xamarin docs

Shared Project Implementation

If you are using a Shared Project instead of .NET Standard there is a small deviation to the recommended code that is affected when the constructor is executed. You will need to update the Namespace variable to use platform directives to input the correct namespace.

.NET Standard Implementation

namespace App
{
    public class AppSettingsManager
    {
        private const string Namespace = "App";
    }
}

Shared Project Implementation

namespace App
{
    public class AppSettingsManager
    {
#if __IOS__
        private const string Namespace = "App.iOS";
#endif
#if __ANDROID__
        private const string Namespace = "App.Droid";
#endif
    }
}

Implement Singleton

Implement the Singleton Property Accessor so we can easily get our App Settings anwhere in the application.

namespace App
{
    public class AppSettingsManager
    {
        // .. omitted code

        public static AppSettingsManager Settings
        {
            get
            {
                if (_instance == null)
                {
                    _instance = new AppSettingsManager();
                }

                return _instance;
            }
        }

        // .. omitted code
    }
}

Implement Property Accessor

Implement the property accessor

  1. Using name.Split(":") to allow easy JSON Nesting traversal
  2. Access the first item in the array
  3. Iterate through array until we get to the final property
namespace App
{
    public class AppSettingsManager
    {
        // .. omitted code

        public string this[string name]
        {
            get
            {
                try
                {
                    var path = name.Split(':');

                    JToken node = _secrets[path[0]];
                    for (int index = 1; index < path.Length; index++)
                    {
                        node = node[path[index]];
                    }

                    return node.ToString();
                }
                catch (Exception)
                {
                    Debug.WriteLine($"Unable to retrieve secret '{name}'");
                    return string.Empty;
                }
            }
        }

        // .. omitted code
    }
}

Complete AppSettingsManager

The Completed AppSettingsManager

namespace App
{
    public class AppSettingsManager
    {
        private static AppSettingsManager _instance;
        private JObject _secrets;

        private const string Namespace = "App";
        private const string FileName = "appsettings.json";

        private AppSettingsManager()
        {
            try
            {
                var assembly = IntrospectionExtensions.GetTypeInfo(typeof(AppSettingsManager)).Assembly;
                var stream = assembly.GetManifestResourceStream($"{Namespace}.{FileName}");
                using (var reader = new StreamReader(stream))
                {
                    var json = reader.ReadToEnd();
                    _secrets = JObject.Parse(json);
                }
            }
            catch (Exception ex)
            {
                Debug.WriteLine("Unable to load secrets file");
            }
        }

        public static AppSettingsManager Settings
        {
            get
            {
                if (_instance == null)
                {
                    _instance = new AppSettingsManager();
                }

                return _instance;
            }
        }

        public string this[string name]
        {
            get
            {
                try
                {
                    var path = name.Split(':');

                    JToken node = _secrets[path[0]];
                    for (int index = 1; index < path.Length; index++)
                    {
                        node = node[path[index]];
                    }

                    return node.ToString();
                }
                catch (Exception)
                {
                    Debug.WriteLine($"Unable to retrieve secret '{name}'");
                    return string.Empty;
                }
            }
        }
    }
}

AppSettingsManager Usage

Once we complete the configuration implementation we can start using it in our App Code. In our example we are going to print out the Service endpoint onto the screen.

XAML

Update the xaml and give the Label a Name

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:XamConfigManager"
             x:Class="XamConfigManager.MainPage">

    <StackLayout>
        <!-- Place new controls here -->
        <Label x:Name="Message" 
               Text="Welcome to Xamarin.Forms!" 
               HorizontalOptions="Center"
               VerticalOptions="CenterAndExpand" />
    </StackLayout>

</ContentPage>

Code Behind

In the code behind update the Message.Text property using our AppSettingsManager

namespace App
{
    public partial class MainPage : ContentPage
    {
        public MainPage()
        {
            InitializeComponent();
            Message.Text = AppSettingsManager.Settings["Service"];
        }
    }
}

Conclusion

With our implemented solution we can now easily update our app to work on different environments. It also facilitates build transforms during your Continuous Integration/Continuous Delivery builds.

 

Useful Links


Share

Tags