0

I try to show a stack of images with overlaying buttons in the UI. Those buttons should be transperent and have an icon at the very top or bottom. While the "buttons" are working, the stack of images does not (or to be more precise the parent grid. Howevern, I'm not 100% sure yet). I would expect a result similar to this one: https://i.stack.imgur.com/qkOeu.png (Orange = Up.png / Down.png; transparent Green = upper CommandGrid / Button; transparent Blue = lower CommandGrid / Button; Black and Red the image stack while red is the selected "level").

This is my code so far:

<Grid HorizontalOptions="End" VerticalOptions="StartAndExpand" Margin="10">
    <Grid.RowDefinitions>
        <RowDefinition Height="*" />
        <RowDefinition Height="*" />
    </Grid.RowDefinitions>

    <controls:ItemsStack Grid.Row="0" Grid.RowSpan="2" ItemsSource="{Binding Levels}" Margin="0,15" Spacing="-8">
        <controls:ItemsStack.ItemTemplate>
            <DataTemplate>
                <controls:TintedImage Source="{Binding Source}" Foreground="{Binding Selected, Converter={StaticResource MapLevelColorConverter}}" />
            </DataTemplate>
        </controls:ItemsStack.ItemTemplate>
    </controls:ItemsStack>

    <controls:CommandGrid Grid.Row="0" Command="{Binding NavigateLevelCommand}" CommandParameter="{Binding LevelUpKey}" MinimumHeightRequest="32" Margin="0" Padding="0">
        <controls:TintedImage Source="Up.png" Foreground="{DynamicResource AccentColor}" VerticalOptions="Start" HorizontalOptions="Center" />
    </controls:CommandGrid>

    <controls:CommandGrid Grid.Row="1" Command="{Binding NavigateLevelCommand}" CommandParameter="{Binding LevelDownKey}" MinimumHeightRequest="32" Margin="0" Padding="0">
        <controls:TintedImage Source="Down.png" Foreground="{DynamicResource AccentColor}" VerticalOptions="End" HorizontalOptions="Center" />
    </controls:CommandGrid>
</Grid>

This is the ItemsStack (just a bindable StackLayout):

public class ItemsStack : StackLayout
{
    public static readonly BindableProperty ItemsSourceProperty =
        BindableProperty.Create(nameof(ItemsSource), typeof(IEnumerable), typeof(ItemsStack),
            default(IEnumerable<object>), BindingMode.TwoWay, null, ItemsSourceChanged);

    public static readonly BindableProperty ItemTemplateProperty =
        BindableProperty.Create(nameof(ItemTemplate), typeof(DataTemplate), typeof(ItemsStack),
            default(DataTemplate), BindingMode.TwoWay);

    public event EventHandler<SelectedItemChangedEventArgs> SelectedItemChanged;

    public IEnumerable ItemsSource
    {
        get => (IEnumerable)GetValue(ItemsSourceProperty);
        set => SetValue(ItemsSourceProperty, value);
    }

    public DataTemplate ItemTemplate
    {
        get => (DataTemplate)GetValue(ItemTemplateProperty);
        set => SetValue(ItemTemplateProperty, value);
    }

    private static void ItemsSourceChanged(BindableObject bindable, object oldValue, object newValue)
    {
        var itemsLayout = (ItemsStack)bindable;
        itemsLayout.SetItems();
    }

    protected readonly ICommand ItemSelectedCommand;

    protected virtual void SetItems()
    {
        Children.Clear();

        if (ItemsSource == null)
        {
            ObservableSource = null;
            return;
        }

        var t = ItemsSource.GetType();
        if (t.IsConstructedGenericType && t.GetGenericTypeDefinition() == typeof(ObservableCollection<>))
        {
            var o = Activator.CreateInstance(typeof(ObservableReadOnlyCollection<>).MakeGenericType(t.GenericTypeArguments), ItemsSource);
            ObservableSource = (IObservableReadOnlyCollection<object>)o;
        }
        else
        {
            foreach (var item in ItemsSource)
                Children.Add(GetItemView(item));
        }
    }

    protected virtual View GetItemView(object item)
    {
        DataTemplate template;
        if (ItemTemplate is DataTemplateSelector selector)
            template = selector.SelectTemplate(item, this);
        else
            template = ItemTemplate;

        var content = template.CreateContent();

        var view = content as View;
        if (view == null)
            return null;

        view.BindingContext = item;

        var gesture = new TapGestureRecognizer
            {
                Command = ItemSelectedCommand,
                CommandParameter = item
            };

        return view;
    }

    IObservableReadOnlyCollection<object> _observableSource;
    protected IObservableReadOnlyCollection<object> ObservableSource
    {
        get => _observableSource;
        set
        {
            if (_observableSource != null)
            {
                _observableSource.CollectionChanged -= CollectionChanged;
            }
            _observableSource = value;

            if (_observableSource != null)
            {
                _observableSource.CollectionChanged += CollectionChanged;
            }
        }
    }

    private void CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        switch (e.Action)
        {
            case NotifyCollectionChangedAction.Add:
                {
                    var index = e.NewStartingIndex;
                    foreach (var item in e.NewItems)
                        Children.Insert(index++, GetItemView(item));
                }
                break;
            case NotifyCollectionChangedAction.Move:
                {
                    var item = ObservableSource[e.OldStartingIndex];
                    Children.RemoveAt(e.OldStartingIndex);
                    Children.Insert(e.NewStartingIndex, GetItemView(item));
                }
                break;
            case NotifyCollectionChangedAction.Remove:
                {
                    Children.RemoveAt(e.OldStartingIndex);
                }
                break;
            case NotifyCollectionChangedAction.Replace:
                {
                    Children.RemoveAt(e.OldStartingIndex);
                    Children.Insert(e.NewStartingIndex, GetItemView(ObservableSource[e.NewStartingIndex]));
                }
                break;
            case NotifyCollectionChangedAction.Reset:
                Children.Clear();
                foreach (var item in ItemsSource)
                    Children.Add(GetItemView(item));
                break;
        }
    }
}

