Bug 44680 - On UWP and WinRT 8.1, Image with IsVisible=false obscures what is beneath it
Summary: On UWP and WinRT 8.1, Image with IsVisible=false obscures what is beneath it
Status: CONFIRMED
Alias: None
Product: Forms
Classification: Xamarin
Component: Forms ()
Version: 2.3.1
Hardware: PC Windows
: Normal normal
Target Milestone: ---
Assignee: Bugzilla
URL:
Depends on:
Blocks:
 
Reported: 2016-09-23 12:53 UTC by John Hardman
Modified: 2016-10-03 20:11 UTC (History)
2 users (show)

Tags: Image IsVisible UWP WinRT ac
Is this bug a regression?: ---
Last known good build:

Notice (2018-05-24): bugzilla.xamarin.com is now in read-only mode.

Please join us on Visual Studio Developer Community and in the Xamarin and Mono organizations on GitHub to continue tracking issues. Bugzilla will remain available for reference in read-only mode. We will continue to work on open Bugzilla bugs, copy them to the new locations as needed for follow-up, and add the new items under Related Links.

Our sincere thanks to everyone who has contributed on this bug tracker over the years. Thanks also for your understanding as we make these adjustments and improvements for the future.


Please create a new report for Bug 44680 on Developer Community or GitHub if you have new information to add and do not yet see a matching new report.

If the latest results still closely match this report, you can use the original description:

  • Export the original title and description: Developer Community HTML or GitHub Markdown
  • Copy the title and description into the new report. Adjust them to be up-to-date if needed.
  • Add your new information.

In special cases on GitHub you might also want the comments: GitHub Markdown with public comments

Related Links:
Status:
CONFIRMED

Description John Hardman 2016-09-23 12:53:04 UTC
I use a Grid to position two Images in the same location on the page. At any time, only one of those images wants to be visible. The purpose of this is to have objects that can be in one of two states, such as a checkbox, radio button, etc. I use this as the content of a ContentButton (my own implementation), so that tapping the button toggles the state of the checkbox etc.

Originally, I toggled the IsVisible property of the Images, so that one is visible and one is not. However, whilst this works as expected on iOS and Android, it fails on UWP and WinRT. On UWP and WinRT, one state works, but in the other state, neither image is visible. This is unexpected.

Whilst there is a workaround (using Opacity instead of IsVisible), the IsVisible handling needs fixing on UWP and WinRT, so that IsVisible works as expected.

In the code below, I have an if/else showing the IsVisible implementation that fails, and the Opacity implementation that works.


            Grid grid = new Grid
            {
                ClassId = "KCI_Grid",
                ColumnDefinitions = new ColumnDefinitionCollection(),
                ColumnSpacing = 0,
                HeightRequest = 44,
                HorizontalOptions = LayoutOptions.Start,
                IsClippedToBounds = true,
                IsVisible = true,
                MinimumHeightRequest = 44,
                MinimumWidthRequest = 44,
                Opacity = 1.0,
                Padding = 0,
                RowDefinitions = new RowDefinitionCollection(),
                RowSpacing = 0,
                VerticalOptions = LayoutOptions.Center,
                WidthRequest = 44,
            };

            grid.SetBinding(Grid.BackgroundColorProperty, "BackgroundColor");
            grid.ColumnDefinitions.Add(new ColumnDefinition { Width = 44 });
            grid.RowDefinitions.Add(new RowDefinition { Height = 44 });

            _toggledImage = new Image { Aspect = Aspect.AspectFit };
            _toggledImage.Source = toggledImageSource;
            _toggledImage.Opacity = 1.0;
            _toggledImage.InputTransparent = true;
            _toggledImage.WidthRequest = 44;

            _untoggledImage = new Image { Aspect = Aspect.AspectFit};
            _untoggledImage.Source = untoggledImageSource;
            _untoggledImage.Opacity = 1.0;
            _untoggledImage.InputTransparent = true;
            _untoggledImage.WidthRequest = 44;

            if (doingItTheOriginalWay)
            {
                _toggledImage.IsVisible = IsToggled;
                _untoggledImage.IsVisible = !IsToggled;
            }
            else
            {
                _toggledImage.Opacity = IsToggled ? 1.0 : 0.0;
                _untoggledImage.Opacity = IsToggled ? 0.0 : 1.0;
            }
Comment 1 John Hardman 2016-09-23 12:54:54 UTC
I should have mentioned that I am using XF 2.3.2.127
Comment 2 John Hardman 2016-09-27 10:40:38 UTC
Apologies - I am using XF 2.3.1.114
Comment 3 Paul DiPietro [MSFT] 2016-09-28 20:35:37 UTC
Could I actually get a hold of your repro if possible? I thought I had the issue replicating on my end but it seems like I'm not and the bottom image is showing up. Sorry for the trouble.
Comment 4 John Hardman 2016-10-03 16:23:25 UTC
This is definitely still a problem in XF 2.3.1.114 on UWP.

Unfortunately, I don't have a simple repro sample, as this is built into my (large) app.

The code below is what I have used for experimenting today. There are a number of booleans at the top that control what it does. Replace the "Device.OS == TargetPlatform.Window" tests with false to see some of the issues. Issues become more apparent when this object is used in a ViewCell in a ListView.

In place of MyAppTemplatedContentView, you can use the XLabs templated content view. It's a one-for-one replacement.

In place of MyAppHoverButton, simply use Button.



using System.Windows.Input;

using Xamarin.Forms;

using CommonInfrastructure;

