diff --git a/components/Extensions/samples/ListViewExtensionsAlternateColorSample.xaml b/components/Extensions/samples/ListViewExtensionsAlternateColorSample.xaml index 46f5fc41..d99fddc7 100644 --- a/components/Extensions/samples/ListViewExtensionsAlternateColorSample.xaml +++ b/components/Extensions/samples/ListViewExtensionsAlternateColorSample.xaml @@ -1,21 +1,33 @@ - + + + + + + + + + - 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(), + }; + } } diff --git a/components/Extensions/src/ListViewBase/ListViewExtensions.AlternateRows.cs b/components/Extensions/src/ListViewBase/ListViewExtensions.AlternateRows.cs index 1e73c325..7fe9c745 100644 --- a/components/Extensions/src/ListViewBase/ListViewExtensions.AlternateRows.cs +++ b/components/Extensions/src/ListViewBase/ListViewExtensions.AlternateRows.cs @@ -10,202 +10,193 @@ 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, 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)); + public static readonly DependencyProperty AlternateItemTemplateProperty = + DependencyProperty.RegisterAttached("AlternateItemTemplate", typeof(DataTemplate), typeof(ListViewExtensions), + 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) - { - 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 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) - { - 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) + private static void OnAlternateRowPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args) { - if (sender is ListViewBase listViewBase) - { - listViewBase.ContainerContentChanging -= ColorContainerContentChanging; - listViewBase.Items.VectorChanged -= ColorItemsVectorChanged; - listViewBase.Unloaded -= OnListViewBaseUnloaded; - - _itemsForList[listViewBase.Items] = listViewBase; - if (AlternateColorProperty != null) - { - listViewBase.ContainerContentChanging += ColorContainerContentChanging; - listViewBase.Items.VectorChanged += ColorItemsVectorChanged; - listViewBase.Unloaded += OnListViewBaseUnloaded; - } - } - } + if (sender is not ListViewBase listViewBase) + return; - private static void ColorContainerContentChanging(ListViewBase sender, ContainerContentChangingEventArgs args) - { - var itemContainer = args.ItemContainer as Control; - SetItemContainerBackground(sender, itemContainer, args.ItemIndex); - } + // Cleanup existing subscriptions + listViewBase.ContainerContentChanging -= OnContainerContentChanging; + listViewBase.Items.VectorChanged -= OnItemsVectorChanged; + listViewBase.Unloaded -= OnListViewBaseUnloaded_AltRow; - private static void OnAlternateItemTemplatePropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args) - { - if (sender is ListViewBase listViewBase) + _trackedListViews[listViewBase.Items] = listViewBase; + + // Resubscribe to events as necessary + var altColor = GetAlternateColor(listViewBase); + var altStyle = GetAlternateStyle(listViewBase); + var altTemplate = GetAlternateItemTemplate(listViewBase); + + // If any of the properties are set, subscribe to the necessary events + if ((altColor ?? altStyle ?? (object?)altTemplate) is not null) { - listViewBase.ContainerContentChanging -= ItemTemplateContainerContentChanging; - listViewBase.Unloaded -= OnListViewBaseUnloaded; - - if (AlternateItemTemplateProperty != null) - { - listViewBase.ContainerContentChanging += ItemTemplateContainerContentChanging; - listViewBase.Unloaded += OnListViewBaseUnloaded; - } + listViewBase.ContainerContentChanging += OnContainerContentChanging; + listViewBase.Items.VectorChanged += OnItemsVectorChanged; + listViewBase.Unloaded += OnListViewBaseUnloaded_AltRow; } + + // Update all items to apply the new property + UpdateItems(listViewBase); } - private static void ItemTemplateContainerContentChanging(ListViewBase sender, ContainerContentChangingEventArgs args) + private static void OnContainerContentChanging(ListViewBase sender, ContainerContentChangingEventArgs args) => UpdateItem(sender, args.ItemIndex); + + private static void OnItemsVectorChanged(IObservableVector sender, IVectorChangedEventArgs args) { - if (args.ItemIndex % 2 == 0) - { - args.ItemContainer.ContentTemplate = GetAlternateItemTemplate(sender); - } - else - { - args.ItemContainer.ContentTemplate = sender.ItemTemplate; - } + // 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; + + // 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; + + // Update all items from the affected index and below + UpdateItems(listViewBase, (int)args.Index); } - private static void OnItemContainerStretchDirectionPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args) + private static void UpdateItems(ListViewBase listViewBase, int startingIndex = 0) { - if (sender is ListViewBase listViewBase) - { - listViewBase.ContainerContentChanging -= ItemContainerStretchDirectionChanging; - listViewBase.Unloaded -= OnListViewBaseUnloaded; - - if (ItemContainerStretchDirectionProperty != null) - { - listViewBase.ContainerContentChanging += ItemContainerStretchDirectionChanging; - listViewBase.Unloaded += OnListViewBaseUnloaded; - } - } + for (int i = startingIndex; i < listViewBase.Items.Count; i++) + UpdateItem(listViewBase, i); } - private static void ItemContainerStretchDirectionChanging(ListViewBase sender, ContainerContentChangingEventArgs args) + private static void UpdateItem(ListViewBase listViewBase, int itemIndex) { - var stretchDirection = GetItemContainerStretchDirection(sender); + // Get the item as a control + var control = listViewBase.ContainerFromIndex(itemIndex) as Control; + control ??= listViewBase.Items[itemIndex] as Control; - if (stretchDirection == ItemContainerStretchDirection.Vertical || stretchDirection == ItemContainerStretchDirection.Both) - { - args.ItemContainer.VerticalContentAlignment = VerticalAlignment.Stretch; - } + // If the item is not a control, there's nothing to be done + if (control is null) + return; - if (stretchDirection == ItemContainerStretchDirection.Horizontal || stretchDirection == ItemContainerStretchDirection.Both) + // Get the item as a container. This may be null if the item is not in a container. + 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); + + // 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) { - args.ItemContainer.HorizontalContentAlignment = HorizontalAlignment.Stretch; + container.ContentTemplate = realizedTemplate; } } - private static void OnListViewBaseUnloaded(object sender, RoutedEventArgs e) + private static void SetRowBackground(ListViewBase sender, Control itemContainer, Brush? brush) { - if (sender is ListViewBase listViewBase) - { - _itemsForList.Remove(listViewBase.Items); + var rootBorder = itemContainer.FindDescendant(); - listViewBase.ContainerContentChanging -= ItemContainerStretchDirectionChanging; - listViewBase.ContainerContentChanging -= ItemTemplateContainerContentChanging; - listViewBase.ContainerContentChanging -= ColorContainerContentChanging; - listViewBase.Items.VectorChanged -= ColorItemsVectorChanged; - listViewBase.Unloaded -= OnListViewBaseUnloaded; + itemContainer.Background = brush; + if (rootBorder is not null) + { + rootBorder.Background = brush; } } - private static void ColorItemsVectorChanged(IObservableVector sender, IVectorChangedEventArgs args) + private static void OnListViewBaseUnloaded_AltRow(object sender, RoutedEventArgs e) { - // If the index is at the end we can ignore - if (args.Index == (sender.Count - 1)) - { + if (sender is not ListViewBase listViewBase) 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)) - { - _itemsForList.TryGetValue(sender, out ListViewBase? listViewBase); - if (listViewBase == null) - { - return; - } - - int index = (int)args.Index; - for (int i = index; i < sender.Count; i++) - { - var itemContainer = listViewBase.ContainerFromIndex(i) as Control; - if (itemContainer != null) - { - SetItemContainerBackground(listViewBase, itemContainer, i); - } - } - } - } + // Untrack the list view + _trackedListViews.Remove(listViewBase.Items); - 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; - } - } + // Unsubscribe from events + listViewBase.ContainerContentChanging -= OnContainerContentChanging; + listViewBase.Items.VectorChanged -= OnItemsVectorChanged; + 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 9258cfc9..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); - } + public static ItemContainerStretchDirection? GetItemContainerStretchDirection(ListViewBase obj) + => (ItemContainerStretchDirection)obj.GetValue(ItemContainerStretchDirectionProperty); /// /// Sets the stretch associated with the specified @@ -30,7 +28,45 @@ public static ItemContainerStretchDirection GetItemContainerStretchDirection(Lis /// The to associate the with /// The for binding to the public static void SetItemContainerStretchDirection(ListViewBase obj, ItemContainerStretchDirection value) + => obj.SetValue(ItemContainerStretchDirectionProperty, value); + + private static void OnItemContainerStretchDirectionPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args) { - obj.SetValue(ItemContainerStretchDirectionProperty, value); + if (sender is not ListViewBase listViewBase) + return; + + // Cleanup existing subscriptions + listViewBase.ContainerContentChanging -= ItemContainerStretchDirectionChanging; + listViewBase.Unloaded -= OnListViewBaseUnloaded_StretchDirection; + + // Resubscribe to events as necessary + if (GetItemContainerStretchDirection(listViewBase) is not null) + { + listViewBase.ContainerContentChanging += ItemContainerStretchDirectionChanging; + listViewBase.Unloaded += OnListViewBaseUnloaded_StretchDirection; + } + } + + private static void ItemContainerStretchDirectionChanging(ListViewBase sender, ContainerContentChangingEventArgs args) + { + var stretchDirection = GetItemContainerStretchDirection(sender); + + // 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; } }