Xamarin.Forms: эквивалент CSS: селектор последнего типа

#c# #xaml #xamarin.forms

#c# #xaml #xamarin.forms

Вопрос:

В настоящее время я перепроектирую страницу, на которой отображается контактная информация в Xamarin.Приложение Forms. На странице будет отображен список разделов (адрес, номера телефонов, адреса электронной почты и т.д.), Каждый со значком и соответствующей информацией. Разделы должны быть разделены строкой, но никакие строки не должны быть выше первого и ниже последнего раздела. Кроме того, пустые разделы вообще не отображаются.

Разметка выглядит в основном так:

 <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>
  

У меня это работает по большей части, за исключением строк. (Я просто использую BoxView с HeightRequest числом 1.) Чтобы заставить их работать должным образом, мне нужно было бы указать программе визуализации нарисовать линию под каждым видимым разделом, кроме последнего. По сути, мне нужен селектор в стиле CSS3 :not(:last-of-type) (или селектор :not(:first-of-type) со строками выше).

Каков наилучший способ сделать это в XAML? (Или в коде, если необходимо?)

Комментарии:

1. Это больной вопрос. Однажды я написал подкласс ListBox, который устанавливал логические свойства FirstItem / LastItem, прикрепленные к контейнерам элементов, которыми он владел. Я полагаю, вы могли бы написать поведение, которое делает то же самое с дочерними элементами StackPanel . Получив это, вы можете легко написать стиль с триггером для свойства LastItem.

Ответ №1:

Вы только что напомнили мне, что я давно этого хотел, поэтому я написал один (с фрагментами, это десятиминутная работа). Дайте мне знать, как / если это работает с Xamarin; Я не могу протестировать это.

ОБНОВЛЕНИЕ: сегодня я, должно быть, наполовину сплю. Я прочитал «StackLayout» как «StackPanel». OP адаптировал его к Xamarin и опубликовал этот рабочий код в качестве другого ответа.

 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
    }
}
  

Пример использования:

 <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>
  

Комментарии:

1. Спасибо, это работает! Я добавлю версию вашего кода на Xamarin в качестве собственного ответа для полноты, но ваш, безусловно, «правильный».

2. @TimOkrongli Это здорово. И спасибо за публикацию версии Xamarin.

Ответ №2:

После того, как Эд Планкетт предоставил решение для WPF, я решил опубликовать Xamarin.Эквивалентные формы я построил из его кода.

 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
    }
}