namespace ViewsUsingXamarinForms
{
    public class MyAppToggleImageButton : MyAppTemplatedContentView
    {
        private const bool _inDepthDebugging = false; // To assist in debugging
        private const bool _explicitlyReleaseImageSourcesToAvoidMemoryLeak = true; // awaiting review by others to confirm whether this is a Xamarin problem, as no obvious reason why having this set to false would prevent containing page from being freed up
        private const bool _usingPropertiesToWorkardoundBug41266 = false; // ok for testing purposes, but will need to replace in production code
        private bool _explicitlySetLayoutOptionsToPreventDifferentSizeImages = Device.OS == TargetPlatform.Windows; // using false in conjunction with setting _useOpacityToWorkaroundBug44680 to false, highlights an unreported sizing issue on UWP
        private bool _useOpacityToWorkaroundBug44680 = Device.OS == TargetPlatform.Windows; // setting this to false on UWP shows up multiple XF problems

        /// <summary>
        /// A near-transparent button that we place over the entire content of the
        /// underlying control, so that pressing on what appears to be the control
        /// triggers the button processing.
        /// </summary>
        private MyAppHoverButton _button; // using a Button here will be fine for testing

        /// <summary>
        /// A grid that is used to position the near-transparent button over the content.
        /// </summary>
        private Grid _grid;

        /// <summary>
        /// A label used to present information during in-depth debugging
        /// </summary>
        private Label _label;

        /// <summary>
        /// At present, this control uses two Image objects at all times.
        /// Will re-write shortly to only have one in the UI hierarchy,
        /// instead changing the ImageSource associated with the Image.
        /// </summary>
        private Image _toggledImage;
        private Image _untoggledImage;

        /// <summary>
        /// The ImageSource objects used for the two toggle states.
        /// Note that under certain circumstances, these need to be 
        /// explicitly set to null in order to allow the containing
        /// page to be freed up. Am awaiting separate review to confirm
        /// whether this is a misunderstanding of the garbage collection
        /// mechanism on my part, or whether it is a Xamarin bug. 
        /// It appears currently to be the latter.
        /// </summary>
        private ImageSource _toggledImageSource;
        private ImageSource _untoggledImageSource;

        // A Xamarin.Forms bug (https://bugzilla.xamarin.com/show_bug.cgi?id=40817 )
        // means that we can only set AutomationId once for each View. Attempting to
        // set AutomationId a second time results in an exception. As a result, we
        // should not rely on the value in AutomationId currently, so should not use 
        // it as a key for example. We include binding support here in preparation for 
        // the bug being fixed.
        public static readonly BindableProperty AutomationIdProperty
            = BindableProperty.Create(
                nameof(AutomationId),
                typeof(string),
                typeof(MyAppContentButton),
                default(string));

        // Intentionally hiding BackgroundColorProperty in base class.
        public new static readonly BindableProperty BackgroundColorProperty
            = BindableProperty.Create(
                nameof(BackgroundColor),
                typeof (Color),
                typeof (MyAppContentButton),
                Color.Default);

        public static readonly BindableProperty CommandProperty
            = BindableProperty.Create(
                nameof(Command),
                typeof(ICommand),
                typeof(MyAppToggleImageButton),
                default(ICommand));

        public static readonly BindableProperty CommandParameterProperty
            = BindableProperty.Create(
                nameof(CommandParameter),
                typeof(object),
                typeof(MyAppToggleImageButton),
                default(object));

        public static readonly BindableProperty IsToggledProperty
            = BindableProperty.Create(
                nameof(IsToggled),
                typeof(bool),
                typeof(MyAppToggleImageButton),
                default(bool),
                BindingMode.TwoWay);

        public string AutomationId
        {
            get { return (string) GetValue(AutomationIdProperty); } //  return base.AutomationId; }

            set
            {
                // A Xamarin.Forms bug (https://bugzilla.xamarin.com/show_bug.cgi?id=40817 )
                // means that we can only set AutomationId once for each View. Attempting to
                // set AutomationId a second time results in an exception. As a result, we
                // should not rely on the value in AutomationId, so should not use it as a key
                // for example.
                if (AutomationId == null)
                {
                    base.AutomationId = value;
                    if (_toggledImage != null)
                        _toggledImage.AutomationId = value + "_toggled";
                    if (_untoggledImage != null)
                        _untoggledImage.AutomationId = value + "_untoggled";
                }

                if (_inDepthDebugging)
                {
                    if (_label == null)
                    {
                        _label = new Label
                        {
                            HorizontalOptions = LayoutOptions.CenterAndExpand,
                            HorizontalTextAlignment = TextAlignment.Center,
                            Text = value ?? "{null}",
                            TextColor = Color.Black,
                            FontSize = Device.GetNamedSize(NamedSize.Small, typeof(Label))
                        };
                        _grid.Children.Add(_label, 0, 2, 1, 2);
                    }
                }

                SetValue(AutomationIdProperty, value);
            }
        }

        protected void OnButtonPressed()
        {
            IsToggled = !IsToggled;

            if (_usingPropertiesToWorkardoundBug41266)
            {
                if (_useOpacityToWorkaroundBug44680)
                {
                    _toggledImage.Opacity = IsToggled ? 1.0 : 0.0;
                    _untoggledImage.Opacity = IsToggled ? 0.0 : 1.0;
                }
                else
                {
                    _toggledImage.IsVisible = IsToggled;
                    _untoggledImage.IsVisible = !IsToggled;
                }
            }
            else
            {
                // no-op, as handled by bindings
            }

            ICommand command = (ICommand) GetValue(CommandProperty);
            if (command != null)
            {
                object commandParameter = GetValue(CommandParameterProperty);
                command.Execute(commandParameter);
            }
        }

        /// <summary>
        /// Command to execute when the button is pressed.
        /// </summary>
        public ICommand Command
        {
            get
            {
                return (ICommand)GetValue(CommandProperty);
            }

            set
            {
                SetValue(CommandProperty, value);
            }
        }

