DragDropListBox

WPF ListBox subclass with added drag/drop support including an Adorner.

Consumers should use the ItemDropOn and ItemDragOver events, which provide the item in the collection the data was dropped on, as well as its index in the collection. There’s also built-in support for reordering items in the ListBox itself by drag and drop. All you have to do is set AllowDragReorder=”True” in the XAML.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;

namespace HollowEarthIndustries
{
    public class DragDropListBox : ListBox
    {
        public DragDropListBox()
        {
            AllowDrop = true;
            DragEnter += DragDropListBox_DragEnter;
            DragOver += DragDropListBox_DragOver;
            DragLeave += DragDropListBox_DragLeave;
            Drop += DragDropListBox_Drop;
        }

        /// <summary>
        /// If ItemsSource is in use, you can't insert into Items.
        /// 
        /// If ItemsSource is in use but it's IEnumerable, as far as I know you can't insert into anything,
        /// but maybe some consumer is doing something clever, so we still allow drop in that case. 
        /// </summary>
        /// <returns></returns>
        public System.Collections.IList GetInsertableCollection()
        {
            if (ItemsSource == null)
                return Items;

            return ItemsSource as System.Collections.IList;
        }

        protected override DependencyObject GetContainerForItemOverride()
        {
            //  We configure the ListBoxItems here rather than with a Style because this is the 
            //  simplest way to allow consumers to use ItemContainerStyle without stepping on 
            //  our internals. 

            var itemContainer = new ListBoxItem
            {
                //  TODO: We need to update the ListBoxItems when the ListBox's AllowDrop changes
                //  TODO: What if consumer provides literal ListBoxItem elements in the XAML? Derp.
                AllowDrop = true,
            };

            //  Events for items as object of drag/drop
            itemContainer.DragEnter += ItemContainer_DragEnter;
            itemContainer.DragOver += ItemContainer_DragOver;
            itemContainer.DragLeave += ItemContainer_DragLeave;
            itemContainer.Drop += ItemContainer_Drop;

            //  Events for items as subject of drag/drop
            itemContainer.PreviewMouseLeftButtonDown += ItemContainer_PreviewMouseLeftButtonDown;
            itemContainer.PreviewMouseMove += ItemContainer_PreviewMouseMove;

            return itemContainer;
        }

        #region Events
        #region ItemDropOn event
        public event EventHandler<ItemDragDropEventArgs> ItemDropOn;

        private string ReorderFormat { get; } = typeof(ItemDragData).FullName;

        protected void OnItemDropOn(DragEventArgs e, object targetItem, int targetIndex)
        {
            if (IsDragReorderingSelf(e))
            {
                var data = (ItemDragData)e.Data.GetData(ReorderFormat);

                System.Collections.IList list = GetInsertableCollection();

                if (list != null)
                {
                    if (!e.KeyStates.HasFlag(DragDropKeyStates.ControlKey) || !AllowDragReorderCopy)
                    {
                        if (data.SourceItemIndex < targetIndex)
                        {
                            //  If we're removing an item that precedes the target item,
                            //  adjust targetIndex down to account for the target item's 
                            //  changed position. 
                            --targetIndex;
                        }
                        list.RemoveAt(data.SourceItemIndex);
                    }

                    list.Insert(targetIndex, data.DragItem);

                    return;
                }
            }
            ItemDropOn?.Invoke(this, new ItemDragDropEventArgs(e, targetItem, targetIndex));
        }
        #endregion ItemDropOn event

        #region ItemDragOver event
        public event EventHandler<ItemDragDropEventArgs> ItemDragOver;

        protected bool IsDragReorderingSelf(DragEventArgs e)
        {
            if (AllowDragReorder && e.Data.GetDataPresent(ReorderFormat))
            {
                var data = (ItemDragData)e.Data.GetData(ReorderFormat);

                if (data.Source == this)
                {
                    return true;
                }
            }
            return false;
        }

        protected void OnItemDragOver(DragEventArgs e, object targetItem, int targetIndex)
        {
            if (IsDragReorderingSelf(e))
            {
                if (e.KeyStates.HasFlag(DragDropKeyStates.ControlKey) && AllowDragReorderCopy)
                {
                    e.Effects = DragDropEffects.Copy;
                }
                else
                {
                    e.Effects = DragDropEffects.Move;
                }
            }

            ItemDragOver?.Invoke(this, new ItemDragDropEventArgs(e, targetItem, targetIndex));
        }
        #endregion ItemDropOn event
        #endregion Events

