Platform Specific Properties

MichaelCookMichaelCook USMember ✭✭

Over in the Custom Renders Feedback thread I floated the idea of being able to have a simple method to set platform specific properties that are not currently implemented in XForms (X.F?). So I started playing around with what it would take to make something like that work and I came up with the following proof-of-concept example:

In the shared/portable view or layout I wanted to be able to write something like this:

var customSearchBar = new PlatformSpecificSearchBar
        {
            Placeholder = "Search",
            WidthRequest = 300,
            PlatformSpecificProperties =
            {
                new PlatformSpecificProperty
                {
                    Name = "BarTintColor",
                    Platform = TargetPlatform.iOS,
                    Value = Color.Red
                }
            }
        };

To make this work you would need a custom class to implement the IPlatformSpecificPropertiesElement interface (a custom SearchBar is used as example below):

 public interface IPlatformSpecificPropertiesElement
    {
        List<PlatformSpecificProperty> PlatformSpecificProperties { get; set; }
    }

   public class PlatformSpecificSearchBar : SearchBar, IPlatformSpecificPropertiesElement
    {
        public PlatformSpecificSearchBar()
        {
            PlatformSpecificProperties = new List<PlatformSpecificProperty>();
        }

        public List<PlatformSpecificProperty> PlatformSpecificProperties { get; set; }
    }

And to implement the properties you will need a custom renderer in each of your supported platforms. The custom render will call a PropertiesRender (in this case a custom SearchBarRender is shown):

   public class CustomSearchBarRenderer : SearchBarRenderer
    {
        private readonly PropertiesRenderer<SearchBar, UISearchBar> _propertiesRenderer;

        public CustomSearchBarRenderer()
        {
            _propertiesRenderer = new PropertiesRenderer<SearchBar, UISearchBar>(this);
        }

        protected override void OnElementChanged(ElementChangedEventArgs<SearchBar> e)
        {
            base.OnElementChanged(e);
            _propertiesRenderer.SetCustomProperties();
        }
    }

The PropertiesRender does all of the heavy lifting for mapping the properties to the base type (iOS example seen below):

    public class PropertiesRenderer<TSource, TArget>
        where TSource : View
        where TArget : UIView 
    {
        private readonly ViewRenderer<TSource, TArget> _renderer;

        public PropertiesRenderer(ViewRenderer<TSource, TArget> renderer)
        {
            _renderer = renderer;
        }

        public void SetCustomProperties()
        {
            var element = (IPlatformSpecificPropertiesElement) _renderer.Element;
            var iOSProperties = element.PlatformSpecificProperties.Where(x => x.Platform == TargetPlatform.iOS).ToList();
            var props = typeof (UISearchBar).GetProperties();

            foreach (var p in iOSProperties)
            {
                var prop = props.FirstOrDefault(x => x.Name == p.Name);
                if (prop == null) continue;

                if (prop.PropertyType == typeof (UIImage))
                {
                    SetImage((ImageSource) p.Value, prop);
                    continue;
                }

                if (prop.PropertyType == typeof(UIFont))
                {
                    SetFont((Font)p.Value, prop);
                    continue;
                }

                if (prop.PropertyType == typeof (UIColor))
                {
                    var typedValue = (Color) p.Value;
                    prop.SetValue(_renderer.Control, typedValue.ToUIColor());
                    continue;
                }

                if (prop.PropertyType.IsEnum)
                {
                    object typedValue = Enum.Parse(prop.PropertyType, p.Value as string);
                    prop.SetValue(_renderer.Control, typedValue);
                    continue;
                }

                prop.SetValue(_renderer.Control, p.Value);
            }
        }

        private void SetFont(Font value, PropertyInfo property)
        {
            property.SetValue(_renderer.Control, value.ToUIFont());
        }

        private async void SetImage(ImageSource source, PropertyInfo property)
        {
            if (source == null) return;
            var type = source.GetType();
            var handler = GetFileHandler(type);

            if (handler == null) return;
            UIImage uiimage;
            try
            {
                uiimage =
                    await handler.LoadImageAsync(source, new CancellationToken(), UIScreen.MainScreen.Scale);
                property.SetValue(_renderer.Control, uiimage);
            }
            finally
            {
                uiimage = null;
            }
        }

        private static IImageSourceHandler GetFileHandler(Type type)
        {
            if (type == typeof(FileImageSource))
            {
                return new FileImageSourceHandler();
            }

            if (type == typeof(StreamImageSource))
            {
                return new StreamImagesourceHandler();
            }
            return null;
        }
    }