        /// <summary>
        /// Parameter to pass to command handler when the button is pressed.
        /// </summary>
        public object CommandParameter
        {
            get
            {
                return GetValue(CommandParameterProperty);
            }

            set
            {
                SetValue(CommandParameterProperty, value);
            }
        }

        /// <summary>
        /// Current state of the button. Typically toggled will equate to checked or expanded or similar
        /// in derived classes.
        /// </summary>
        public bool IsToggled
        {
            get
            {
                return (bool)GetValue(IsToggledProperty);
            }

            set
            {
                SetValue(IsToggledProperty, value);
            }
        }

        protected override void OnBindingContextChanged()
        {
            base.OnBindingContextChanged();

            if (BindingContext == null)
            {
                if (_explicitlyReleaseImageSourcesToAvoidMemoryLeak)
                {
                    _untoggledImage.Source = null;
                    _toggledImage.Source = null;
                }
            }
            else
            {
                _untoggledImage.Source = _untoggledImageSource;
                _toggledImage.Source = _toggledImageSource;
            }
        }

        public MyAppToggleImageButton(
            ImageSource untoggledImageSource,
            ImageSource toggledImageSource,
            bool initiallyToggled)
            : base()
        {
            _untoggledImageSource = untoggledImageSource;
            _toggledImageSource = toggledImageSource;

            // look into changing this to re-use DataTemplate
            ContentTemplate = new DataTemplate(() =>
            {
                int imageWidth = _inDepthDebugging ? 22 : 44;

                // although we overwrite the grid with images below, those images may be .png files with transparent pixels,
                // so we need to ensure the expected color is displayed beneath those transparent pixels.

                _toggledImage = new Image
                {
                    Aspect = Aspect.AspectFill,
                    Source = toggledImageSource,
                    InputTransparent = true,
                    HeightRequest = imageWidth,
                    WidthRequest = imageWidth
                };
                _toggledImage.SetBinding(
                    BackgroundColorProperty,
                    nameof(BackgroundColor));
                if (_explicitlySetLayoutOptionsToPreventDifferentSizeImages)
                {
                    _toggledImage.HorizontalOptions = LayoutOptions.Start;
                    _toggledImage.VerticalOptions = LayoutOptions.Start;
                }

                _untoggledImage = new Image
                {
                    Aspect = Aspect.AspectFill,
                    Source = untoggledImageSource,
                    InputTransparent = true,
                    HeightRequest = imageWidth,
                    WidthRequest = imageWidth
                };
                _untoggledImage.SetBinding(
                    BackgroundColorProperty,
                    nameof(BackgroundColor));
                if (_explicitlySetLayoutOptionsToPreventDifferentSizeImages)
                {
                    _untoggledImage.HorizontalOptions = LayoutOptions.Start;
                    _untoggledImage.VerticalOptions = LayoutOptions.Start;
                }

                if (_usingPropertiesToWorkardoundBug41266)
                {
                    if (_useOpacityToWorkaroundBug44680)
                    {
                        _toggledImage.Opacity = IsToggled ? 1.0 : 0.0;
                        _untoggledImage.Opacity = IsToggled ? 0.0 : 1.0;
                    }
                    else
                    {
                        _toggledImage.IsVisible = IsToggled;
                        _untoggledImage.IsVisible = !IsToggled;
                    }
                }
                else if (_useOpacityToWorkaroundBug44680)
                {
                    // opacity with bindings

                    Binding untoggledBinding = new Binding
                    {
                        Source = this,
                        Path = nameof(MyAppToggleImageButton.IsToggled),
                        Mode = BindingMode.OneWay,
                        Converter = MyAppBooleanToDoubleTrueToZeroFalseToOneConverter.Instance
                    };
                    _untoggledImage.SetBinding(
                        Image.OpacityProperty, untoggledBinding); // TODO - change this back

                    Binding toggledBinding = new Binding
                    {
                        Source = this,
                        Path = nameof(MyAppToggleImageButton.IsToggled),
                        Mode = BindingMode.OneWay,
                        Converter = MyAppBooleanToDoubleTrueToOneFalseToZeroConverter.Instance
                    };
                    _toggledImage.SetBinding(
                        Image.OpacityProperty, toggledBinding);
                }
                else
                {
                    // visibility with bindings

                    // Note that this exhibits a Xamarin bug that I haven't
                    // reported yet - when IsToggled is changed, the images
                    // are different sizes if I let HorizontalOptions and
                    // VerticalOptions use their default values. My guess is
                    // that this results from an Image having IsVisible=false
                    // when first laid out. I'm guessing that when IsVisible
                    // is later set to true, the layout mechanism skips some
                    // of the properties explicitly specified in the code.

                    Binding untoggledBinding = new Binding
                    {
                        Source = this,
                        Path = nameof(MyAppToggleImageButton.IsToggled),
                        Mode = BindingMode.OneWay,
                        Converter = MyAppNegateBooleanConverter.Instance
                    };
                    _untoggledImage.SetBinding(
                        Image.IsVisibleProperty, untoggledBinding);

                    Binding toggledBinding = new Binding
                    {
                        Source = this,
                        Path = nameof(MyAppToggleImageButton.IsToggled),
                        Mode = BindingMode.OneWay,
                        Converter = MyAppDummyConverter.Instance, // should be equivalent to not having this binding at all
                    };
                    _toggledImage.SetBinding(
                        Image.IsVisibleProperty, toggledBinding);
                }

                _button = new MyAppHoverButton
                {
                    Opacity = 0.02,
                    BorderRadius = 0,
                    BorderWidth = 0,
                    ClassId = "KCI_Button",
                    Command = new DelegateCommand(OnButtonPressed),
                    CommandParameter = null,
                    HorizontalOptions = LayoutOptions.FillAndExpand,
                    Margin = 0,
                    WidthRequest = 1, // workaround for bug https://bugzilla.xamarin.com/show_bug.cgi?id=42921
                    InputTransparent = false,
                    TextColor = Color.Transparent,
                    VerticalOptions = LayoutOptions.FillAndExpand,
                };
                _button.SetBinding(
                    Button.BorderColorProperty,
                    nameof(BackgroundColor));
                //_button.SetBinding(Button.CommandProperty, "Command");
                //_button.SetBinding(Button.CommandParameterProperty, "CommandParameter");

                _grid = new Grid
                {
                    ClassId = "KCI_Grid",
                    ColumnDefinitions = new ColumnDefinitionCollection(),
                    ColumnSpacing = 0,
                    HeightRequest = 44,
                    HorizontalOptions = LayoutOptions.Start,
                    IsClippedToBounds = true,
                    IsVisible = true,
                    MinimumHeightRequest =
                        ViewsUsingXamarinForms.MyAppViewRelatedConstants.MinimumHeightOfTappableObject,
                    MinimumWidthRequest =
                        ViewsUsingXamarinForms.MyAppViewRelatedConstants.MinimumWidthOfTappableObject,
                    Opacity = 1.0,
                    Padding = 0,
                    RowDefinitions = new RowDefinitionCollection(),
                    RowSpacing = 0,
                    VerticalOptions = LayoutOptions.Center,
                    WidthRequest = 44,
                };
                _grid.SetBinding(
                    BackgroundColorProperty,
                    nameof(BackgroundColor));

                _grid.ColumnDefinitions.Add(new ColumnDefinition {Width = imageWidth});
                if (_inDepthDebugging)
                    _grid.ColumnDefinitions.Add(new ColumnDefinition {Width = imageWidth});
                _grid.RowDefinitions.Add(new RowDefinition {Height = imageWidth});
                if (_inDepthDebugging)
                    _grid.RowDefinitions.Add(new RowDefinition {Height = imageWidth});

                _grid.Children.Add(_untoggledImage, 0, 1, 0, 1);
                if (_inDepthDebugging)
                {
                    _grid.Children.Add(_toggledImage, 1, 2, 0, 1);
                    _grid.Children.Add(_button, 0, 2, 0, 2);
                }
                else
                {
                    _grid.Children.Add(_toggledImage, 0, 1, 0, 1);
                    _grid.Children.Add(_button, 0, 1, 0, 1);
                }

                this.BackgroundColor = Color.Transparent;
                this.HorizontalOptions = LayoutOptions.StartAndExpand;
                this.VerticalOptions = LayoutOptions.FillAndExpand;
                this.Padding = 0;

                return _grid;
            });
        } // constructor
    }

} // namespace ViewsUsingXamarinForms

