Tim Okrongli
Tim Okrongli

Reputation: 169

Xamarin.Forms: Equivalent to CSS :last-of-type selector

I'm currently redesigning a page that displays contact information in a Xamarin.Forms app. The page will display a list of sections (address, phone numbers, email addresses etc.), each with an icon and relevant information. The sections should be separated by a line but no lines should be above the first and below the last section. Also, empty sections are not displayed at all.

The markup looks basically like this:

<ScrollView>
  <StackLayout>
    <Label Text="{Binding Contact.Name}" />
    <controls:ContactSection Icon="address.png">
      <!-- address-type stuff -->
    </controls:ContactSection>
    <controls:ContactSection Icon="phone.png">
      <!-- phone numbers -->
    </controls:ContactSection>
    <!-- further sections -->
  </StackLayout>
</ScrollView>

I've got it working for the most part except for the lines. (I just use BoxView with a HeightRequest of 1.) In order to make them work properly I'd need to tell the renderer to draw a line below every visible section except for the last one. In essence, I'd need a CSS3-style :not(:last-of-type) selector (or a :not(:first-of-type) one with the lines above).

What's the best way of doing that in XAML? (Or in the code-behind if necessary?)

Upvotes: 1

Views: 293

Answers (2)

You just reminded me that I've been wanting this for a while, so I wrote one (with snippets, it's a ten minute job). Let me know how/if this works with Xamarin; I'm not able to test with that.

UPDATE: I must be half asleep today. I read "StackLayout" as "StackPanel". OP adapted it to Xamarin and posted that working code as another answer.

using System;
using System.Windows;
using System.Windows.Controls;

namespace HollowEarth.AttachedProperties
{
    public static class PanelBehaviors
    {
        public static void UpdateChildFirstLastProperties(Panel panel)
        {
            for (int i = 0; i < panel.Children.Count; ++i)
            {
                var child = panel.Children[i];

                SetIsFirstChild(child, i == 0);
                SetIsLastChild(child, i == panel.Children.Count - 1);
            }
        }

        #region PanelExtensions.IdentifyFirstAndLastChild Attached Property
        public static bool GetIdentifyFirstAndLastChild(Panel panel)
        {
            return (bool)panel.GetValue(IdentifyFirstAndLastChildProperty);
        }

        public static void SetIdentifyFirstAndLastChild(Panel panel, bool value)
        {
            panel.SetValue(IdentifyFirstAndLastChildProperty, value);
        }

        /// <summary>
        /// Behavior which causes the Panel to identify its first and last children with attached properties. 
        /// </summary>
        public static readonly DependencyProperty IdentifyFirstAndLastChildProperty =
            DependencyProperty.RegisterAttached("IdentifyFirstAndLastChild", typeof(bool), typeof(PanelBehaviors),
                //  Default MUST be false, or else True won't be a change in 
                //  the property value, so PropertyChanged callback won't be 
                //  called, and nothing will happen. 
                new PropertyMetadata(false, IdentifyFirstAndLastChild_PropertyChanged));

        private static void IdentifyFirstAndLastChild_PropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            Panel panel = (Panel)d;

            ((Panel)d).LayoutUpdated += (s, e2) => UpdateChildFirstLastProperties(panel);
        }

        #endregion PanelExtensions.IdentifyFirstAndLastChild Attached Property

        #region PanelExtensions.IsFirstChild Attached Property
        public static bool GetIsFirstChild(UIElement obj)
        {
            return (bool)obj.GetValue(IsFirstChildProperty);
        }

        public static void SetIsFirstChild(UIElement obj, bool value)
        {
            obj.SetValue(IsFirstChildProperty, value);
        }

        /// <summary>
        /// True if UIElement is first child of a Panel
        /// </summary>
        public static readonly DependencyProperty IsFirstChildProperty =
            DependencyProperty.RegisterAttached("IsFirstChild", typeof(bool), typeof(PanelBehaviors),
                new PropertyMetadata(false));
        #endregion PanelExtensions.IsFirstChild Attached Property