        #region Drag Over Helpers
        protected void SetDragAfterLastItem(bool isDragging)
        {
            if (HasItems)
            {
                UpdateAdorner(GetLastListBoxItem(), isDragging, false);
            }
        }

        protected void UpdateAdorner(ListBoxItem lbi, bool isDragging, bool insertingBefore)
        {
            var adornerLayer = AdornerLayer.GetAdornerLayer(lbi);
            adornerLayer.IsHitTestVisible = false;

            if (isDragging)
            {
                //  It's a nullable bool; adornerLayer.GetAdorners() is old fashioned and may return null. 
                if (adornerLayer.GetAdorners(lbi)?.OfType<DropTargetAdorner>().Any() != true)
                {
                    adornerLayer.Add(/*_adorner = */new DropTargetAdorner(lbi, insertingBefore, TargetMarkerBrush));
                }
            }
            else
            {
                adornerLayer.GetAdorners(lbi)?.OfType<DropTargetAdorner>().ToList().ForEach(a => adornerLayer.Remove(a));
            }
        }

        protected ListBoxItem GetLastListBoxItem()
        {
            if (HasItems)
            {
                return (ListBoxItem)ItemContainerGenerator.ContainerFromIndex(Items.Count - 1);
            }
            return null;
        }

        #endregion Drag Over Helpers

        #region ListBox Event Handlers
        private void DragDropListBox_DragEnter(object sender, DragEventArgs e)
        {
            SetDragAfterLastItem(true);

            OnItemDragOver(e, null, Items.Count);

            e.Handled = true;
        }

        private void DragDropListBox_DragOver(object sender, DragEventArgs e)
        {
            OnItemDragOver(e, null, Items.Count);

            e.Handled = true;
        }

        private void DragDropListBox_DragLeave(object sender, DragEventArgs e)
        {
            SetDragAfterLastItem(false);

            e.Handled = true;
        }

        private void DragDropListBox_Drop(object sender, DragEventArgs e)
        {
            SetDragAfterLastItem(false);

            if (e.Effects != DragDropEffects.None)
            {
                OnItemDropOn(e, null, Items.Count);
            }

            e.Handled = true;
        }
        #endregion ListBox Event Handlers

        #region ListBoxItem event handlers

        #region Drag source event handlers
        Point? _mouseDownPoint = null;
        private void ItemContainer_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            if (AllowItemDrag)
            {
                _mouseDownPoint = Mouse.GetPosition((Control)sender);
            }
        }

        private void ItemContainer_PreviewMouseMove(object sender, MouseEventArgs e)
        {
            //System.Diagnostics.Trace.WriteLine($"ItemContainer_PreviewMouseMove e.LeftButton {e.LeftButton}");

            if (AllowItemDrag && e.LeftButton == MouseButtonState.Pressed && _mouseDownPoint.HasValue)
            {
                var pos = Mouse.GetPosition((Control)sender);

                //System.Diagnostics.Trace.WriteLine($"_mouseDownPoint {_mouseDownPoint}  pos {pos}");

                if (Distance(pos, _mouseDownPoint.Value) > 4)
                {
                    var lbi = (ListBoxItem)sender;

                    var dragData = new ItemDragData
                    {
                        DragItem = lbi.DataContext ?? lbi,
                        SourceItemIndex = ItemContainerGenerator.IndexFromContainer(lbi),
                        Source = this
                    };

                    var effect = DragDrop.DoDragDrop(this, dragData, DragDropEffects.Move | DragDropEffects.Copy);

                    _mouseDownPoint = null;

                    e.Handled = true;
                }
            }
            else
            {
                _mouseDownPoint = null;
            }
        }

        protected static double Distance(Point a, Point b)
        {
            var xdist = Math.Abs(a.X - b.X);
            var ydist = Math.Abs(a.Y - b.Y);

            return Math.Sqrt((xdist * xdist) + (ydist * ydist));
        }

        #endregion Drag source event handlers

        #region Drag target event handlers
        private void ItemContainer_DragEnter(object sender, DragEventArgs e)
        {
            var lbi = (ListBoxItem)sender;

            UpdateAdorner(lbi, true, true);

            int index = ItemContainerGenerator.IndexFromContainer(lbi);

            OnItemDragOver(e, lbi.DataContext ?? lbi, index);

            e.Handled = true;
        }