// eof



using System;
using System.Globalization;

using Xamarin.Forms;

namespace ViewsUsingXamarinForms
{
    /// <summary>
    /// This converter class is used to help debug binding issues.
    /// It is not expected to be used in production code.
    /// </summary>
    public class MyAppDummyConverter : IValueConverter
    {
        public static MyAppDummyConverter Instance = new MyAppDummyConverter();

        private MyAppDummyConverter()
        {
            // no-op
        }

        public object Convert(object value, Type targetType,
                                object parameter, CultureInfo culture)
        {
            return value;
        }

        public object ConvertBack(object value, Type targetType,
                                    object parameter, CultureInfo culture)
        {
            return value;
        }

    } // public class MyAppDummyConverter : IValueConverter

} // namespace ViewsUsingXamarinForms

// eof





using System;

using Xamarin.Forms;

namespace ViewsUsingXamarinForms
{
    public class MyAppNegateBooleanConverter : IValueConverter
    {
        public static MyAppNegateBooleanConverter Instance = new MyAppNegateBooleanConverter();

        private MyAppNegateBooleanConverter()
        {
            // no-op
        }

        public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            System.Diagnostics.Debug.WriteLine("In MyAppNegateBooleanConverter.Convert. Input value = {0}, TargetType = {1}", value, targetType );
            if (value is bool)
                return !((bool)value);
            return value;
        }
        public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            System.Diagnostics.Debug.WriteLine("In MyAppNegateBooleanConverter.ConvertBack. Input value = {0}, TargetType = {1}", value, targetType);
            if (value is bool)
                return !((bool)value);
            return value;
        }

    } // public class MyAppNegateBooleanConverter : IValueConverter

} // namespace ViewsUsingXamarinForms

// eof



using System;

using Xamarin.Forms;

using CommonInfrastructure;

namespace ViewsUsingXamarinForms
{
    public class MyAppBooleanToDoubleTrueToZeroFalseToOneConverter : IValueConverter
    {
        public static MyAppBooleanToDoubleTrueToZeroFalseToOneConverter Instance = new MyAppBooleanToDoubleTrueToZeroFalseToOneConverter();

        private MyAppBooleanToDoubleTrueToZeroFalseToOneConverter()
        {
            // no-op
        }

        public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            if (value is bool)
            {
                bool b = (bool)value;
                double d = b ? 0.0 : 1.0;
                System.Diagnostics.Debug.WriteLine("Convert (hopefully opposite) from {0} to {1}", b, d);

                IDebuggingService service = DependencyService.Get<IDebuggingService>();
                if ((service != null) && service.StackTraceAvailable)
                {
                    System.Diagnostics.Debug.WriteLine("Above line with callstack = {0}", service.StackTrace);
                }
                return (Double) d;
            }
            else
            {
                throw new ArgumentException(
                    string.Format(MyAppExceptions.ValueConverter_SourceNotBooleanForNegation, value.GetType()));
            }
        }

