Control only on one platform - XAML and OnPlatform

I want to add label (or other element) only on one platform, how achieve that using XAML? I tried:

<OnPlatform x:TypeArguments="View">
  <OnPlatform.WinPhone>
    <Label Text="Only on WinPhone"></Label>
  </OnPlatform.WinPhone>
</OnPlatform>

but it throws exception. I guess I could use visibility and set false on every platform, but one, but maybe there is more elegant way.

Posts

  • adamkempadamkemp USInsider, Developer Group Leader mod

    I've seen this before, and I finally spent some time trying to figure out how all this works and why this particular case doesn't work. For a hacky workaround skip to the end. Here's the long, boring explanation of why it doesn't work as-is:

    When the XAML loader adds children to an element it looks for a method named "Add" (using reflection) on the content property of the parent. A Layout<T> has a Children property, which is an IList<T>, which has an Add(T) method. So when the parser creates the child element and wants to add it to the parent it goes to the content property and looks for the Add method and then calls it using a reflection Invoke method something like this:

    methodInfo.Invoke(content, new object[] { newChild }); // content.Add(newChild)
    

    The problem is that Add takes a View in this case, and OnPlatform is not a View. Instead, OnPlatform<T> has an implicit operator to convert to T (so in this case OnPlatform<View> can implicitly convert to a View). It turns out that the Invoke method does not do any implicit conversions so it ends up actually trying to insert the OnPlatform object into the list of children instead of the View that would result from the conversion.

    I think I found a way that Xamarin could have written their code to get the conversion, which would require using a TypeConverter and a custom Binder implementation. For instance, this code works:

        public void Foo(A a)
        {
        }
    
        public class A {}
    
        [TypeConverter(typeof(BToATypeConverter))]
        private class B
        {
            public static implicit operator A(B b)
            {
                return new A();
            }
        }
    
        public class BToATypeConverter : TypeConverter
        {
            public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
            {
                return destinationType == typeof(A);
            }
    
            public override object ConvertTo(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value, Type destinationType)
            {
                A converted = (B)value;
                return converted;
            }
        }
    
        private class MyBinder : Binder
        {
            public override MethodBase BindToMethod(BindingFlags bindingAttr, MethodBase[] match, ref object[] args, ParameterModifier[] modifiers, System.Globalization.CultureInfo culture, string[] names, out object state)
            {
                throw new NotImplementedException();
            }
    
            #region implemented abstract members of Binder
    
            public override FieldInfo BindToField(BindingFlags bindingAttr, FieldInfo[] match, object value, System.Globalization.CultureInfo culture)
            {
                throw new NotImplementedException();
            }
    
            public override object ChangeType(object value, Type type, System.Globalization.CultureInfo culture)
            {
                // This is almost certainly an incomplete implementation, but it works for this case.
                return TypeDescriptor.GetConverter(value.GetType()).ConvertTo(value, type);
            }
    
            public override void ReorderArgumentArray(ref object[] args, object state)
            {
                throw new NotImplementedException();
            }
    
            public override MethodBase SelectMethod(BindingFlags bindingAttr, MethodBase[] match, Type[] types, ParameterModifier[] modifiers)
            {
                throw new NotImplementedException();
            }
    
            public override PropertyInfo SelectProperty(BindingFlags bindingAttr, PropertyInfo[] match, Type returnType, Type[] indexes, ParameterModifier[] modifiers)
            {
                throw new NotImplementedException();
            }
    
            #endregion
        }
    

    And the invocation:

            var mi = typeof(AppDelegate).GetMethod("Foo");
    
            mi.Invoke(this, BindingFlags.InvokeMethod, new MyBinder(), new object[] { new B() }, null);
    

    What's going on here is that I have two unrelated classes A and B and a method Foo that takes an A. I then dynamically call that method and give it a B (an unrelated type), but when I do that I give it a custom Binder implementation that knows how to handle type conversions. Then I added a custom TypeConverter to B that tells it how to convert to an A.

    In this example A is like View and B is like OnPlatform<View>. Since OnPlatform is generic the converter gets a bit trickier. I made it work doing something like this:

        [TypeConverter(typeof(BToATypeConverter))]
        private class B<T> where T : new()
        {
            public static implicit operator T(B<T> b)
            {
                return new T();
            }
    
            public T Cast()
            {
                return this;
            }
        }
    
        public class BToATypeConverter : TypeConverter
        {
            public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
            {
                return destinationType == typeof(A);
            }
    
            public override object ConvertTo(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value, Type destinationType)
            {
                object converted = value.GetType().GetMethod("Cast").Invoke(value, null);
                return converted;
            }
        }
    

    There might be a more direct way to get the implicit operator through reflection instead of introducing a new method.

    Of course there could be a much more elegant way to handle this, but that's what I know so far. It seems plausible.

    Here's the hacky workaround:

    Create a custom View class like this:

    [ContentProperty("Child")]
    public class OnPlatformView : Grid
    {
        private OnPlatform<View> _child;
    
        public OnPlatform<View> Child
        {
            get { return _child; }
            set
            {
                if (_child != value)
                {
                    if (_child != null)
                    {
                        Children.Remove(_child);
                    }
                    _child = value;
    
                    if (_child != null)
                    {
                        Children.Add(_child);
                    }
                }
            }
        }
    }
    

    Then just wrap your OnPlatform element in your new custom view:

        <custom:OnPlatformView>
            <OnPlatform x:TypeArguments="View">
                <OnPlatform.iOS>
                    <Label Text="iOS" />
                </OnPlatform.iOS>
                <OnPlatform.Android>
                    <Label Text="Android" />
                </OnPlatform.Android>
            </OnPlatform>
        </custom:OnPlatformView>
    

    Now the view that's added to the parent is a Grid (subclass), and the custom view handles the conversion.

  • MI.5069MI.5069 USMember

    @adamkemp
    Thanks for comprehensive answer, wraping with ContentView works.

  • ClapotiClapoti CAMember ✭✭

    Wouldn't it be detrimental to the performance when displaying the page ?
    It would have to calculate the layout even if IsVisible is false, or maybe I'm mistaken ?

  • jamelljamell Member
    edited March 2018

    @Sreeraj.0276 said:
    A control can be added just to one platform using XAML by setting its IsVisible property using OnPlatform.

    For example if we want a StackLayout just in iOS, we lay that out using XAML as below

    <StackLayout> <StackLayout.IsVisible> <OnPlatform x:TypeArguments="x:Boolean"> <OnPlatform.iOS> true </OnPlatform.iOS> <OnPlatform.Android> false </OnPlatform.Android> </OnPlatform> </StackLayout.IsVisible> </StackLayout>

    It works like this. But I have a question to this code. First I forgot the 'x:' before the Boolean, I just wrote:

    OnPlatform x:TypeArguments="Boolean"

    After adding the x: to the TypeArguments, it worked:

    OnPlatform x:TypeArguments="x:Boolean"

    What is the x: doing and why is it necessary?

    Thanks & Cheers
    J.

  • SteveShaw.5557SteveShaw.5557 USMember ✭✭✭
    edited September 2018

    @Clapoti - my timing tests suggest that using IsVisible=False creates (constructs) the invisible content, but does not do layout call. So in release build, using XAML compilation and AOT+LLVM [VS Enterprise edition], the time is much less than when content is visible. So it is a useful technique in many cases.

    However, for the case of platform-specific content, using OnPlatform is superior, as then the content is completely skipped.

  • NMackayNMackay GBInsider, University mod

    @SteveShaw.5557 said:
    @Clapoti - my timing tests suggest that using IsVisible=False creates (constructs) the invisible content, but does not do layout call. So in release build, using XAML compilation and AOT+LLVM [VS Enterprise edition], the time is much less than when content is visible. So it is a useful technique in many cases.

    However, for the case of platform-specific content, using OnPlatform is superior, as then the content is completely skipped.

    Yeah, using visibility isn't the best approach in this case.

  • SteveShaw.5557SteveShaw.5557 USMember ✭✭✭
    edited September 2018

    NOTE: The current revised syntax for OnPlatform (so new platforms can be added more easily, as the names are strings rather than values of an enum), means changing @adamkemp's code to:

        <ContentView>
            <OnPlatform x:TypeArguments="View">
                <On Platform="iOS">
                    <Label Text="iOS" />
                </On>
                <On Platform="Android">
                    <Label Text="Android" />
                </On>
            </OnPlatform>
        </ContentView>
    

    Note the space between "On" and "Platform" in the sub-nodes.

  • NipunKalraNipunKalra USMember ✭✭

    @jamell

    What is the x: doing and why is it necessary?

    x: is the mapping to the xaml namespace. refer the link for detail understanding.
    lhttps://docs.microsoft.com/en-us/dotnet/framework/wpf/advanced/xaml-namespaces-and-namespace-mapping-for-wpf-xaml

Sign In or Register to comment.