        #region PanelExtensions.IsLastChild Attached Property
        public static bool GetIsLastChild(UIElement obj)
        {
            return (bool)obj.GetValue(IsLastChildProperty);
        }

        public static void SetIsLastChild(UIElement obj, bool value)
        {
            obj.SetValue(IsLastChildProperty, value);
        }

        /// <summary>
        /// True if UIElement is last child of a Panel
        /// </summary>
        public static readonly DependencyProperty IsLastChildProperty =
            DependencyProperty.RegisterAttached("IsLastChild", typeof(bool), typeof(PanelBehaviors),
                new PropertyMetadata(false));
        #endregion PanelExtensions.IsLastChild Attached Property
    }
}

Usage example:

<StackPanel 
    xmlns:heap="clr-namespace:HollowEarth.AttachedProperties"
    heap:PanelBehaviors.IdentifyFirstAndLastChild="True" 
    HorizontalAlignment="Left" 
    Orientation="Vertical"
    >
    <StackPanel.Resources>
        <Style TargetType="Label">
            <Setter Property="Content" Value="Blah blah" />
            <Setter Property="Background" Value="SlateGray" />
            <Setter Property="Margin" Value="4" />

            <Style.Triggers>
                <Trigger Property="heap:PanelBehaviors.IsFirstChild" Value="True">
                    <Setter Property="Background" Value="DeepSkyBlue" />
                    <Setter Property="Content" Value="First Child" />
                </Trigger>
                <Trigger Property="heap:PanelBehaviors.IsLastChild" Value="True">
                    <Setter Property="Background" Value="SeaGreen" />
                    <Setter Property="Content" Value="Last Child" />
                </Trigger>
            </Style.Triggers>
        </Style>
    </StackPanel.Resources>
    <Label />
    <Label />
    <Label />
    <Label />
</StackPanel>

Upvotes: 1

Tim Okrongli
Tim Okrongli

Reputation: 169

After Ed Plunkett supplied a solution for WPF I decided to post the Xamarin.Forms equivalent I built from his code.

namespace Foo.Behaviors
{
    using System.Linq;

    using Xamarin.Forms;

    /// <summary>
    ///     Identifies the first and last child of a <see cref="Layout{View}"/>.
    /// </summary>
    public class FirstAndLastChildBehavior
    {
        /// <summary>
        ///     Identifies the first and last child of the given <see cref="Layout{View}"/>.
        /// </summary>
        /// <param name="layout">The <see cref="Layout{View}"/>.</param>
        public static void UpdateChildFirstLastProperties(Layout<View> layout)
        {
            // This is just here to provide a convenient place to do filtering, e.g. .Where(v => v.IsVisible).
            var children = layout.Children;

            for (var i = 0; i < children.Length; ++i)
            {
                var child = children[i];

                SetIsFirstChild(child, i == 0);
                SetIsLastChild(child, i == children.Length - 1);
            }
        }

        #region PanelExtensions.IdentifyFirstAndLastChild Attached Property
        /// <summary>
        ///     Gets a value that controls whether the child-identifying functionality is enabled for the given <see cref="Layout{View}"/>.
        /// </summary>
        /// <param name="layout">The <see cref="Layout{View}"/>.</param>
        /// <returns><c>True</c> if functionality has been enabled, <c>false</c> otherwise.</returns>
        public static bool GetIdentifyFirstAndLastChild(Layout<View> layout)
        {
            return (bool)layout.GetValue(IdentifyFirstAndLastChildProperty);
        }

        /// <summary>
        ///     Sets a value that controls whether the child-identifying functionality is enabled for the given <see cref="Layout{View}"/>.
        /// </summary>
        /// <param name="layout">The <see cref="Layout{View}"/>.</param>
        /// <param name="value">The value.</param>
        public static void SetIdentifyFirstAndLastChild(Layout<View> layout, bool value)
        {
            layout.SetValue(IdentifyFirstAndLastChildProperty, value);
        }