        public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            if (value is double)
            {
                double d = (double)value;
                bool b = (d <= 0.5);
                System.Diagnostics.Debug.WriteLine("ConvertBack (hopefully opposite) from {0} to {1}", d, b);
                return b;
            }
            else
            {
                throw new ArgumentException(
                    string.Format(MyAppExceptions.ValueConverter_SourceNotDoubleForNegation, value.GetType()));
            }
        }

    } // public class MyAppBooleanToDoubleTrueToZeroFalseToOneConverter : IValueConverter

} // namespace ViewsUsingXamarinForms

// eof



using System;

using Xamarin.Forms;

using CommonInfrastructure;

namespace ViewsUsingXamarinForms
{
    public class MyAppBooleanToDoubleTrueToOneFalseToZeroConverter : IValueConverter
    {
        public static MyAppBooleanToDoubleTrueToOneFalseToZeroConverter Instance = new MyAppBooleanToDoubleTrueToOneFalseToZeroConverter();

        private MyAppBooleanToDoubleTrueToOneFalseToZeroConverter()
        {
            // no-op
        }

        public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            if (value is bool)
            {
                bool b = (bool) value;
                double d = b ? 1.0 : 0.0;
                System.Diagnostics.Debug.WriteLine("Convert (hopefully same) from {0} to {1}", b, d);
                return d;
            }
            else
            {
                throw new ArgumentException(
                    string.Format(MyAppExceptions.ValueConverter_SourceNotBoolean, value.GetType()));
            }
        }

        public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            if (value is double)
            {
                double d = (double) value;
                bool b = (d > 0.5);
                return b;
            }
            else
            {
                throw new ArgumentException(
                    string.Format(MyAppExceptions.ValueConverter_SourceNotDouble, value.GetType()));
            }
        }

    } // public class MyAppBooleanToDoubleTrueToOneFalseToZeroConverter : IValueConverter

} // namespace ViewsUsingXamarinForms

// eof
Comment 5 John Hardman 2016-10-03 16:39:25 UTC
Note that the issue mentioned in the code about images being sized incorrectly on UWP has now been raised as bug 44981.
Comment 6 John Hardman 2016-10-03 19:02:09 UTC
@Paul DiPietro - I am wondering whether 44680, 36097 and 44981 are somehow related. Whilst using Opacity rather than IsVisible works, so there is definitely an issue with IsVisible, and changing the state of IsVisible definitely triggers an issue around sizing of an Image, it's also true that when 44680 happens, re-sizing the containing window results in the image then being drawn. It all feels intertwined, but whether one code change in the XF Layout handling would resolve all three, or whether one code change in the XF Image handling would resolve all three, or whether it takes three different fixes, I don't know.
Comment 7 John Hardman 2016-10-03 20:11:28 UTC
Here's an updated portion of the above code, to allow use of AbsoluteLayout instead of Grid. (I haven't copied the converters here, so you'll need to get those from the previous sample)

using System.Threading.Tasks;
using System.Windows.Input;

using Xamarin.Forms;

using CommonInfrastructure;

namespace ViewsUsingXamarinForms
{
    public class MyAppToggleImageButton : MyAppTemplatedContentView
    {
        private const bool _inDepthDebugging = false; // To assist in debugging
        private const bool _useAbsoluteLayoutForPerformance = true; // what I will use in production
        private const bool _useJustOneImage = false; // what I will use in production
        private const bool _explicitlyReleaseImageSourcesToAvoidMemoryLeak = true; // awaiting review by others to confirm whether this is a Xamarin problem, as no obvious reason why having this set to false would prevent containing page from being freed up
        private const bool _usingPropertiesToWorkardoundBug41266 = false; // ok for testing purposes, but will need to replace in production code
        private bool _explicitlySetLayoutOptionsToPreventDifferentSizeImagesToWorkaround44981 = false; // Device.OS == TargetPlatform.Windows; // using false in conjunction with setting _useOpacityToWorkaroundBug44680 to false, highlights an unreported sizing issue on UWP
        private bool _useOpacityToWorkaroundBug44680 = false; // Device.OS == TargetPlatform.Windows; // setting this to false on UWP shows up multiple XF problems

        private AbsoluteLayout _absoluteLayout;

        /// <summary>
        /// A grid that is used to position the near-transparent button over the content.
        /// </summary>
        private Grid _grid;

        /// <summary>
        /// A label used to present information during in-depth debugging
        /// </summary>
        private Label _label;

        /// <summary>
        /// At present, this control uses two Image objects at all times.
        /// Will re-write shortly to only have one in the UI hierarchy,
        /// instead changing the ImageSource associated with the Image.
        /// </summary>
        private Image _toggledImage;
        private Image _untoggledImage;

        // And when we use just one image, this will be the one.
        private Image TheOneImage => _toggledImage;

        /// <summary>
        /// The ImageSource objects used for the two toggle states.
        /// Note that under certain circumstances, these need to be 
        /// explicitly set to null in order to allow the containing
        /// page to be freed up. Am awaiting separate review to confirm
        /// whether this is a misunderstanding of the garbage collection
        /// mechanism on my part, or whether it is a Xamarin bug. 
        /// It appears currently to be the latter.
        /// </summary>
        private ImageSource _toggledImageSource;
        private ImageSource _untoggledImageSource;

        public static readonly BindableProperty AnimateProperty 
            = BindableProperty.Create(
                nameof(Animate), 
                typeof(bool),
                typeof(MyAppToggleImageButton),
                default(bool));