        private void ItemContainer_DragOver(object sender, DragEventArgs e)
        {
            var lbi = (ListBoxItem)sender;

            int index = ItemContainerGenerator.IndexFromContainer(lbi);

            OnItemDragOver(e, lbi.DataContext ?? lbi, index);

            e.Handled = true;
        }

        private void ItemContainer_DragLeave(object sender, DragEventArgs e)
        {
            UpdateAdorner((ListBoxItem)sender, false, true);

            e.Handled = true;
        }

        private void ItemContainer_Drop(object sender, DragEventArgs e)
        {
            System.Diagnostics.Trace.WriteLine($"ItemContainer_Drop e.Effects {e.Effects}");

            var lbi = (ListBoxItem)sender;

            UpdateAdorner(lbi, false, true);

            if (e.Effects != DragDropEffects.None)
            {
                int index = ItemContainerGenerator.IndexFromContainer(lbi);
                OnItemDropOn(e, lbi.DataContext ?? lbi, index);
            }

            e.Handled = true;
        }
        #endregion Drag target event handlers
        #endregion ListBoxItem event handlers

        #region Dependency Properties
        #region TargetMarkerBrush Property
        public Brush TargetMarkerBrush
        {
            get { return (Brush)GetValue(TargetMarkerBrushProperty); }
            set { SetValue(TargetMarkerBrushProperty, value); }
        }

        public static readonly DependencyProperty TargetMarkerBrushProperty =
            DependencyProperty.Register(nameof(TargetMarkerBrush), typeof(Brush), typeof(DragDropListBox),
                new PropertyMetadata(Brushes.Black));
        #endregion TargetMarkerBrush Property

        #region AllowItemDrag Property
        public bool AllowItemDrag
        {
            get { return (bool)GetValue(AllowItemDragProperty); }
            set { SetValue(AllowItemDragProperty, value); }
        }

        public static readonly DependencyProperty AllowItemDragProperty =
            DependencyProperty.Register(nameof(AllowItemDrag), typeof(bool), typeof(DragDropListBox),
                new PropertyMetadata(false));
        #endregion AllowItemDrag Property

        #region AllowDragReorderCopy Property
        /// <summary>
        /// If true, drag-reorder operation will copy when control key is pressed
        /// </summary>
        public bool AllowDragReorderCopy
        {
            get { return (bool)GetValue(AllowDragReorderCopyProperty); }
            set { SetValue(AllowDragReorderCopyProperty, value); }
        }

        public static readonly DependencyProperty AllowDragReorderCopyProperty =
            DependencyProperty.Register(nameof(AllowDragReorderCopy), typeof(bool), typeof(DragDropListBox),
                new PropertyMetadata(false));
        #endregion AllowDragReorderCopy Property


        #region AllowDragReorder Property
        public bool AllowDragReorder
        {
            get { return (bool)GetValue(AllowDragReorderProperty); }
            set { SetValue(AllowDragReorderProperty, value); }
        }

        public static readonly DependencyProperty AllowDragReorderProperty =
            DependencyProperty.Register(nameof(AllowDragReorder), typeof(bool), typeof(DragDropListBox),
                new FrameworkPropertyMetadata(false,
                                              FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
                                              AllowDragReorder_PropertyChanged)
                { DefaultUpdateSourceTrigger = System.Windows.Data.UpdateSourceTrigger.PropertyChanged });

