November 30th, 2016

Creating Platform-Specifics in Xamarin.Forms

Recently on the blog, Pierce introduced Platform-Specifics, which allow you to consume functionality that’s only available on a specific platform without having to implementing custom renderers or effects. With the use of a simple Fluent API, you can make platform-specific tweaks from shared code in Xamarin.Forms.

At the time of writing, Xamarin.Forms includes the following Platform-Specifics:

  • Blur support for any VisualElement on iOS.
  • A translucent navigation bar on iOS.
  • The ability to set the operating mode for a soft keyboard input area on Android.
  • Toolbar placement options on Windows.
  • A partially collapsible MasterDetailPage navigation bar on Windows.

In addition, vendors can create their own Platform-Specifics with Effects. An Effect provides the desired functionality, which is then exposed through a Platform-Specific. The result is an Effect that can more easily be consumed through XAML and a fluent code API.

In this blog post, I’m going to demonstrate how to expose an Effect through a Platform-Specific that adds a shadow to the text displayed by a Label. All the code can be found in a sample application. However, the blog post will focus on the implementation for the iOS platform.

Creating a Platform-Specific

The process for creating a Platform-Specific is:

  1. Implement the desired functionality as an Effect.
  2. Create a Platform-Specific class that will expose the Effect.
  3. Add an attached property to the Platform-Specific class to allow the Platform-Specific to be consumed through XAML.
  4. Add extension methods in the Platform-Specific class to allow it to be consumed through a fluent code API.
  5. Modify the Effect implementation so that the Effect is only applied if the Platform-Specific has been invoked on the same platform as the Effect.

Each item will now be considered in turn.

Implement the Desired Functionality as an Effect

In the sample application, the iOS project provides the LabelShadowEffect class, which adds a shadow to a Label by setting the following properties:

Control.Layer.CornerRadius = 5;
Control.Layer.ShadowColor = UIColor.Black.CGColor;
Control.Layer.ShadowOffset = new CGSize(5, 5);
Control.Layer.ShadowOpacity = 1.0f;

For more information about creating this Effect, see Creating an Effect and Passing Effect Parameters as Attached Properties.

Create a Platform-Specific Class

A Platform-Specific is created as a public static class:

namespace MyCompany.Forms.PlatformConfiguration.iOS
{
	using System.Linq;
	using Xamarin.Forms;
	using Xamarin.Forms.PlatformConfiguration;
	using FormsElement = Xamarin.Forms.Label;

	public static class Shadow
	{
		const string EffectName = "MyCompany.LabelShadowEffect";
        ...

		static void AttachEffect(FormsElement element)
		{
			IElementController controller = element;
			if (controller == null || controller.EffectIsAttached(EffectName))
			{
				return;
			}
			element.Effects.Add(Effect.Resolve(EffectName));
		}

		static void DetachEffect(FormsElement element)
		{
			IElementController controller = element;
			if (controller == null || !controller.EffectIsAttached(EffectName))
			{
				return;
			}

			var toRemove = element.Effects.FirstOrDefault(e => e.ResolveId == Effect.Resolve(EffectName).ResolveId);
			if (toRemove != null)
			{
				element.Effects.Remove(toRemove);
			}
		}
	}
}

The AttachEffect and DetachEffect methods will be used to add or remove the Effect, and will be invoked by additional code that’s added to the Shadow class.

Add an Attached Property

An attached property must be added to the Platform-Specific class to allow consumption through XAML:

public static class Shadow
{
    ...
	public static readonly BindableProperty IsShadowedProperty = 
       BindableProperty.CreateAttached("IsShadowed", typeof(bool), typeof(Shadow), false, propertyChanged: OnIsShadowedPropertyChanged);

	public static bool GetIsShadowed(BindableObject element)
	{
		return (bool)element.GetValue(IsShadowedProperty);
	}

	public static void SetIsShadowed(BindableObject element, bool value)
	{
		element.SetValue(IsShadowedProperty, value);
	}

	static void OnIsShadowedPropertyChanged(BindableObject element, object oldValue, object newValue)
	{
		if ((bool)newValue)
		{
			AttachEffect(element as FormsElement);
		}
		else
		{
			DetachEffect(element as FormsElement);
		}
	}
        ...
}

The IsShadowed attached property is used to add the effect to, and remove it from, the control that the Shadow class is attached to. The attached property registers the OnIsShadowedPropertyChanged method that will be executed when the value of the property changes. In turn, this method calls the AttachEffect or DetachEffect method.

Add Extension Methods

Extension methods must be added to the Platform-Specific to allow consumption through a fluent code API:

public static class Shadow
{
    ...
	public static bool IsShadowed(this IPlatformElementConfiguration<iOS, FormsElement> config)
	{
		return GetIsShadowed(config.Element);
	}

	public static IPlatformElementConfiguration<iOS, FormsElement> SetIsShadowed(this IPlatformElementConfiguration<iOS, FormsElement> config, bool value)
	{
		SetIsShadowed(config.Element, value);
		return config;
	}
    ...
}

The IsShadowed and SetIsShadowed extension methods invoke the get and set accessors for the IsShadowed attached property, respectively. Each extension method operates on the IPlatformElementConfiguration<iOS, FormsElement> type, which specifies that the Platform-Specific can only be invoked on Label instances from iOS.

Modify the Effect Implementation

The Effect implementation must be modified so that it can only be invoked from the same platform the Platform-Specific is implemented for:

if (((Label)Element).OnThisPlatform().IsShadowed())
{
	Control.Layer.CornerRadius = 5;
	Control.Layer.ShadowColor = UIColor.Black.CGColor;
	Control.Layer.ShadowOffset = new CGSize(5, 5);
	Control.Layer.ShadowOpacity = 1.0f;
}
else if (!((Label)Element).OnThisPlatform().IsShadowed())
{
	Control.Layer.ShadowOpacity = 0;
}

A shadow is added to the Label text provided that the IsShadowed attached property is set to true, and provided that the Shadow Platform-Specific has been invoked on the same platform that the Effect is implemented for. This check is performed with the OnThisPlatform method.

Consuming the Platform-Specific

The Shadow Platform-Specific can be consumed in XAML by setting the Shadow.IsShadowed attached property to a boolean value:

<ContentPage xmlns:ios="clr-namespace:MyCompany.Forms.PlatformConfiguration.iOS" ...>
  ...
  <Label Text="Label Shadow Effect" ios:Shadow.IsShadowed="true" ... />
  ...
</ContentPage>

Alternatively, the Shadow platform-specific can be consumed through the fluent code API:

using Xamarin.Forms.PlatformConfiguration;
using MyCompany.Forms.PlatformConfiguration.iOS;

...

shadowLabel.On<iOS>().SetIsShadowed(true);

This results in a shadow being applied to the text displayed by a Label:

screenshot

Wrapping Up

The result of exposing an Effect as a Platform-Specific is that the Effect can be more easily consumed through XAML and through a fluent code API.

For more information about consuming Platform-Specifics, see Consuming Platform-Specifics. For more information about creating Platform-Specifics, see Creating Platform-Specifics.

Author

0 comments

Discussion are closed.