It can currently handle Colors, Fonts, Images, Enums and basic types. A few more usage examples:

     // Set SearchBarStyle which is an enum    
    customSearchBar.PlatformSpecificProperties.Add(
                new PlatformSpecificProperty
                {
                    Name = "SearchBarStyle",
                    Platform = TargetPlatform.iOS,
                    Value = "Minimal"
                });

    // Set an image
    customSearchBar.PlatformSpecificProperties.Add(new PlatformSpecificProperty
            {
                Name = "BackgroundImage",
                Platform = TargetPlatform.iOS,
                Value = ImageSource.FromFile("logo.png")
            });

This is all obviously very rough around the edges and I haven't worked on an Android property renderer class, though it shouldn't be much different than the iOS version. It also is non-bindable for XAML folks (and unless someone who is more comfortable with XAML can come up with a solution). But it has already helped me be more productive with my project, so I thought I'd put it out there.

Posts

  • JonDouglasJonDouglas USXamarin Team, University, Developer Group Leader Xamurai

    Thanks for adding this to the community @MichaelCook‌!

  • MichaelCookMichaelCook USMember ✭✭

    You're welcome!

  • HugoLogmans_HugoLogmans_ NLMember ✭✭✭

    @MichaelCook‌ Thanks for this example code. I ran into this when researching my own ways to extend the views. I had the following way to work (using the WebView control). I post this here to provide alternatives to your approach, so people can choose. And feel free to tell me the weaknesses for my approach.

    I had to solve a few issues:

    • adding functionality (methods) that is still missing in the current X.F controls, but is available some way or another on all platforms.
    • handling certain events which impact platform specific code, and is only needed on a specific platform.

    The major part I wanted to solve is to handle methods and events. In Wpf and MvvmCross I often used setting a property just to propagate what essentially was a command. (I could create a bindable property of type string, and when set, it executes the script). But I don't like that.

    This solution forces a tighter coupling between the view and the renderer, but makes sure you have implemented everything for what is expected of the platform-specific renderer. This way there no such thing as trying to attach a boolean value to a image-property.

    And now for the code ;-)

    First, I define the used interfaces: one for common code (for something like executing javascript that is available on every platform), and one for each specific platform.

        public interface ICustomWebviewExtension
        {
            void ExecuteScript(String script);
        }
    
        public interface ICustomWebviewExtensionIOS : ICustomWebviewExtension
        {
            void HandleRotation();
        }
    

    I have a common public interface that makes the renderer able to link the interface (itself) to the original View.
    public interface IRenderExtendable
    {
    void SetExtensionInterface(object ext);
    }

    Then make supporting code for the WebView:

    public class CustomWebView : WebView, IRenderExtendable
    {
        public ICustomWebviewExtension Ext { get; private set; }
    
        public void SetExtensionInterface(object ext)
        {
            Ext = ext as ICustomWebviewExtension;
            // you might want an event to initialize after setting the extension.
        }
    
       /// Use this like: HandlePlatformSpecific<ICustomWebviewExtensionIOS>( (p) => p.HandleRotation());
        public void HandlePlatformSpecific<T>(Action<T> callback) where T:class
        {
            if (Ext as T == null) return;
            callback(Ext as T);
        }
     }
    

    And the renderer needs some small changes:

       public class BaseUrlWebViewRenderer : WebViewRenderer, ICustomWebviewExtensionIOS
        {
            public void ExecuteScript(string script)
            {
                base.EvaluateJavascript(script);
            }
    
            public void HandleRotation()
            {
                // do nothing for the moment
            }
           protected override void OnElementChanged(VisualElementChangedEventArgs e)
            {
                base.OnElementChanged(e);
                var view = e.NewElement as IRenderExtendable;
                if (view != null) view.SetExtensionInterface(this);
            }
        }
    
Sign In or Register to comment.