And this is the CommandGrid (basically a "button" with custom content and transparency):

public class CommandGrid : Grid
{
    public static readonly BindableProperty CommandProperty =
        BindableProperty.Create(nameof(Command), typeof(ICommand), typeof(CommandGrid),
            default(ICommand), propertyChanged: OnCommandPropertyChanged);

    public static readonly BindableProperty CommandParameterProperty =
        BindableProperty.Create(nameof(CommandParameter), typeof(object), typeof(CommandGrid),
            default(object), propertyChanged: OnCommandPropertyChanged);

    public ICommand Command
    {
        get => (ICommand)GetValue(CommandProperty);
        set => SetValue(CommandProperty, value);
    }

    public object CommandParameter
    {
        get => GetValue(CommandParameterProperty);
        set => SetValue(CommandParameterProperty, value);
    }

    private static void OnCommandPropertyChanged(BindableObject bindable, object oldvalue, object newvalue)
    {
        if (bindable is CommandGrid grid)
        {
            grid.GestureRecognizers.Clear();
            grid.GestureRecognizers.Add(new TapGestureRecognizer { Command = grid.Command, CommandParameter = grid.CommandParameter });
        }
    }
}

I'm now trying to fix this the whole day and have no ideas anymore how I can possible fix it...

Edit: This is what it currently looks like (far away from how it should look): https://i.stack.imgur.com/EyNAc.png

Edit 2: As for now, to have a solution to work with at first place, I used code behind to solve this. But I should clearly do some more research on this and maybe take a look at it after a break (or if someone here has a good idea how it could work or what is wrong here). This is how I "solved" it for now:

private async void SetLevelSize()
{
    await Task.Delay(200);

    var requiredHeight = 64d;
    if (ViewModel.ShowMapLevelIcon)
    {
        LevelStack.Margin = ViewModel.ShowMapLevelButtons ? new Thickness(0, 15) : new Thickness(0);
        requiredHeight = LevelStack.Children.Sum(c => c.Height + c.Margin.Top + c.Margin.Bottom) + LevelStack.Spacing * (LevelStack.Children.Count - 1) + LevelStack.Margin.Top + LevelStack.Margin.Bottom;
        requiredHeight = Math.Max(requiredHeight, 64);
    }

    LevelGrid.HeightRequest = requiredHeight;
}
Sebastian
  • 259
  • 3
  • 18
  • Could you post the `NavigateLevelCommand` method code? I don't see it in the code that's shown. That might be the area with the issue. – TaylorD May 03 '19 at 20:28
  • @TaylorD for what exactly should the command binding be relevant to this? It's just a ICommand within my ViewModel. – Sebastian May 03 '19 at 20:36
  • If it's not relevant, then I'm probably having trouble understanding the question. It says what you expect, but not what you're experiencing. I was thinking that the images weren't changing colors appropriately when selected/NavigateLevelCommand was executed. If that's the case, one of the command methods might be at fault or you could be missing an INotifyPropertyChanged. – TaylorD May 03 '19 at 20:46
  • @TaylorD No, the colors and navigations are working fine... the issue is that I'm not able to arrange them the way I want them to. The two "buttons" are currently both stacking at the top while the image stack expands below the bottom button - which should not be possible. The problem is, the ItemStack does not set it's height properly so the parent grid does not rearrange to the child size (it has just the size for the buttons even if the image stack is way bigger). And I don't get it why the ItemStack won't set it's height and notify the parent about it as it should work... – Sebastian May 03 '19 at 22:21
  • @TaylorD See edit above. I added an image how it currently looks so that there is a visual representation how it is (and how it should be which was there from the beginning). – Sebastian May 03 '19 at 22:23
  • why don't you use three rows?set your `ItemsStack` `Grid.Row = "1"` Up.png `Grid.Row = "0" `, Down.png `Grid.Row = "2"` ? – Leo Zhu May 06 '19 at 02:14
  • @LeoZhu-MSFT because it would decrease the touchable area a lot which will lead into a worse UX. Also in this case the Grid would be the wrong layout because a StackLayout would be the way to go. But as I already mentioned it is nothing I want. – Sebastian May 07 '19 at 08:29

0 Answers0