        // A Xamarin.Forms bug (https://bugzilla.xamarin.com/show_bug.cgi?id=40817 )
        // means that we can only set AutomationId once for each View. Attempting to
        // set AutomationId a second time results in an exception. As a result, we
        // should not rely on the value in AutomationId currently, so should not use 
        // it as a key for example. We include binding support here in preparation for 
        // the bug being fixed.
        public static readonly BindableProperty AutomationIdProperty
            = BindableProperty.Create(
                nameof(AutomationId),
                typeof(string),
                typeof(MyAppContentButton),
                default(string));

        // Intentionally hiding BackgroundColorProperty in base class.
        public new static readonly BindableProperty BackgroundColorProperty
            = BindableProperty.Create(
                nameof(BackgroundColor),
                typeof (Color),
                typeof (MyAppContentButton),
                Color.Default);

        public static readonly BindableProperty CommandProperty
            = BindableProperty.Create(
                nameof(Command),
                typeof(ICommand),
                typeof(MyAppToggleImageButton),
                default(ICommand));

        public static readonly BindableProperty CommandParameterProperty
            = BindableProperty.Create(
                nameof(CommandParameter),
                typeof(object),
                typeof(MyAppToggleImageButton),
                default(object));

        public static readonly BindableProperty IsToggledProperty
            = BindableProperty.Create(
                nameof(IsToggled),
                typeof(bool),
                typeof(MyAppToggleImageButton),
                default(bool),
                BindingMode.TwoWay);

        public static readonly BindableProperty ToggledImageSourceProperty
            = BindableProperty.Create(
                nameof(ToggledImageSource),
                typeof (ImageSource),
                typeof (MyAppToggleImageButton),
                null);

        public static readonly BindableProperty UntoggledImageSourceProperty
            = BindableProperty.Create(
                nameof(UntoggledImageSource),
                typeof(ImageSource),
                typeof(MyAppToggleImageButton),
                null);

        public bool Animate
        {
            get { return (bool)GetValue(AnimateProperty); }
            set { SetValue(AnimateProperty, value); }
        }

        public new string AutomationId
        {
            get { return (string) GetValue(AutomationIdProperty); } //  return base.AutomationId; }

            set
            {
                // A Xamarin.Forms bug (https://bugzilla.xamarin.com/show_bug.cgi?id=40817 )
                // means that we can only set AutomationId once for each View. Attempting to
                // set AutomationId a second time results in an exception. As a result, we
                // should not rely on the value in AutomationId, so should not use it as a key
                // for example.
                if (AutomationId == null)
                {
                    base.AutomationId = value;
                    if (TheOneImage != null)
                        TheOneImage.AutomationId = value + "_image";
                    else
                    {
                        if (_toggledImage != null)
                            _toggledImage.AutomationId = value + "_toggled";
                        if (_untoggledImage != null)
                            _untoggledImage.AutomationId = value + "_untoggled";
                    }
                }

                if (_inDepthDebugging)
                {
                    if (_label == null)
                    {
                        _label = new Label
                        {
                            HorizontalOptions = LayoutOptions.CenterAndExpand,
                            HorizontalTextAlignment = TextAlignment.Center,
                            InputTransparent = true,
                            Text = value ?? "{null}",
                            TextColor = Color.Black,
                            FontSize = Device.GetNamedSize(NamedSize.Small, typeof(Label))
                        };
                        if (_useAbsoluteLayoutForPerformance)
                        {
                            _absoluteLayout.Children.Add(_label, new Rectangle(0, 22, 44, 22));
                        }
                        else
                        {
                            _grid.Children.Add(_label, 0, 2, 1, 2);
                        }
                    }
                }

                SetValue(AutomationIdProperty, value);
            }
        }

        /// <summary>
        /// Command to execute when the button is pressed.
        /// </summary>
        public ICommand Command
        {
            get { return (ICommand)GetValue(CommandProperty); }
            set { SetValue(CommandProperty, value); }
        }

        /// <summary>
        /// Parameter to pass to command handler when the button is pressed.
        /// </summary>
        public object CommandParameter
        {
            get { return GetValue(CommandParameterProperty); }
            set { SetValue(CommandParameterProperty, value); }
        }

        /// <summary>
        /// Current state of the button. Typically toggled will equate to checked or expanded or similar
        /// in derived classes.
        /// </summary>
        public bool IsToggled
        {
            get { return (bool)GetValue(IsToggledProperty); }
            set { SetValue(IsToggledProperty, value); }
        }

        public ImageSource ToggledImageSource
        {
            get { return (ImageSource)GetValue(ToggledImageSourceProperty); }
            set { SetValue(ToggledImageSourceProperty, value); }
        }

        public ImageSource UntoggledImageSource
        {
            get { return (ImageSource) GetValue(UntoggledImageSourceProperty); }
            set { SetValue(UntoggledImageSourceProperty, value); }
        }

        protected override void OnBindingContextChanged()
        {
            base.OnBindingContextChanged();

            if (BindingContext == null)
            {
                if (_useJustOneImage)
                {
                    TheOneImage.Source = null;
                }
                else if (_explicitlyReleaseImageSourcesToAvoidMemoryLeak)
                {
                    _untoggledImage.Source = null;
                    _toggledImage.Source = null;
                }
            }
            else if (_useJustOneImage)
            {
                TheOneImage.Source = IsToggled ? _toggledImageSource : _untoggledImageSource;
            }
            else
            {
                _untoggledImage.Source = _untoggledImageSource;
                _toggledImage.Source = _toggledImageSource;
            }
        }