        protected static void AllowDragReorder_PropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            (d as DragDropListBox).OnAllowDragReorderChanged(e.OldValue);
        }

        private void OnAllowDragReorderChanged(object oldValue)
        {
            if (AllowDragReorder)
            {
                AllowItemDrag = true;
            }
        }
        #endregion AllowDragReorder Property


        #endregion Dependency Properties

        #region ItemDragData class
        public class ItemDragData
        {
            public object DragItem { get; set; }
            public int SourceItemIndex { get; set; }

            public DragDropListBox Source { get; set; }
        }
        #endregion ItemDragData class
    }

    #region ItemDragDropEventArgs class
    public class ItemDragDropEventArgs : EventArgs
    {
        public ItemDragDropEventArgs(DragEventArgs e, object targetItem, int targetIndex)
        {
            _e = e;
            TargetItem = targetItem;
            TargetIndex = targetIndex;
        }

        private DragEventArgs _e;

        public int TargetIndex { get; private set; }

        public IDataObject Data => _e.Data;

        public DragDropEffects Effects
        {
            get => _e.Effects;
            set => _e.Effects = value;
        }
        public DragDropKeyStates KeyStates => _e.KeyStates;
        public DragDropEffects AllowedEffects => _e.AllowedEffects;

        /// <summary>
        /// Either ListBoxItem or DataContext of ListBoxItem
        /// If null, user is dragging over the empty space after the last item in the listbox
        /// </summary>
        public Object TargetItem { get; private set; }
    }
    #endregion ItemDragDropEventArgs class

    #region DropTargetAdorner class
    public class DropTargetAdorner : Adorner
    {
        /*
         * AdornerContentPresenter with a DataTemplate would be preferable:
         * 
         * https://stackoverflow.com/a/10034274/424129
         * 
         * */
        public DropTargetAdorner(UIElement adornedElement, bool insertingBefore, Brush targetHighlightBrush = null)
            : base(adornedElement)
        {
            IsHitTestVisible = false;
            _insertingBefore = insertingBefore;
            _targetHighlightBrush = targetHighlightBrush ?? Brushes.Black;

            Border border = new Border();
        }

        protected double _targetBarHeight = 2;
        protected bool _insertingBefore;
        protected Brush _targetHighlightBrush;

        protected override void OnRender(DrawingContext drawingContext)
        {
            var lbi = AdornedElement as ListBoxItem;

            Rect adornedElementRect = new Rect(0, 0, lbi.ActualWidth, lbi.ActualHeight);
            Size targetBarSize = new Size(adornedElementRect.Width, _targetBarHeight);
            Rect targetBarRect = new Rect(adornedElementRect.Location, targetBarSize);

            if (!_insertingBefore)
            {
                targetBarRect.Location = new Point(targetBarRect.Left, adornedElementRect.BottomLeft.Y - _targetBarHeight);
            }

            drawingContext.DrawRectangle(_targetHighlightBrush, null, targetBarRect);
        }
    }
    #endregion DropTargetAdorner class
}
Advertisements

Improved PanelBehaviors

Improved PanelBehaviors. This adds a ChildIndex property.

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

namespace HollowEarth.Behaviors
{
    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);
                SetChildIndex(child, i);
            }
        }

        #region PanelBehaviors.IsChildPositionIndicated Attached Property
        public static bool GetIsChildPositionIndicated(Panel panel)
        {
            return (bool)panel.GetValue(IsChildPositionIndicatedProperty);
        }
        public static void SetIsChildPositionIndicated(Panel panel, bool value)
        {
            panel.SetValue(IsChildPositionIndicatedProperty, value);
        }
    
        /// <summary>
        /// Behavior which causes the Panel to identify its first and last children with attached properties. 
        /// </summary>
        public static readonly DependencyProperty IsChildPositionIndicatedProperty =
            DependencyProperty.RegisterAttached("IsChildPositionIndicated", typeof(bool), typeof(PanelBehaviors),
                new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.AffectsArrange, IsChildPositionIndicated_PropertyChanged));
        private static void IsChildPositionIndicated_PropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            Panel panel = (Panel)d;
            ((Panel)d).LayoutUpdated += (s, e2) => UpdateChildFirstLastProperties(panel);
        }
        #endregion PanelBehaviors.IsChildPositionIndicated Attached Property
    
        #region PanelBehaviors.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 FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsParentArrange));
        #endregion PanelBehaviors.IsFirstChild Attached Property

        #region PanelBehaviors.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 FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsParentArrange));
        #endregion PanelBehaviors.IsLastChild Attached Property

        #region PanelBehaviors.ChildIndex Attached Property
        public static int GetChildIndex(UIElement obj)
        {
            return (int)obj.GetValue(ChildIndexProperty);
        }

        public static void SetChildIndex(UIElement obj, int value)
        {
            obj.SetValue(ChildIndexProperty, value);
        }

        public static readonly DependencyProperty ChildIndexProperty =
            DependencyProperty.RegisterAttached("ChildIndex", typeof(int), typeof(PanelBehaviors),
                new FrameworkPropertyMetadata(-1, 
                    FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsParentArrange));
        #endregion PanelBehaviors.ChildIndex Attached Property
    }
}

Dynamic Visitor

The dynamic keyword in C# has a fun property: When you pass a dynamic to a method, method overload resolution is performed at runtime using the runtime binder. I learned that here.

The runtime binder is limited: It ignores inherited methods.

But you can write a quick and dirty visitor pattern without the visited objects needing to support it with a Visit method. I’m sure it’s not the fastest thing at runtime, but one of these days I’ll find a use for it.

Full code below, and here’s a fiddle.

using System;
using System.Collections;

