0

First of all I am working with MVVM / WPF / .Net Framework 4.6.1

I have a ListView configured with ItemsPanelTemplate in horizontal orientation that displays items from a DataTemplate. This setup allows me to fit as many items inside the Width of the ListView (the witdth size is the same from the Window), and behaves responsively when I resize the window.

So far everything is fine, now I just want to Identify what items are positioned on the first row, including when the window get resized and items inside the first row increase or decrease.

I merely want to accomplish this behavior because I would like to apply a different template style for those items (let's say a I bigger image or different text color).

enter image description here

Here below the XAML definition for the ListView:

<ListView x:Name="lv"  
          ItemsSource="{Binding Path = ItemsSource}"
          SelectedItem="{Binding Path = SelectedItem}">
    <ListView.ItemsPanel>
        <ItemsPanelTemplate>
            <WrapPanel Orientation="Horizontal"></WrapPanel>
        </ItemsPanelTemplate>
    </ListView.ItemsPanel>
    <ListView.ItemTemplate>
        <DataTemplate>
            <Grid Width="180" Height="35">
                <Grid.RowDefinitions>
                    <RowDefinition />
                </Grid.RowDefinitions>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="auto"/>
                    <ColumnDefinition Width="*"/>
                </Grid.ColumnDefinitions>
                <Ellipse Grid.Column="0" Grid.Row="0" Height="32" Width="32" 
                         VerticalAlignment="Top" HorizontalAlignment="Left">
                    <Ellipse.Fill>
                        <ImageBrush ImageSource="{Binding IconPathName}" />
                    </Ellipse.Fill>
                </Ellipse>
                <TextBlock Grid.Column="1" Grid.Row="0" TextWrapping="WrapWithOverflow"
                           HorizontalAlignment="Left" VerticalAlignment="Top"
                           Text="{Binding Name}" />

            </Grid> 
        </DataTemplate>
    </ListView.ItemTemplate>
</ListView>

BTW: I already did a work around where I am getting the Index from each ListViewItem and calculating against the Width of the Grid inside the DataTemplate that is a fixed value of 180, but unfortunately it did not work as I expected since I had to use a DependencyProperty to bind the ActualWidth of the of the ListView to my ViewModel and did not responded very well when I resized the window.

I know I am looking for a very particular behavior, but if anyone has any suggestions about how to deal with this I would really appreciate. Any thoughts are welcome even if you think I should be using a different control, please detail.

Thanks in advance!

luis_laurent
  • 784
  • 1
  • 12
  • 32
  • You can use ItemTemplateSelector to assign a different template to a specific item: https://learn.microsoft.com/en-us/dotnet/api/system.windows.controls.itemscontrol.itemtemplateselector?view=netframework-4.8 – zaphod-ii Dec 18 '19 at 22:27
  • Thanks but that would only work for one item at the time, if you see the screenshot attached I want to apply a different template for those items highlighted – luis_laurent Dec 18 '19 at 22:32
  • I see, this logic should be based on the current layout of the panel. You mentioned that you are using a binding to Width, but it does not get updated. Have you tried ActualWidth instead? It should respond to UI changes. – zaphod-ii Dec 18 '19 at 22:38
  • Yes I did, actually I am implementing this SizeObserver class for it https://stackoverflow.com/a/1083733/1754814 – luis_laurent Dec 19 '19 at 00:14

1 Answers1

1

You shouldn't handle the layout in any view model. If you didn't extend ListView consider to use an attached behavior (raw example):

ListBox.cs

public class ListBox : DependencyObject
{
  #region IsAlternateFirstRowTemplateEnabled attached property

  public static readonly DependencyProperty IsAlternateFirstRowTemplateEnabledProperty = DependencyProperty.RegisterAttached(
    "IsAlternateFirstRowTemplateEnabled", 
    typeof(bool), typeof(ListView), 
    new PropertyMetadata(default(bool), ListBox.OnIsEnabledChanged));

  public static void SetIsAlternateFirstRowTemplateEnabled(DependencyObject attachingElement, bool value) => attachingElement.SetValue(ListBox.IsAlternateFirstRowTemplateEnabledProperty, value);

  public static bool GetIsAlternateFirstRowTemplateEnabled(DependencyObject attachingElement) => (bool)attachingElement.GetValue(ListBox.IsAlternateFirstRowTemplateEnabledProperty);

  #endregion

  private static void OnIsEnabledChanged(DependencyObject attachingElement, DependencyPropertyChangedEventArgs e)
  {
    if (!(attachingElement is System.Windows.Controls.ListBox listBox))
    {
      return;
    }

    if ((bool)e.NewValue)
    {
      listBox.Loaded += ListBox.Initialize;
    }
    else
    {
      listBox.SizeChanged -= ListBox.OnListBoxSizeChanged;
    }
  }

  private static void Initialize(object sender, RoutedEventArgs e)
  {
    var listBox = sender as System.Windows.Controls.ListBox;
    listBox.Loaded -= ListBox.Initialize;

    // Check if items panel is WrapPanel
    if (!listBox.TryFindVisualChildElement(out WrapPanel panel))
    {
      return;
    }

    listBox.SizeChanged += ListBox.OnListBoxSizeChanged;
    ListBox.ApplyFirstRowDataTemplate(listBox);
  }

  private static void OnListBoxSizeChanged(object sender, SizeChangedEventArgs e)
  {
    if (!e.WidthChanged)
    {
      return;
    }
    var listBox = sender as System.Windows.Controls.ListBox;
    ListBox.ApplyFirstRowDataTemplate(listBox);
  }

  private static void ApplyFirstRowDataTemplate(System.Windows.Controls.ListBox listBox)
  {
    double calculatedFirstRowWidth = 0;
    var firstRowDataTemplate = listBox.Resources["FirstRowDataTemplate"] as DataTemplate;
    foreach (FrameworkElement itemContainer in listBox.ItemContainerGenerator.Items
      .Select(listBox.ItemContainerGenerator.ContainerFromItem).Cast<FrameworkElement>())
    {
      calculatedFirstRowWidth += itemContainer.ActualWidth;
      if (itemContainer.TryFindVisualChildElement(out ContentPresenter contentPresenter))
      {
        if (calculatedFirstRowWidth > listBox.ActualWidth - listBox.Padding.Right - listBox.Padding.Left)
        {
          if (contentPresenter.ContentTemplate == firstRowDataTemplate)
          {
            // Restore the default template of previous first row items
            contentPresenter.ContentTemplate = listBox.ItemTemplate;
            continue;
          }

          break;
        }

        contentPresenter.ContentTemplate = firstRowDataTemplate;
      }
    }
  }
}

Helper Extension Method

/// <summary>
/// Traverses the visual tree towards the leafs until an element with a matching element type is found.
/// </summary>
/// <typeparam name="TChild">The type the visual child must match.</typeparam>
/// <param name="parent"></param>
/// <param name="resultElement"></param>
/// <returns></returns>
public static bool TryFindVisualChildElement<TChild>(this DependencyObject parent, out TChild resultElement)
  where TChild : DependencyObject
{
  resultElement = null;

  if (parent is Popup popup)
  {
    parent = popup.Child;
    if (parent == null)
    {
      return false;
    }
  }

  for (var childIndex = 0; childIndex < VisualTreeHelper.GetChildrenCount(parent); childIndex++)
  {
    DependencyObject childElement = VisualTreeHelper.GetChild(parent, childIndex);

    if (childElement is TChild child)
    {
      resultElement = child;
      return true;
    }

    if (childElement.TryFindVisualChildElement(out resultElement))
    {
      return true;
    }
  }

  return false;
}

Usage

<ListView x:Name="lv"  
          ListBox.IsAlternateFirstRowTemplateEnabled="True"
          ItemsSource="{Binding Path = ItemsSource}"
          SelectedItem="{Binding Path = SelectedItem}">
    <ListView.ItemsPanel>
        <ItemsPanelTemplate>
            <WrapPanel Orientation="Horizontal" />
        </ItemsPanelTemplate>
    </ListView.ItemsPanel>
    <ListView.Resources>
        <DataTemplate x:Key="FirstRowDataTemplate">

            <!-- Draw a red border around first row items -->
            <Border BorderThickness="2" BorderBrush="Red">
                <Grid Width="180" Height="35">
                    <Grid.RowDefinitions>
                        <RowDefinition />
                    </Grid.RowDefinitions>
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="auto"/>
                        <ColumnDefinition Width="*"/>
                    </Grid.ColumnDefinitions>
                    <Ellipse Grid.Column="0" Grid.Row="0" Height="32" Width="32" 
                         VerticalAlignment="Top" HorizontalAlignment="Left">
                        <Ellipse.Fill>
                            <ImageBrush ImageSource="{Binding IconPathName}" />
                        </Ellipse.Fill>
                    </Ellipse>
                    <TextBlock Grid.Column="1" Grid.Row="0" TextWrapping="WrapWithOverflow"
                           HorizontalAlignment="Left" VerticalAlignment="Top"
                           Text="{Binding Name}" />

                </Grid> 
            </Border>
        </DataTemplate>
    </ListView.Resources>
    <ListView.ItemTemplate>
        <DataTemplate>
            <Grid Width="180" Height="35">
                <Grid.RowDefinitions>
                    <RowDefinition />
                </Grid.RowDefinitions>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="auto"/>
                    <ColumnDefinition Width="*"/>
                </Grid.ColumnDefinitions>
                <Ellipse Grid.Column="0" Grid.Row="0" Height="32" Width="32" 
                         VerticalAlignment="Top" HorizontalAlignment="Left">
                    <Ellipse.Fill>
                        <ImageBrush ImageSource="{Binding IconPathName}" />
                    </Ellipse.Fill>
                </Ellipse>
                <TextBlock Grid.Column="1" Grid.Row="0" TextWrapping="WrapWithOverflow"
                           HorizontalAlignment="Left" VerticalAlignment="Top"
                           Text="{Binding Name}" />

            </Grid> 
        </DataTemplate>
    </ListView.ItemTemplate>
</ListView>

Remarks
If the visual tree itself will not change for the first row, consider to add a second attached property to the ListBox class (e.g., IsFirstRowItem) which you would set on the ListBoxItems. You can then use a DataTrigger to modify the control properties to change the appearance. This will very likely increase the performance too.

luis_laurent
  • 784
  • 1
  • 12
  • 32
BionicCode
  • 1
  • 4
  • 28
  • 44