        protected async void OnButtonPressed()
        {
            IsToggled = !IsToggled;

            if (_useJustOneImage)
            {
                TheOneImage.Source = IsToggled ? _toggledImageSource : _untoggledImageSource;
            }
            else if (_usingPropertiesToWorkardoundBug41266)
            {
                if (_useOpacityToWorkaroundBug44680)
                {
                    _toggledImage.Opacity = IsToggled ? 1.0 : 0.0;
                    _untoggledImage.Opacity = IsToggled ? 0.0 : 1.0;
                }
                else
                {
                    _toggledImage.IsVisible = IsToggled;
                    _untoggledImage.IsVisible = !IsToggled;
                }
            }
            else
            {
                // no-op, as handled by bindings
            }

            if (Animate)
            {
                await this.ScaleTo(0.8, 50, Easing.Linear);
                await Task.Delay(100);
                await this.ScaleTo(1, 50, Easing.Linear);
            }

            ICommand command = (ICommand)GetValue(CommandProperty);
            if (command != null)
            {
                object commandParameter = GetValue(CommandParameterProperty);
                command.Execute(commandParameter);
            }
        }

        protected override void OnParentSet()
        {
            base.OnParentSet();
            TheOneImage.Source = IsToggled ? _toggledImageSource : _untoggledImageSource;
            //??? need to set Content here?
        }

