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.
1
2
3
4
5
6
7
8
9
10
11
12
13
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://SkyeThe.Dev/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.
- Right click on the newly created
appsettings.json
- 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.
1
2
3
4
{
"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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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
1
2
3
4
5
6
7
namespace App
{
public class AppSettingsManager
{
private const string Namespace = "App";
}
}
Shared Project Implementation
1
2
3
4
5
6
7
8
9
10
11
12
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.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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
- Using
name.Split(":")
to allow easy JSON Nesting traversal - Access the first item in the array
- Iterate through array until we get to the final property
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?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
1
2
3
4
5
6
7
8
9
10
11
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