namespace DynamicOverloadResolution
{
    public class DynamicVisitor
    {
        public void Visit(IEnumerable value)
        {
            Console.WriteLine($"IEnumerable {value}");
            foreach (dynamic d in value)
                Visit(d);
        }

        public void Visit(IList value)
        {
            Console.WriteLine($"IList       {value.Count}");
            foreach (dynamic d in value)
                Visit(d);
        }

        public void Visit(Object value) => Console.WriteLine($"Object      {value}");
        public void Visit(Int32 value) => Console.WriteLine($"Int32       {value}");
        public void Visit(DateTime value) => Console.WriteLine($"DateTime    {value}");
        public void Visit(String value) => Console.WriteLine($"String      {value}");
        public void Visit(Double value) => Console.WriteLine($"Double      {value}");
    }

    public class DynamicVisitorBase
    {
        public void Visit(IEnumerable value)
        {
            Console.WriteLine($"IEnumerable {value}");
            foreach (dynamic d in value)
                Visit(d);
        }

        public void Visit(IList value)
        {
            Console.WriteLine($"IList       {value.Count}");
            foreach (dynamic d in value)
                Visit(d);
        }
    }

    public class DynamicVisitorSubclass : DynamicVisitorBase
    {
        public void Visit(Object value) => Console.WriteLine($"Object      {value}");
        public void Visit(Int32 value) =>  Console.WriteLine($"Int32       {value}");
        public void Visit(DateTime value) => Console.WriteLine($"DateTime    {value}");
        public void Visit(String value) => Console.WriteLine($"String      {value}");
        public void Visit(Double value) => Console.WriteLine($"Double      {value}");
    }

    class Program
    {
        static void Main()
        {
            var objects = new ArrayList()
            {
                "Foo", 1L, 1.1, 2.2M,
                DateTime.Now,
                new Nullable<DateTime>(DateTime.Now.AddDays(1)),
                new List<object>()
                {
                    new List<String> { "list1", "list2" },
                    Enumerable.Range(0,2)
                },
                new Dictionary<String, String> {
                    ["key string"] = "value string"
                }
            };

            Console.WriteLine("DynamicVisitor:");
            new DynamicVisitor().Visit(objects);

            Console.WriteLine("\n\nDynamicVisitorSubclass -- inheritance doesn't work");
            new DynamicVisitorSubclass().Visit(objects);
        }
    }
}

Program output:

DynamicVisitor:
IList       8
String      Foo
Double      1
Double      1.1
Object      2.2
DateTime    10/28/2016 12:07:01 PM
DateTime    10/29/2016 12:07:01 PM
IList       2
IList       2
String      list1
String      list2
IEnumerable System.Linq.Enumerable+<RangeIterator>d__110
Int32       0
Int32       1
IEnumerable System.Collections.Generic.Dictionary`2[System.String,System.String]
Object      [key string, value string]


DynamicVisitorSubclass -- inheritance doesn't work
Object      System.Collections.ArrayList

Aphorism

Using UI controls as a data structure is one of those square wheels that every programmer instinctively reinvents.

It’s so universal that I suspect it may be hard-wired in the human brain, like Chomsky’s universal grammar (which if I understand correctly still hasn’t been disproven, largely because the theory’s been refined to the point where it basically amounts to “most people say stuff”).

Styling first and last items of a StackPanel or Grid in WPF

Here’s a good StackOverflow question: Xamarin.Forms: Equivalent to CSS :last-of-type selector.

He wanted a Style Trigger that applies on the first or last item of a StackPanel. You can’t do that out of the box, but you can write a behavior in five or ten minutes that decorates the control’s children with their relative positions via an attached property. Panel is the base class for StackPanel, Grid, and UniformGrid, so Panel is our target for the behavior.

This is WPF, not Xamarin, but I wrote it in the hope that the guy asking the question would be able to convert it to Xamarin without any real trouble. That turned out to be true, and he found that it worked as intended. His Xamarin version is at the link.

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

And here’s a usage example in XAML:

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

Here’s what makes XAML cool: You can apply this to an ItemsControl, or any ItemsControl descendant such as a ListBox or TreeView/TreeViewItem, by applying it to the ItemsPanel:

<ListBox.ItemsPanel>
    <ItemsPanelTemplate>
        <StackPanel heap:PanelBehaviors.IdentifyFirstAndLastChild="True"
                    Orientation="Vertical" />
    </ItemsPanelTemplate>
</ListBox.ItemsPanel>