Simon
Simon

Reputation: 135

Dynamically adjust ItemWidth in WrapPanel

I am using WPF MVVM. I have the following code:

<ScrollViewer VerticalScrollBarVisibility="Auto">
    <ListView ItemsSource="{Binding ItemCollection}" Height="160" Width="810">
        <ListView.ItemsPanel>
            <ItemsPanelTemplate>
                <WrapPanel Width="500" Height ="150" ItemWidth="100" ItemHeight="30"/>
            </ItemsPanelTemplate>
        </ListView.ItemsPanel>
        <ListView.ItemTemplate>
            <DataTemplate>
                <CheckBox IsChecked="{Binding Checked}">
                    <TextBlock Text="{Binding Label}"/>
                </CheckBox>
            </DataTemplate>
        </ListView.ItemTemplate>
    </ListView>
</ScrollViewer>

The code shows that each CheckBox has a width of 100. Each line in the WrapPanel can contain at most 5 CheckBoxes due to its size and the ItemWidth (500 / 100).

I have multiple CheckBox with different widths.

I do not want to set the ItemWidth explicitly to 280, because most of the items are smaller. Instead, I want each CheckBox to take up the space of a multiple of 100 that can display all of its content. If the current line in the WrapPanel does not have enough space, then move it to next line.

For the sample widths above I expect this.

How can I achieve that?

Upvotes: 0

Views: 804

Answers (1)

thatguy
thatguy

Reputation: 22089

The ItemWidth explicitly defines the width for all items.

A Double that represents the uniform width of all items that are contained within the WrapPanel. The default value is NaN.

Do not set an ItemWidth. Then each item occupies its individual size.

A child element of a WrapPanel may have its width property set explicitly. ItemWidth specifies the size of the layout partition that is reserved by the WrapPanel for the child element. As a result, ItemWidth takes precedence over an element's own width.

Now, if you do not explicitly define the widths of your items, they are sized to fit their content, but do not align with a multiple of 100. Scaling items up to a multiple of a defined size automatically is not supported in WrapPanel.

If you want to enable this kind of dynamic sizing, you will have to create a custom wrap panel or you can write a custom behavior. I show you an example of the latter, as it is reusable and more flexible. I use the Microsoft.Xaml.Behaviors.Wpf NuGet package that contains base classes for that.

public class AlignWidthBehavior : Behavior<FrameworkElement>
{
   public static readonly DependencyProperty AlignmentProperty = DependencyProperty.Register(
      nameof(Alignment), typeof(double), typeof(AlignWidthBehavior), new PropertyMetadata(double.NaN));

   public double Alignment
   {
      get => (double)GetValue(AlignmentProperty);
      set => SetValue(AlignmentProperty, value);
   }

   protected override void OnAttached()
   {
      base.OnAttached();
      AssociatedObject.LayoutUpdated += OnLayoutUpdated;
   }

   protected override void OnDetaching()
   {
      base.OnDetaching();
      AssociatedObject.LayoutUpdated -= OnLayoutUpdated;
   }

   private void OnLayoutUpdated(object sender, EventArgs e)
   {
      var size = AssociatedObject.ActualWidth;
      var alignment = Alignment;
      var isAligned = size % alignment < 10E-12;

      if (!double.IsNaN(alignment) && !isAligned)
         AssociatedObject.Width = Math.Ceiling(size / alignment) * alignment;
   }
}

This behavior is triggered, when the layout of an item changes. It then checks, if the width of the associated item is aligned with the value given by the Alignment property and adapts it if not.

<DataTemplate>
   <CheckBox IsChecked="{Binding Checked}">
      <b:Interaction.Behaviors>
         <local:AlignWidthBehavior Alignment="100"/>
      </b:Interaction.Behaviors>
      <TextBlock Text="{Binding Label}"/>
   </CheckBox>
</DataTemplate>

You have to attach the behavior in XAML as shown above and set the Alignment to your desired value and do not forget to remove the ItemWidth property from WrapPanel.

Upvotes: 2

Related Questions