        public MyAppToggleImageButton(
            ImageSource untoggledImageSource,
            ImageSource toggledImageSource,
            bool initiallyToggled)
            : base()
        {
            _untoggledImageSource = untoggledImageSource;
            _toggledImageSource = toggledImageSource;

            // look into changing this to re-use DataTemplate
            ContentTemplate = new DataTemplate(() =>
            {
                int imageWidth = _inDepthDebugging ? 22 : 44;

                // although we overwrite the grid with images below, those images may be .png files with transparent pixels,
                // so we need to ensure the expected color is displayed beneath those transparent pixels.

                _toggledImage = new Image
                {
                    Aspect = Aspect.AspectFill,
                    Source = toggledImageSource,
                    InputTransparent = true,
                    HeightRequest = imageWidth,
                    WidthRequest = imageWidth
                };
                _toggledImage.SetBinding(
                    BackgroundColorProperty,
                    nameof(BackgroundColor));
                if (_explicitlySetLayoutOptionsToPreventDifferentSizeImagesToWorkaround44981)
                {
                    _toggledImage.HorizontalOptions = LayoutOptions.Start;
                    _toggledImage.VerticalOptions = LayoutOptions.Start;
                }

                Animate = true;

                if (_useJustOneImage)
                {
                    TheOneImage.Source = IsToggled ? _toggledImageSource : _untoggledImageSource;
                }
                else
                {
                    _untoggledImage = new Image
                    {
                        Aspect = Aspect.AspectFill,
                        Source = untoggledImageSource,
                        InputTransparent = true,
                        HeightRequest = imageWidth,
                        WidthRequest = imageWidth
                    };
                    _untoggledImage.SetBinding(
                        BackgroundColorProperty,
                        nameof(BackgroundColor));
                    if (_explicitlySetLayoutOptionsToPreventDifferentSizeImagesToWorkaround44981)
                    {
                        _untoggledImage.HorizontalOptions = LayoutOptions.Start;
                        _untoggledImage.VerticalOptions = LayoutOptions.Start;
                    }

                    if (_usingPropertiesToWorkardoundBug41266)
                    {
                        if (_useOpacityToWorkaroundBug44680)
                        {
                            _toggledImage.Opacity = IsToggled ? 1.0 : 0.0;
                            _untoggledImage.Opacity = IsToggled ? 0.0 : 1.0;
                        }
                        else
                        {
                            _toggledImage.IsVisible = IsToggled;
                            _untoggledImage.IsVisible = !IsToggled;
                        }
                    }
                    else if (_useOpacityToWorkaroundBug44680)
                    {
                        // opacity with bindings

                        Binding untoggledBinding = new Binding
                        {
                            Source = this,
                            Path = nameof(MyAppToggleImageButton.IsToggled),
                            Mode = BindingMode.OneWay,
                            Converter = MyAppBooleanToDoubleTrueToZeroFalseToOneConverter.Instance
                        };
                        _untoggledImage.SetBinding(
                            Image.OpacityProperty, untoggledBinding); // TODO - change this back

                        Binding toggledBinding = new Binding
                        {
                            Source = this,
                            Path = nameof(MyAppToggleImageButton.IsToggled),
                            Mode = BindingMode.OneWay,
                            Converter = MyAppBooleanToDoubleTrueToOneFalseToZeroConverter.Instance
                        };
                        _toggledImage.SetBinding(
                            Image.OpacityProperty, toggledBinding);
                    }
                    else
                    {
                        // visibility with bindings

                        // Note that this exhibits a Xamarin bug that I haven't
                        // reported yet - when IsToggled is changed, the images
                        // are different sizes if I let HorizontalOptions and
                        // VerticalOptions use their default values. My guess is
                        // that this results from an Image having IsVisible=false
                        // when first laid out. I'm guessing that when IsVisible
                        // is later set to true, the layout mechanism skips some
                        // of the properties explicitly specified in the code.

                        Binding untoggledBinding = new Binding
                        {
                            Source = this,
                            Path = nameof(MyAppToggleImageButton.IsToggled),
                            Mode = BindingMode.OneWay,
                            Converter = MyAppNegateBooleanConverter.Instance
                        };
                        _untoggledImage.SetBinding(
                            Image.IsVisibleProperty, untoggledBinding);

                        Binding toggledBinding = new Binding
                        {
                            Source = this,
                            Path = nameof(MyAppToggleImageButton.IsToggled),
                            Mode = BindingMode.OneWay,
                            Converter = MyAppDummyConverter.Instance,
                            // should be equivalent to not having this binding at all
                        };
                        _toggledImage.SetBinding(
                            Image.IsVisibleProperty, toggledBinding);
                    }
                }

                var button = new Button() // MyAppHoverButton
                {
                    Opacity = 0.02,
                    BorderRadius = 0,
                    BorderWidth = 0,
                    ClassId = "KCI_Button",
                    Command = new DelegateCommand(OnButtonPressed),
                    CommandParameter = null,
                    HorizontalOptions = LayoutOptions.FillAndExpand,
                    Margin = 0,
                    WidthRequest = 1, // workaround for bug https://bugzilla.xamarin.com/show_bug.cgi?id=42921
                    InputTransparent = false,
                    TextColor = Color.Transparent,
                    VerticalOptions = LayoutOptions.FillAndExpand,
                };
                button.SetBinding(
                    Button.BorderColorProperty,
                    nameof(BackgroundColor));
                //_button.SetBinding(Button.CommandProperty, "Command");
                //_button.SetBinding(Button.CommandParameterProperty, "CommandParameter");

                this.BackgroundColor = Color.Transparent;
                this.HorizontalOptions = LayoutOptions.StartAndExpand;
                this.VerticalOptions = LayoutOptions.FillAndExpand;
                this.Padding = 0;

                // TODO - change this to AbsoluteLayout
                if (_useAbsoluteLayoutForPerformance)
                {
                    _absoluteLayout = new AbsoluteLayout()
                    {
                        ClassId = "KCI_AbsoluteLayout",
                        HeightRequest = 44,
                        HorizontalOptions = LayoutOptions.Start,
                        IsClippedToBounds = true,
                        IsVisible = true,
                        MinimumHeightRequest =
                            ViewsUsingXamarinForms.MyAppViewRelatedConstants.MinimumHeightOfTappableObject,
                        MinimumWidthRequest =
                            ViewsUsingXamarinForms.MyAppViewRelatedConstants.MinimumWidthOfTappableObject,
                        Opacity = 1.0,
                        Padding = 0,
                        VerticalOptions = LayoutOptions.Center,
                        WidthRequest = 44,
                    };
                    _absoluteLayout.SetBinding(
                        BackgroundColorProperty,
                        nameof(BackgroundColor));

                    if (_useJustOneImage)
                    {
                        if (_inDepthDebugging)
                            _absoluteLayout.Children.Add(TheOneImage, new Rectangle(11, 0, 22, 44));
                        else
                            _absoluteLayout.Children.Add(TheOneImage, new Rectangle(0, 0, 44, 44));
                        _absoluteLayout.Children.Add(button, new Rectangle(0, 0, 44, 44));
                    }
                    else if (_inDepthDebugging)
                    {
                        _absoluteLayout.Children.Add(_untoggledImage, new Rectangle(0, 0, 22, 22));
                        _absoluteLayout.Children.Add(_toggledImage, new Rectangle(22, 0, 22, 22));
                        _absoluteLayout.Children.Add(button, new Rectangle(0, 0, 44, 44));
                    }
                    else
                    {
                        _absoluteLayout.Children.Add(_untoggledImage, new Rectangle(0, 0, 44, 44));
                        _absoluteLayout.Children.Add(_toggledImage, new Rectangle(0, 0, 44, 44));
                        _absoluteLayout.Children.Add(button, new Rectangle(0, 0, 44, 44));
                    }

                    return _absoluteLayout;
                }
                else
                {
                    _grid = new Grid
                    {
                        ClassId = "KCI_Grid",
                        ColumnDefinitions = new ColumnDefinitionCollection(),
                        ColumnSpacing = 0,
                        HeightRequest = 44,
                        HorizontalOptions = LayoutOptions.Start,
                        IsClippedToBounds = true,
                        IsVisible = true,
                        MinimumHeightRequest =
                            ViewsUsingXamarinForms.MyAppViewRelatedConstants.MinimumHeightOfTappableObject,
                        MinimumWidthRequest =
                            ViewsUsingXamarinForms.MyAppViewRelatedConstants.MinimumWidthOfTappableObject,
                        Opacity = 1.0,
                        Padding = 0,
                        RowDefinitions = new RowDefinitionCollection(),
                        RowSpacing = 0,
                        VerticalOptions = LayoutOptions.Center,
                        WidthRequest = 44,
                    };
                    _grid.SetBinding(
                        BackgroundColorProperty,
                        nameof(BackgroundColor));

                    _grid.ColumnDefinitions.Add(new ColumnDefinition {Width = imageWidth});
                    if (_inDepthDebugging && (!_useJustOneImage))
                        _grid.ColumnDefinitions.Add(new ColumnDefinition {Width = imageWidth});
                    _grid.RowDefinitions.Add(new RowDefinition {Height = imageWidth});
                    if (_inDepthDebugging && (!_useJustOneImage))
                        _grid.RowDefinitions.Add(new RowDefinition {Height = imageWidth});

                    if (_useJustOneImage)
                    {
                        _grid.Children.Add(TheOneImage, 0, 1, 0, 1);
                        _grid.Children.Add(button, 0, 1, 0, 1);
                    }
                    else
                    {
                        _grid.Children.Add(_untoggledImage, 0, 1, 0, 1);
                        if (_inDepthDebugging)
                        {
                            _grid.Children.Add(_toggledImage, 1, 2, 0, 1);
                            _grid.Children.Add(button, 0, 2, 0, 2);
                        }
                        else
                        {
                            _grid.Children.Add(_toggledImage, 0, 1, 0, 1);
                            _grid.Children.Add(button, 0, 1, 0, 1);
                        }
                    }

                    return _grid;
                }
            });
        } // constructor
    }

} // namespace ViewsUsingXamarinForms

// eof