        /// <summary>
        ///     Identifies the <see cref="IdentifyFirstAndLastChild"/> property.
        /// </summary>
        /// <remarks>
        ///     The behavior can't be turned off; once the value is set to <c>true</c> the behavior will stick even if it's set back to
        ///     <c>false</c> later.
        /// </remarks>
        public static readonly BindableProperty IdentifyFirstAndLastChildProperty = BindableProperty.CreateAttached(
            "IdentifyFirstAndLastChild",
            typeof(bool),
            typeof(FirstAndLastChildBehavior),
            false,
            BindingMode.OneWay,
            null,
            IdentifyFirstAndLastChildPropertyChanged);

        /// <summary>
        ///     Gets called when IdentifyFirstAndLastChildProperty changes.
        /// </summary>
        /// <param name="bindable">The object we're bound to.</param>
        /// <param name="oldValue">This parameter is not used.</param>
        /// <param name="newValue">This parameter is not used.</param>
        private static void IdentifyFirstAndLastChildPropertyChanged(BindableObject bindable, object oldValue, object newValue)
        {
            var layout = (Layout<View>)bindable;

            ((Layout<View>)bindable).LayoutChanged += (a, b) => UpdateChildFirstLastProperties(layout);
        }
        #endregion PanelExtensions.IdentifyFirstAndLastChild Attached Property

        #region PanelExtensions.IsFirstChild Attached Property
        /// <summary>
        ///     Gets a value that determines whether the given <see cref="View"/> is the first child of its parent.
        /// </summary>
        /// <param name="obj">The <see cref="View"/>.</param>
        /// <returns><c>True</c> if the <see cref="View"/> is the first child, <c>false</c> otherwise.</returns>
        public static bool GetIsFirstChild(View obj)
        {
            return (bool)obj.GetValue(IsFirstChildProperty);
        }

        /// <summary>
        ///     Sets a value that determines whether the given <see cref="View"/> is the first child of its parent.
        /// </summary>
        /// <param name="obj">The <see cref="View"/>.</param>
        /// <param name="value">The value.</param>
        public static void SetIsFirstChild(View obj, bool value)
        {
            obj.SetValue(IsFirstChildProperty, value);
        }

        /// <summary>
        ///     Identifies the <see cref="IsFirstChild"/> property.
        /// </summary>
        public static readonly BindableProperty IsFirstChildProperty = BindableProperty.CreateAttached(
            "IsFirstChild",
            typeof(bool),
            typeof(FirstAndLastChildBehavior),
            false);
        #endregion PanelExtensions.IsFirstChild Attached Property

        #region PanelExtensions.IsLastChild Attached Property
        /// <summary>
        ///     Gets a value that determines whether the given <see cref="View"/> is the last child of its parent.
        /// </summary>
        /// <param name="obj">The <see cref="View"/>.</param>
        /// <returns><c>True</c> if the <see cref="View"/> is the last child, <c>false</c> otherwise.</returns>
        public static bool GetIsLastChild(View obj)
        {
            return (bool)obj.GetValue(IsLastChildProperty);
        }

        /// <summary>
        ///     Sets a value that determines whether the given <see cref="View"/> is the last child of its parent.
        /// </summary>
        /// <param name="obj">The <see cref="View"/>.</param>
        /// <param name="value">The value.</param>
        public static void SetIsLastChild(View obj, bool value)
        {
            obj.SetValue(IsLastChildProperty, value);
        }

        /// <summary>
        ///     Identifies the <see cref="IsLastChild"/> property.
        /// </summary>
        public static readonly BindableProperty IsLastChildProperty = BindableProperty.CreateAttached(
            "IsLastChild",
            typeof(bool),
            typeof(FirstAndLastChildBehavior),
            false);
        #endregion PanelExtensions.IsLastChild Attached Property
    }
}

Upvotes: 1

Related Questions