From 295450935ec2d312a3b8acec4a61c1c0496b66af Mon Sep 17 00:00:00 2001 From: Avishai Dernis Date: Fri, 19 Dec 2025 16:41:34 +0200 Subject: [PATCH 1/7] Added item fallback if container is null in ListViewExtensions alternate rows --- .../ListViewExtensions.AlternateRows.cs | 30 ++++++------------- 1 file changed, 9 insertions(+), 21 deletions(-) diff --git a/components/Extensions/src/ListViewBase/ListViewExtensions.AlternateRows.cs b/components/Extensions/src/ListViewBase/ListViewExtensions.AlternateRows.cs index 1e73c325..4eca4ec6 100644 --- a/components/Extensions/src/ListViewBase/ListViewExtensions.AlternateRows.cs +++ b/components/Extensions/src/ListViewBase/ListViewExtensions.AlternateRows.cs @@ -171,15 +171,16 @@ private static void ColorItemsVectorChanged(IObservableVector sender, IV { _itemsForList.TryGetValue(sender, out ListViewBase? listViewBase); if (listViewBase == null) - { return; - } int index = (int)args.Index; for (int i = index; i < sender.Count; i++) { + // Get item container or element at index var itemContainer = listViewBase.ContainerFromIndex(i) as Control; - if (itemContainer != null) + itemContainer ??= listViewBase.Items[i] as Control; + + if (itemContainer is not null) { SetItemContainerBackground(listViewBase, itemContainer, i); } @@ -189,23 +190,10 @@ private static void ColorItemsVectorChanged(IObservableVector sender, IV private static void SetItemContainerBackground(ListViewBase sender, Control itemContainer, int itemIndex) { - if (itemIndex % 2 == 0) - { - itemContainer.Background = GetAlternateColor(sender); - var rootBorder = itemContainer.FindDescendant(); - if (rootBorder != null) - { - rootBorder.Background = GetAlternateColor(sender); - } - } - else - { - itemContainer.Background = null; - var rootBorder = itemContainer.FindDescendant(); - if (rootBorder != null) - { - rootBorder.Background = null; - } - } + var brush = itemIndex % 2 == 0 ? GetAlternateColor(sender) : null; + var rootBorder = itemContainer.FindDescendant(); + + itemContainer.Background = brush; + rootBorder?.Background = brush; } } From da3ce4f017ebefa5d3e04adb081baed0f86646b8 Mon Sep 17 00:00:00 2001 From: Avishai Dernis Date: Fri, 19 Dec 2025 16:56:21 +0200 Subject: [PATCH 2/7] Rewrote without null conditional assignment --- .../src/ListViewBase/ListViewExtensions.AlternateRows.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/components/Extensions/src/ListViewBase/ListViewExtensions.AlternateRows.cs b/components/Extensions/src/ListViewBase/ListViewExtensions.AlternateRows.cs index 4eca4ec6..2e5ba59b 100644 --- a/components/Extensions/src/ListViewBase/ListViewExtensions.AlternateRows.cs +++ b/components/Extensions/src/ListViewBase/ListViewExtensions.AlternateRows.cs @@ -194,6 +194,9 @@ private static void SetItemContainerBackground(ListViewBase sender, Control item var rootBorder = itemContainer.FindDescendant(); itemContainer.Background = brush; - rootBorder?.Background = brush; + if (rootBorder is not null) + { + rootBorder.Background = brush; + } } } From 5d5070801b7d790decff6ccc3667d424850f79b4 Mon Sep 17 00:00:00 2001 From: Avishai Dernis Date: Sat, 20 Dec 2025 17:37:28 +0200 Subject: [PATCH 3/7] Fixed unneccesary event subscriptions in ListViewExtensions --- .../ListViewExtensions.AlternateRows.cs | 77 +++++++------------ ...ListViewExtensions.StretchItemContainer.cs | 35 ++++++++- 2 files changed, 60 insertions(+), 52 deletions(-) diff --git a/components/Extensions/src/ListViewBase/ListViewExtensions.AlternateRows.cs b/components/Extensions/src/ListViewBase/ListViewExtensions.AlternateRows.cs index 2e5ba59b..e488225d 100644 --- a/components/Extensions/src/ListViewBase/ListViewExtensions.AlternateRows.cs +++ b/components/Extensions/src/ListViewBase/ListViewExtensions.AlternateRows.cs @@ -64,19 +64,22 @@ public static void SetAlternateItemTemplate(ListViewBase obj, DataTemplate value private static void OnAlternateColorPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args) { - if (sender is ListViewBase listViewBase) - { - listViewBase.ContainerContentChanging -= ColorContainerContentChanging; - listViewBase.Items.VectorChanged -= ColorItemsVectorChanged; - listViewBase.Unloaded -= OnListViewBaseUnloaded; + if (sender is not ListViewBase listViewBase) + return; - _itemsForList[listViewBase.Items] = listViewBase; - if (AlternateColorProperty != null) - { - listViewBase.ContainerContentChanging += ColorContainerContentChanging; - listViewBase.Items.VectorChanged += ColorItemsVectorChanged; - listViewBase.Unloaded += OnListViewBaseUnloaded; - } + // Cleanup existing subscriptions + listViewBase.ContainerContentChanging -= ColorContainerContentChanging; + listViewBase.Items.VectorChanged -= ColorItemsVectorChanged; + listViewBase.Unloaded -= OnListViewBaseUnloaded; + + _itemsForList[listViewBase.Items] = listViewBase; + + // Resubscribe to events as necessary + if (GetAlternateColor(listViewBase) is not null) + { + listViewBase.ContainerContentChanging += ColorContainerContentChanging; + listViewBase.Items.VectorChanged += ColorItemsVectorChanged; + listViewBase.Unloaded += OnListViewBaseUnloaded; } } @@ -88,16 +91,18 @@ private static void ColorContainerContentChanging(ListViewBase sender, Container private static void OnAlternateItemTemplatePropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args) { - if (sender is ListViewBase listViewBase) - { - listViewBase.ContainerContentChanging -= ItemTemplateContainerContentChanging; - listViewBase.Unloaded -= OnListViewBaseUnloaded; + if (sender is not ListViewBase listViewBase) + return; - if (AlternateItemTemplateProperty != null) - { - listViewBase.ContainerContentChanging += ItemTemplateContainerContentChanging; - listViewBase.Unloaded += OnListViewBaseUnloaded; - } + // Cleanup existing subscriptions + listViewBase.ContainerContentChanging -= ItemTemplateContainerContentChanging; + listViewBase.Unloaded -= OnListViewBaseUnloaded; + + // Resubscribe to events as necessary + if (GetAlternateItemTemplate(listViewBase) != null) + { + listViewBase.ContainerContentChanging += ItemTemplateContainerContentChanging; + listViewBase.Unloaded += OnListViewBaseUnloaded; } } @@ -113,36 +118,6 @@ private static void ItemTemplateContainerContentChanging(ListViewBase sender, Co } } - private static void OnItemContainerStretchDirectionPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args) - { - if (sender is ListViewBase listViewBase) - { - listViewBase.ContainerContentChanging -= ItemContainerStretchDirectionChanging; - listViewBase.Unloaded -= OnListViewBaseUnloaded; - - if (ItemContainerStretchDirectionProperty != null) - { - listViewBase.ContainerContentChanging += ItemContainerStretchDirectionChanging; - listViewBase.Unloaded += OnListViewBaseUnloaded; - } - } - } - - private static void ItemContainerStretchDirectionChanging(ListViewBase sender, ContainerContentChangingEventArgs args) - { - var stretchDirection = GetItemContainerStretchDirection(sender); - - if (stretchDirection == ItemContainerStretchDirection.Vertical || stretchDirection == ItemContainerStretchDirection.Both) - { - args.ItemContainer.VerticalContentAlignment = VerticalAlignment.Stretch; - } - - if (stretchDirection == ItemContainerStretchDirection.Horizontal || stretchDirection == ItemContainerStretchDirection.Both) - { - args.ItemContainer.HorizontalContentAlignment = HorizontalAlignment.Stretch; - } - } - private static void OnListViewBaseUnloaded(object sender, RoutedEventArgs e) { if (sender is ListViewBase listViewBase) diff --git a/components/Extensions/src/ListViewBase/ListViewExtensions.StretchItemContainer.cs b/components/Extensions/src/ListViewBase/ListViewExtensions.StretchItemContainer.cs index 9258cfc9..e0fe06c6 100644 --- a/components/Extensions/src/ListViewBase/ListViewExtensions.StretchItemContainer.cs +++ b/components/Extensions/src/ListViewBase/ListViewExtensions.StretchItemContainer.cs @@ -19,7 +19,7 @@ public static partial class ListViewExtensions /// /// The to get the associated from /// The associated with the - public static ItemContainerStretchDirection GetItemContainerStretchDirection(ListViewBase obj) + public static ItemContainerStretchDirection? GetItemContainerStretchDirection(ListViewBase obj) { return (ItemContainerStretchDirection)obj.GetValue(ItemContainerStretchDirectionProperty); } @@ -33,4 +33,37 @@ public static void SetItemContainerStretchDirection(ListViewBase obj, ItemContai { obj.SetValue(ItemContainerStretchDirectionProperty, value); } + + private static void OnItemContainerStretchDirectionPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args) + { + if (sender is not ListViewBase listViewBase) + return; + + // Cleanup existing subscriptions + listViewBase.ContainerContentChanging -= ItemContainerStretchDirectionChanging; + listViewBase.Unloaded -= OnListViewBaseUnloaded; + + // Resubscribe to events as necessary + if (GetItemContainerStretchDirection(listViewBase) is not null) + { + listViewBase.ContainerContentChanging += ItemContainerStretchDirectionChanging; + listViewBase.Unloaded += OnListViewBaseUnloaded; + } + } + + private static void ItemContainerStretchDirectionChanging(ListViewBase sender, ContainerContentChangingEventArgs args) + { + var stretchDirection = GetItemContainerStretchDirection(sender); + + if (stretchDirection == ItemContainerStretchDirection.Vertical || stretchDirection == ItemContainerStretchDirection.Both) + { + args.ItemContainer.VerticalContentAlignment = VerticalAlignment.Stretch; + } + + if (stretchDirection == ItemContainerStretchDirection.Horizontal || stretchDirection == ItemContainerStretchDirection.Both) + { + args.ItemContainer.HorizontalContentAlignment = HorizontalAlignment.Stretch; + } + } + } From f2f45b5391139733631e14339bc945cf9f78c0fc Mon Sep 17 00:00:00 2001 From: Avishai Dernis Date: Mon, 22 Dec 2025 06:52:21 +0200 Subject: [PATCH 4/7] More ListViewExtensions cleanup --- .../ListViewExtensions.AlternateRows.cs | 130 +++++++++--------- .../ListViewExtensions.Command.cs | 46 +++---- ...ListViewExtensions.StretchItemContainer.cs | 37 ++--- 3 files changed, 99 insertions(+), 114 deletions(-) diff --git a/components/Extensions/src/ListViewBase/ListViewExtensions.AlternateRows.cs b/components/Extensions/src/ListViewBase/ListViewExtensions.AlternateRows.cs index e488225d..26853790 100644 --- a/components/Extensions/src/ListViewBase/ListViewExtensions.AlternateRows.cs +++ b/components/Extensions/src/ListViewBase/ListViewExtensions.AlternateRows.cs @@ -10,57 +10,49 @@ namespace CommunityToolkit.WinUI; /// public static partial class ListViewExtensions { - private static Dictionary, ListViewBase> _itemsForList = new Dictionary, ListViewBase>(); + private static readonly Dictionary, ListViewBase> _trackedListViews = []; /// /// Attached for binding a as an alternate background color to a /// - public static readonly DependencyProperty AlternateColorProperty = DependencyProperty.RegisterAttached("AlternateColor", typeof(Brush), typeof(ListViewExtensions), new PropertyMetadata(null, OnAlternateColorPropertyChanged)); + public static readonly DependencyProperty AlternateColorProperty = + DependencyProperty.RegisterAttached("AlternateColor", typeof(Brush), typeof(ListViewExtensions), + new PropertyMetadata(null, OnAlternateColorPropertyChanged)); /// /// Attached for binding a as an alternate template to a /// - public static readonly DependencyProperty AlternateItemTemplateProperty = DependencyProperty.RegisterAttached("AlternateItemTemplate", typeof(DataTemplate), typeof(ListViewExtensions), new PropertyMetadata(null, OnAlternateItemTemplatePropertyChanged)); + public static readonly DependencyProperty AlternateItemTemplateProperty = + DependencyProperty.RegisterAttached("AlternateItemTemplate", typeof(DataTemplate), typeof(ListViewExtensions), + new PropertyMetadata(null, OnAlternateItemTemplatePropertyChanged)); /// /// Gets the alternate associated with the specified /// /// The to get the associated from /// The associated with the - public static Brush GetAlternateColor(ListViewBase obj) - { - return (Brush)obj.GetValue(AlternateColorProperty); - } + public static Brush GetAlternateColor(ListViewBase obj) => (Brush)obj.GetValue(AlternateColorProperty); /// /// Sets the alternate associated with the specified /// /// The to associate the with /// The for binding to the - public static void SetAlternateColor(ListViewBase obj, Brush value) - { - obj.SetValue(AlternateColorProperty, value); - } + public static void SetAlternateColor(ListViewBase obj, Brush value) => obj.SetValue(AlternateColorProperty, value); /// /// Gets the associated with the specified /// /// The to get the associated from /// The associated with the - public static DataTemplate GetAlternateItemTemplate(ListViewBase obj) - { - return (DataTemplate)obj.GetValue(AlternateItemTemplateProperty); - } + public static DataTemplate GetAlternateItemTemplate(ListViewBase obj) => (DataTemplate)obj.GetValue(AlternateItemTemplateProperty); /// /// Sets the associated with the specified /// /// The to associate the with /// The for binding to the - public static void SetAlternateItemTemplate(ListViewBase obj, DataTemplate value) - { - obj.SetValue(AlternateItemTemplateProperty, value); - } + public static void SetAlternateItemTemplate(ListViewBase obj, DataTemplate value) => obj.SetValue(AlternateItemTemplateProperty, value); private static void OnAlternateColorPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args) { @@ -70,23 +62,29 @@ private static void OnAlternateColorPropertyChanged(DependencyObject sender, Dep // Cleanup existing subscriptions listViewBase.ContainerContentChanging -= ColorContainerContentChanging; listViewBase.Items.VectorChanged -= ColorItemsVectorChanged; - listViewBase.Unloaded -= OnListViewBaseUnloaded; + listViewBase.Unloaded -= OnListViewBaseUnloaded_AltRow; - _itemsForList[listViewBase.Items] = listViewBase; + _trackedListViews[listViewBase.Items] = listViewBase; // Resubscribe to events as necessary if (GetAlternateColor(listViewBase) is not null) { listViewBase.ContainerContentChanging += ColorContainerContentChanging; listViewBase.Items.VectorChanged += ColorItemsVectorChanged; - listViewBase.Unloaded += OnListViewBaseUnloaded; + listViewBase.Unloaded += OnListViewBaseUnloaded_AltRow; } } private static void ColorContainerContentChanging(ListViewBase sender, ContainerContentChangingEventArgs args) { - var itemContainer = args.ItemContainer as Control; - SetItemContainerBackground(sender, itemContainer, args.ItemIndex); + // Get the row's item container, or contents as a fallback + Control? control = args.ItemContainer ?? args.Item as Control; + + // Update the row background if the item was found + if (control is not null) + { + SetRowBackground(sender, control, args.ItemIndex); + } } private static void OnAlternateItemTemplatePropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args) @@ -96,74 +94,55 @@ private static void OnAlternateItemTemplatePropertyChanged(DependencyObject send // Cleanup existing subscriptions listViewBase.ContainerContentChanging -= ItemTemplateContainerContentChanging; - listViewBase.Unloaded -= OnListViewBaseUnloaded; + listViewBase.Unloaded -= OnListViewBaseUnloaded_AltRow; // Resubscribe to events as necessary if (GetAlternateItemTemplate(listViewBase) != null) { listViewBase.ContainerContentChanging += ItemTemplateContainerContentChanging; - listViewBase.Unloaded += OnListViewBaseUnloaded; + listViewBase.Unloaded += OnListViewBaseUnloaded_AltRow; } } private static void ItemTemplateContainerContentChanging(ListViewBase sender, ContainerContentChangingEventArgs args) { - if (args.ItemIndex % 2 == 0) - { - args.ItemContainer.ContentTemplate = GetAlternateItemTemplate(sender); - } - else - { - args.ItemContainer.ContentTemplate = sender.ItemTemplate; - } - } - - private static void OnListViewBaseUnloaded(object sender, RoutedEventArgs e) - { - if (sender is ListViewBase listViewBase) - { - _itemsForList.Remove(listViewBase.Items); - - listViewBase.ContainerContentChanging -= ItemContainerStretchDirectionChanging; - listViewBase.ContainerContentChanging -= ItemTemplateContainerContentChanging; - listViewBase.ContainerContentChanging -= ColorContainerContentChanging; - listViewBase.Items.VectorChanged -= ColorItemsVectorChanged; - listViewBase.Unloaded -= OnListViewBaseUnloaded; - } + var template = args.ItemIndex % 2 == 0 ? GetAlternateItemTemplate(sender) : sender.ItemTemplate; + args.ItemContainer.ContentTemplate = template; } private static void ColorItemsVectorChanged(IObservableVector sender, IVectorChangedEventArgs args) { - // If the index is at the end we can ignore + // If the index is at the end, no other items were affected + // and there's no action to take if (args.Index == (sender.Count - 1)) - { return; - } - // Only need to handle Inserted and Removed because we'll handle everything else in the - // ColorContainerContentChanging method - if ((args.CollectionChange == CollectionChange.ItemInserted) || (args.CollectionChange == CollectionChange.ItemRemoved)) + // This function is for updating indirectly affected items + // Therefore we only need to handle items inserted and removed where every + // item beneath would potentially change if they are even or odd. + if (args.CollectionChange is not (CollectionChange.ItemInserted or CollectionChange.ItemRemoved)) + return; + + // Attempt to get the list view for the affected items + _trackedListViews.TryGetValue(sender, out ListViewBase? listViewBase); + if (listViewBase is null) + return; + + int index = (int)args.Index; + for (int i = index; i < sender.Count; i++) { - _itemsForList.TryGetValue(sender, out ListViewBase? listViewBase); - if (listViewBase == null) - return; + // Get item container or element at index + var itemContainer = listViewBase.ContainerFromIndex(i) as Control; + itemContainer ??= listViewBase.Items[i] as Control; - int index = (int)args.Index; - for (int i = index; i < sender.Count; i++) + if (itemContainer is not null) { - // Get item container or element at index - var itemContainer = listViewBase.ContainerFromIndex(i) as Control; - itemContainer ??= listViewBase.Items[i] as Control; - - if (itemContainer is not null) - { - SetItemContainerBackground(listViewBase, itemContainer, i); - } + SetRowBackground(listViewBase, itemContainer, i); } } } - private static void SetItemContainerBackground(ListViewBase sender, Control itemContainer, int itemIndex) + private static void SetRowBackground(ListViewBase sender, Control itemContainer, int itemIndex) { var brush = itemIndex % 2 == 0 ? GetAlternateColor(sender) : null; var rootBorder = itemContainer.FindDescendant(); @@ -174,4 +153,19 @@ private static void SetItemContainerBackground(ListViewBase sender, Control item rootBorder.Background = brush; } } + + private static void OnListViewBaseUnloaded_AltRow(object sender, RoutedEventArgs e) + { + if (sender is not ListViewBase listViewBase) + return; + + // Untrack the list view + _trackedListViews.Remove(listViewBase.Items); + + // Unsubscribe from events + listViewBase.ContainerContentChanging -= ItemTemplateContainerContentChanging; + listViewBase.ContainerContentChanging -= ColorContainerContentChanging; + listViewBase.Items.VectorChanged -= ColorItemsVectorChanged; + listViewBase.Unloaded -= OnListViewBaseUnloaded_AltRow; + } } diff --git a/components/Extensions/src/ListViewBase/ListViewExtensions.Command.cs b/components/Extensions/src/ListViewBase/ListViewExtensions.Command.cs index 7932543d..eda987d1 100644 --- a/components/Extensions/src/ListViewBase/ListViewExtensions.Command.cs +++ b/components/Extensions/src/ListViewBase/ListViewExtensions.Command.cs @@ -12,47 +12,39 @@ namespace CommunityToolkit.WinUI; public static partial class ListViewExtensions { /// - /// Attached for binding an to handle ListViewBase Item interaction by means of ItemClick event. ListViewBase IsItemClickEnabled must be set to true. + /// Attached for binding an to handle ListViewBase Item interaction by means of ItemClick event. ListViewBase IsItemClickEnabled must be set to true. /// - public static readonly DependencyProperty CommandProperty = DependencyProperty.RegisterAttached("Command", typeof(ICommand), typeof(ListViewExtensions), new PropertyMetadata(null, OnCommandPropertyChanged)); + public static readonly DependencyProperty CommandProperty = + DependencyProperty.RegisterAttached("Command", typeof(ICommand), typeof(ListViewExtensions), + new PropertyMetadata(null, OnCommandPropertyChanged)); /// /// Gets the associated with the specified /// /// The to get the associated from /// The associated with the - public static ICommand GetCommand(ListViewBase obj) - { - return (ICommand)obj.GetValue(CommandProperty); - } + public static ICommand GetCommand(ListViewBase obj) => (ICommand)obj.GetValue(CommandProperty); /// /// Sets the associated with the specified /// /// The to associate the with /// The for binding to the - public static void SetCommand(ListViewBase obj, ICommand value) - { - obj.SetValue(CommandProperty, value); - } + public static void SetCommand(ListViewBase obj, ICommand value) => obj.SetValue(CommandProperty, value); private static void OnCommandPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args) { - var listViewBase = sender as ListViewBase; - - if (listViewBase == null) - { + if (sender is not ListViewBase listViewBase) return; - } var oldCommand = args.OldValue as ICommand; - if (oldCommand != null) + if (oldCommand is not null) { listViewBase.ItemClick -= OnListViewBaseItemClick; } var newCommand = args.NewValue as ICommand; - if (newCommand != null) + if (newCommand is not null) { listViewBase.ItemClick += OnListViewBaseItemClick; } @@ -60,18 +52,14 @@ private static void OnCommandPropertyChanged(DependencyObject sender, Dependency private static void OnListViewBaseItemClick(object sender, ItemClickEventArgs e) { - if (sender is ListViewBase listViewBase) - { - var command = GetCommand(listViewBase); - if (listViewBase == null || command == null) - { - return; - } + if (sender is not ListViewBase listViewBase) + return; - if (command.CanExecute(e.ClickedItem)) - { - command.Execute(e.ClickedItem); - } - } + var command = GetCommand(listViewBase); + if (command is null) + return; + + if (command.CanExecute(e.ClickedItem)) + command.Execute(e.ClickedItem); } } diff --git a/components/Extensions/src/ListViewBase/ListViewExtensions.StretchItemContainer.cs b/components/Extensions/src/ListViewBase/ListViewExtensions.StretchItemContainer.cs index e0fe06c6..0ade1097 100644 --- a/components/Extensions/src/ListViewBase/ListViewExtensions.StretchItemContainer.cs +++ b/components/Extensions/src/ListViewBase/ListViewExtensions.StretchItemContainer.cs @@ -14,15 +14,13 @@ public static partial class ListViewExtensions /// public static readonly DependencyProperty ItemContainerStretchDirectionProperty = DependencyProperty.RegisterAttached("ItemContainerStretchDirection", typeof(ItemContainerStretchDirection), typeof(ListViewExtensions), new PropertyMetadata(null, OnItemContainerStretchDirectionPropertyChanged)); - /// + /// /// Gets the stretch associated with the specified /// /// The to get the associated from /// The associated with the public static ItemContainerStretchDirection? GetItemContainerStretchDirection(ListViewBase obj) - { - return (ItemContainerStretchDirection)obj.GetValue(ItemContainerStretchDirectionProperty); - } + => (ItemContainerStretchDirection)obj.GetValue(ItemContainerStretchDirectionProperty); /// /// Sets the stretch associated with the specified @@ -30,9 +28,7 @@ public static partial class ListViewExtensions /// The to associate the with /// The for binding to the public static void SetItemContainerStretchDirection(ListViewBase obj, ItemContainerStretchDirection value) - { - obj.SetValue(ItemContainerStretchDirectionProperty, value); - } + => obj.SetValue(ItemContainerStretchDirectionProperty, value); private static void OnItemContainerStretchDirectionPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args) { @@ -41,13 +37,13 @@ private static void OnItemContainerStretchDirectionPropertyChanged(DependencyObj // Cleanup existing subscriptions listViewBase.ContainerContentChanging -= ItemContainerStretchDirectionChanging; - listViewBase.Unloaded -= OnListViewBaseUnloaded; + listViewBase.Unloaded -= OnListViewBaseUnloaded_StretchDirection; // Resubscribe to events as necessary if (GetItemContainerStretchDirection(listViewBase) is not null) { listViewBase.ContainerContentChanging += ItemContainerStretchDirectionChanging; - listViewBase.Unloaded += OnListViewBaseUnloaded; + listViewBase.Unloaded += OnListViewBaseUnloaded_StretchDirection; } } @@ -55,15 +51,22 @@ private static void ItemContainerStretchDirectionChanging(ListViewBase sender, C { var stretchDirection = GetItemContainerStretchDirection(sender); - if (stretchDirection == ItemContainerStretchDirection.Vertical || stretchDirection == ItemContainerStretchDirection.Both) - { - args.ItemContainer.VerticalContentAlignment = VerticalAlignment.Stretch; - } - - if (stretchDirection == ItemContainerStretchDirection.Horizontal || stretchDirection == ItemContainerStretchDirection.Both) - { + // Set vertical content stretching + if (stretchDirection is ItemContainerStretchDirection.Horizontal or ItemContainerStretchDirection.Both) args.ItemContainer.HorizontalContentAlignment = HorizontalAlignment.Stretch; - } + + // Set horizontal content stretching + if (stretchDirection is ItemContainerStretchDirection.Vertical or ItemContainerStretchDirection.Both) + args.ItemContainer.VerticalContentAlignment = VerticalAlignment.Stretch; } + private static void OnListViewBaseUnloaded_StretchDirection(object sender, RoutedEventArgs e) + { + if (sender is not ListViewBase listViewBase) + return; + + // Unsubscribe from events + listViewBase.ContainerContentChanging -= ItemContainerStretchDirectionChanging; + listViewBase.Unloaded -= OnListViewBaseUnloaded_StretchDirection; + } } From 38bed5eafd3890a12f375e3e11e102c172926c29 Mon Sep 17 00:00:00 2001 From: Avishai Dernis Date: Mon, 29 Dec 2025 05:18:31 +0200 Subject: [PATCH 5/7] Added AlternateStyle to ListViewExtensions --- .../ListViewExtensions.AlternateRows.cs | 131 ++++++++++-------- 1 file changed, 74 insertions(+), 57 deletions(-) diff --git a/components/Extensions/src/ListViewBase/ListViewExtensions.AlternateRows.cs b/components/Extensions/src/ListViewBase/ListViewExtensions.AlternateRows.cs index 26853790..de7cce93 100644 --- a/components/Extensions/src/ListViewBase/ListViewExtensions.AlternateRows.cs +++ b/components/Extensions/src/ListViewBase/ListViewExtensions.AlternateRows.cs @@ -17,100 +17,93 @@ public static partial class ListViewExtensions /// public static readonly DependencyProperty AlternateColorProperty = DependencyProperty.RegisterAttached("AlternateColor", typeof(Brush), typeof(ListViewExtensions), - new PropertyMetadata(null, OnAlternateColorPropertyChanged)); + new PropertyMetadata(null, OnAlternateRowPropertyChanged)); + + /// + /// Attached for binding a as an alternate style to a + /// + public static readonly DependencyProperty AlternateStyleProperty = + DependencyProperty.RegisterAttached("AlternateStyle", typeof(Style), typeof(ListViewExtensions), + new PropertyMetadata(null, OnAlternateRowPropertyChanged)); /// /// Attached for binding a as an alternate template to a /// public static readonly DependencyProperty AlternateItemTemplateProperty = DependencyProperty.RegisterAttached("AlternateItemTemplate", typeof(DataTemplate), typeof(ListViewExtensions), - new PropertyMetadata(null, OnAlternateItemTemplatePropertyChanged)); + new PropertyMetadata(null, OnAlternateRowPropertyChanged)); /// /// Gets the alternate associated with the specified /// /// The to get the associated from /// The associated with the - public static Brush GetAlternateColor(ListViewBase obj) => (Brush)obj.GetValue(AlternateColorProperty); + public static Brush? GetAlternateColor(ListViewBase obj) => (Brush?)obj.GetValue(AlternateColorProperty); /// /// Sets the alternate associated with the specified /// /// The to associate the with /// The for binding to the - public static void SetAlternateColor(ListViewBase obj, Brush value) => obj.SetValue(AlternateColorProperty, value); + public static void SetAlternateColor(ListViewBase obj, Brush? value) => obj.SetValue(AlternateColorProperty, value); + + /// + /// Gets the alternate associated with the specified + /// + /// The to get the associated from + /// The associated with the + public static Style? GetAlternateStyle(ListViewBase obj) => (Style?)obj.GetValue(AlternateStyleProperty); + + /// + /// Sets the alternate associated with the specified + /// + /// The to associate the with + /// The for binding to the + public static void SetAlternateStyle(ListViewBase obj, Style? value) => obj.SetValue(AlternateStyleProperty, value); /// /// Gets the associated with the specified /// /// The to get the associated from /// The associated with the - public static DataTemplate GetAlternateItemTemplate(ListViewBase obj) => (DataTemplate)obj.GetValue(AlternateItemTemplateProperty); + public static DataTemplate? GetAlternateItemTemplate(ListViewBase obj) => (DataTemplate?)obj.GetValue(AlternateItemTemplateProperty); /// /// Sets the associated with the specified /// /// The to associate the with /// The for binding to the - public static void SetAlternateItemTemplate(ListViewBase obj, DataTemplate value) => obj.SetValue(AlternateItemTemplateProperty, value); + public static void SetAlternateItemTemplate(ListViewBase obj, DataTemplate? value) => obj.SetValue(AlternateItemTemplateProperty, value); - private static void OnAlternateColorPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args) + private static void OnAlternateRowPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args) { if (sender is not ListViewBase listViewBase) return; // Cleanup existing subscriptions - listViewBase.ContainerContentChanging -= ColorContainerContentChanging; - listViewBase.Items.VectorChanged -= ColorItemsVectorChanged; + listViewBase.ContainerContentChanging -= OnContainerContentChanging; + listViewBase.Items.VectorChanged -= OnItemsVectorChanged; listViewBase.Unloaded -= OnListViewBaseUnloaded_AltRow; _trackedListViews[listViewBase.Items] = listViewBase; // Resubscribe to events as necessary - if (GetAlternateColor(listViewBase) is not null) - { - listViewBase.ContainerContentChanging += ColorContainerContentChanging; - listViewBase.Items.VectorChanged += ColorItemsVectorChanged; - listViewBase.Unloaded += OnListViewBaseUnloaded_AltRow; - } - } - - private static void ColorContainerContentChanging(ListViewBase sender, ContainerContentChangingEventArgs args) - { - // Get the row's item container, or contents as a fallback - Control? control = args.ItemContainer ?? args.Item as Control; - - // Update the row background if the item was found - if (control is not null) - { - SetRowBackground(sender, control, args.ItemIndex); - } - } - - private static void OnAlternateItemTemplatePropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args) - { - if (sender is not ListViewBase listViewBase) - return; - - // Cleanup existing subscriptions - listViewBase.ContainerContentChanging -= ItemTemplateContainerContentChanging; - listViewBase.Unloaded -= OnListViewBaseUnloaded_AltRow; + var altColor = GetAlternateColor(listViewBase); + var altStyle = GetAlternateStyle(listViewBase); + var altTemplate = GetAlternateItemTemplate(listViewBase); - // Resubscribe to events as necessary - if (GetAlternateItemTemplate(listViewBase) != null) + // If any of the properties are set, subscribe to the necessary events + if ((altColor ?? altStyle ?? (object?)altTemplate) is not null) { - listViewBase.ContainerContentChanging += ItemTemplateContainerContentChanging; + listViewBase.ContainerContentChanging += OnContainerContentChanging; + listViewBase.Items.VectorChanged += OnItemsVectorChanged; listViewBase.Unloaded += OnListViewBaseUnloaded_AltRow; } } - private static void ItemTemplateContainerContentChanging(ListViewBase sender, ContainerContentChangingEventArgs args) - { - var template = args.ItemIndex % 2 == 0 ? GetAlternateItemTemplate(sender) : sender.ItemTemplate; - args.ItemContainer.ContentTemplate = template; - } + private static void OnContainerContentChanging(ListViewBase sender, ContainerContentChangingEventArgs args) => UpdateItem(sender, args.ItemIndex); - private static void ColorItemsVectorChanged(IObservableVector sender, IVectorChangedEventArgs args) + private static void OnItemsVectorChanged(IObservableVector sender, IVectorChangedEventArgs args) { // If the index is at the end, no other items were affected // and there's no action to take @@ -130,15 +123,40 @@ private static void ColorItemsVectorChanged(IObservableVector sender, IV int index = (int)args.Index; for (int i = index; i < sender.Count; i++) + UpdateItem(listViewBase, i); + } + + private static void UpdateItem(ListViewBase listViewBase, int itemIndex) + { + // Get the item as a control + var control = listViewBase.ContainerFromIndex(itemIndex) as Control; + control ??= listViewBase.Items[itemIndex] as Control; + + // If the item is not a control, there's nothing to be done + if (control is null) + return; + + // Get the item as a container. This may be null if the item is not in a container. + // Also get all the alternate properties + var container = control as SelectorItem; + var altColor = GetAlternateColor(listViewBase); + var altStyle = GetAlternateStyle(listViewBase); + var altTemplate = GetAlternateItemTemplate(listViewBase); + + // Apply the alternate properties as necessary + if (altStyle is not null) + { + control.Style = itemIndex % 2 == 0 ? altStyle : listViewBase.ItemContainerStyle; + } + + if (altColor is not null) + { + SetRowBackground(listViewBase, control, itemIndex); + } + + if (altTemplate is not null && container is not null) { - // Get item container or element at index - var itemContainer = listViewBase.ContainerFromIndex(i) as Control; - itemContainer ??= listViewBase.Items[i] as Control; - - if (itemContainer is not null) - { - SetRowBackground(listViewBase, itemContainer, i); - } + container.ContentTemplate = itemIndex % 2 == 0 ? altTemplate : listViewBase.ItemTemplate; } } @@ -163,9 +181,8 @@ private static void OnListViewBaseUnloaded_AltRow(object sender, RoutedEventArgs _trackedListViews.Remove(listViewBase.Items); // Unsubscribe from events - listViewBase.ContainerContentChanging -= ItemTemplateContainerContentChanging; - listViewBase.ContainerContentChanging -= ColorContainerContentChanging; - listViewBase.Items.VectorChanged -= ColorItemsVectorChanged; + listViewBase.ContainerContentChanging -= OnContainerContentChanging; + listViewBase.Items.VectorChanged -= OnItemsVectorChanged; listViewBase.Unloaded -= OnListViewBaseUnloaded_AltRow; } } From fe7c8ce5cd74fc7e8276ebda32ac068f7294c509 Mon Sep 17 00:00:00 2001 From: Avishai Dernis Date: Mon, 29 Dec 2025 05:18:54 +0200 Subject: [PATCH 6/7] Improved ListViewExtensions alternate color sample --- ...istViewExtensionsAlternateColorSample.xaml | 45 ++++++++++++------- ...ViewExtensionsAlternateColorSample.xaml.cs | 19 ++++++++ 2 files changed, 48 insertions(+), 16 deletions(-) diff --git a/components/Extensions/samples/ListViewExtensionsAlternateColorSample.xaml b/components/Extensions/samples/ListViewExtensionsAlternateColorSample.xaml index 46f5fc41..c64d34f1 100644 --- a/components/Extensions/samples/ListViewExtensionsAlternateColorSample.xaml +++ b/components/Extensions/samples/ListViewExtensionsAlternateColorSample.xaml @@ -1,21 +1,34 @@ - + - + + + + + + + + + - One - Two - Three - Four - Five - Six - Seven - Eight - Nine - Ten + 0 + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 diff --git a/components/Extensions/samples/ListViewExtensionsAlternateColorSample.xaml.cs b/components/Extensions/samples/ListViewExtensionsAlternateColorSample.xaml.cs index 20318e11..6c6f2825 100644 --- a/components/Extensions/samples/ListViewExtensionsAlternateColorSample.xaml.cs +++ b/components/Extensions/samples/ListViewExtensionsAlternateColorSample.xaml.cs @@ -14,4 +14,23 @@ public ListViewExtensionsAlternateColorSample() { this.InitializeComponent(); } + + public static string NaiveHumanize(int num) + { + return num switch + { + 0 => "zero", + 1 => "one", + 2 => "two", + 3 => "three", + 4 => "four", + 5 => "five", + 6 => "six", + 7 => "seven", + 8 => "eight", + 9 => "nine", + 10 => "ten", + _ => num.ToString(), + }; + } } From 0d23e1fdaef953fbeb175084944bd14f5dcd679d Mon Sep 17 00:00:00 2001 From: Avishai Dernis Date: Mon, 29 Dec 2025 08:15:56 +0200 Subject: [PATCH 7/7] Fixed property updates for alternate row properties --- ...istViewExtensionsAlternateColorSample.xaml | 17 +++---- .../ListViewExtensions.AlternateRows.cs | 50 ++++++++++++------- 2 files changed, 40 insertions(+), 27 deletions(-) diff --git a/components/Extensions/samples/ListViewExtensionsAlternateColorSample.xaml b/components/Extensions/samples/ListViewExtensionsAlternateColorSample.xaml index c64d34f1..d99fddc7 100644 --- a/components/Extensions/samples/ListViewExtensionsAlternateColorSample.xaml +++ b/components/Extensions/samples/ListViewExtensionsAlternateColorSample.xaml @@ -1,20 +1,19 @@ - + - + diff --git a/components/Extensions/src/ListViewBase/ListViewExtensions.AlternateRows.cs b/components/Extensions/src/ListViewBase/ListViewExtensions.AlternateRows.cs index de7cce93..7fe9c745 100644 --- a/components/Extensions/src/ListViewBase/ListViewExtensions.AlternateRows.cs +++ b/components/Extensions/src/ListViewBase/ListViewExtensions.AlternateRows.cs @@ -99,6 +99,9 @@ private static void OnAlternateRowPropertyChanged(DependencyObject sender, Depen listViewBase.Items.VectorChanged += OnItemsVectorChanged; listViewBase.Unloaded += OnListViewBaseUnloaded_AltRow; } + + // Update all items to apply the new property + UpdateItems(listViewBase); } private static void OnContainerContentChanging(ListViewBase sender, ContainerContentChangingEventArgs args) => UpdateItem(sender, args.ItemIndex); @@ -121,8 +124,13 @@ private static void OnItemsVectorChanged(IObservableVector sender, IVect if (listViewBase is null) return; - int index = (int)args.Index; - for (int i = index; i < sender.Count; i++) + // Update all items from the affected index and below + UpdateItems(listViewBase, (int)args.Index); + } + + private static void UpdateItems(ListViewBase listViewBase, int startingIndex = 0) + { + for (int i = startingIndex; i < listViewBase.Items.Count; i++) UpdateItem(listViewBase, i); } @@ -137,32 +145,38 @@ private static void UpdateItem(ListViewBase listViewBase, int itemIndex) return; // Get the item as a container. This may be null if the item is not in a container. - // Also get all the alternate properties var container = control as SelectorItem; + + // Get the base properties + // The base color cannot be retrieved, and therefore cannot be unapplied. + // NOTE: This is a huge design limitation, and one reason I believe + // AlternateColor should be replaced entirely with AlternateStyle. + var baseStyle = listViewBase.ItemContainerStyle; + var baseTemplate = listViewBase.ItemTemplate; + + // Get all the alternate properties. var altColor = GetAlternateColor(listViewBase); var altStyle = GetAlternateStyle(listViewBase); var altTemplate = GetAlternateItemTemplate(listViewBase); - // Apply the alternate properties as necessary - if (altStyle is not null) - { - control.Style = itemIndex % 2 == 0 ? altStyle : listViewBase.ItemContainerStyle; - } - - if (altColor is not null) - { - SetRowBackground(listViewBase, control, itemIndex); - } - - if (altTemplate is not null && container is not null) + // Determine the realized properties based on the item index and + // whether or not alternate properties are set. + bool altRow = itemIndex % 2 == 0; + var realizedColor = (altRow ? altColor : null) ?? null; + var realizedStyle = (altRow ? altStyle : baseStyle) ?? baseStyle; + var realizedTemplate = (altRow ? altTemplate : baseTemplate) ?? baseTemplate; + + // Apply the realized properties + SetRowBackground(listViewBase, control, realizedColor); + control.Style = realizedStyle; + if (container is not null) { - container.ContentTemplate = itemIndex % 2 == 0 ? altTemplate : listViewBase.ItemTemplate; + container.ContentTemplate = realizedTemplate; } } - private static void SetRowBackground(ListViewBase sender, Control itemContainer, int itemIndex) + private static void SetRowBackground(ListViewBase sender, Control itemContainer, Brush? brush) { - var brush = itemIndex % 2 == 0 ? GetAlternateColor(sender) : null; var rootBorder = itemContainer.FindDescendant(); itemContainer.Background = brush;