WPF ListBox WrapPanel clips long groups - wpf

I've created a ListBox to display items in groups, where the groups are wrapped right to left when they can no longer fit within the height of the ListBox's panel. So, the groups would appear similar to this in the listbox, where each group's height is arbitrary (group 1, for instance, is twice as tall as group 2):
[ 1 ][ 3 ][ 5 ]
[ ][ 4 ][ 6 ]
[ 2 ][ ]
The following XAML works correctly in that it performs the wrapping, and allows the horizontal scroll bar to appear when the items run off the right side of the ListBox.
<ListBox>
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Vertical"/>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.GroupStyle>
<ItemsPanelTemplate>
<WrapPanel Orientation="Vertical"
Height="{Binding Path=ActualHeight,
RelativeSource={RelativeSource
FindAncestor,
AncestorLevel=1,
AncestorType={x:Type ScrollContentPresenter}}}"/>
</ItemsPanelTemplate>
</ListBox.GroupStyle>
</ListBox>
The problem occurs when a group of items is longer than the height of the WrapPanel. Instead of allowing the vertical scroll bar to appear to view the cutoff item group, the items in that group are simply clipped. I'm assuming that this is a side effect of the Height binding in the WrapPanel - the scrollbar thinks it does not have to enabled.
Is there any way to enable the scrollbar, or another way around this issue that I'm not seeing?

By setting the Height property on the WrapPanel to the height of the ScrollContentPresenter, it will never scroll vertically. However, if you remove that Binding, it will never wrap, since in the layout pass, it has infinite height to layout in.
I would suggest creating your own panel class to get the behavior you want. Have a separate dependency property that you can bind the desired height to, so you can use that to calculate the target height in the measure and arrange steps. If any one child is taller than the desired height, use that child's height as the target height to calculate the wrapping.
Here is an example panel to do this:
public class SmartWrapPanel : WrapPanel
{
/// <summary>
/// Identifies the DesiredHeight dependency property
/// </summary>
public static readonly DependencyProperty DesiredHeightProperty = DependencyProperty.Register(
"DesiredHeight",
typeof(double),
typeof(SmartWrapPanel),
new FrameworkPropertyMetadata(Double.NaN,
FrameworkPropertyMetadataOptions.AffectsArrange |
FrameworkPropertyMetadataOptions.AffectsMeasure));
/// <summary>
/// Gets or sets the height to attempt to be. If any child is taller than this, will use the child's height.
/// </summary>
public double DesiredHeight
{
get { return (double)GetValue(DesiredHeightProperty); }
set { SetValue(DesiredHeightProperty, value); }
}
protected override Size MeasureOverride(Size constraint)
{
Size ret = base.MeasureOverride(constraint);
double h = ret.Height;
if (!Double.IsNaN(DesiredHeight))
{
h = DesiredHeight;
foreach (UIElement child in Children)
{
if (child.DesiredSize.Height > h)
h = child.DesiredSize.Height;
}
}
return new Size(ret.Width, h);
}
protected override System.Windows.Size ArrangeOverride(Size finalSize)
{
double h = finalSize.Height;
if (!Double.IsNaN(DesiredHeight))
{
h = DesiredHeight;
foreach (UIElement child in Children)
{
if (child.DesiredSize.Height > h)
h = child.DesiredSize.Height;
}
}
return base.ArrangeOverride(new Size(finalSize.Width, h));
}
}

Here is the slightly modified code - all credit given to Abe Heidebrecht, who previously posted it - that allows both horizontal and vertical scrolling. The only change is that the return value of MeasureOverride needs to be base.MeasureOverride(new Size(ret.width, h)).
// Original code : Abe Heidebrecht
public class SmartWrapPanel : WrapPanel
{
/// <summary>
/// Identifies the DesiredHeight dependency property
/// </summary>
public static readonly DependencyProperty DesiredHeightProperty = DependencyProperty.Register(
"DesiredHeight",
typeof(double),
typeof(SmartWrapPanel),
new FrameworkPropertyMetadata(Double.NaN,
FrameworkPropertyMetadataOptions.AffectsArrange |
FrameworkPropertyMetadataOptions.AffectsMeasure));
/// <summary>
/// Gets or sets the height to attempt to be. If any child is taller than this, will use the child's height.
/// </summary>
public double DesiredHeight
{
get { return (double)GetValue(DesiredHeightProperty); }
set { SetValue(DesiredHeightProperty, value); }
}
protected override Size MeasureOverride(Size constraint)
{
Size ret = base.MeasureOverride(constraint);
double h = ret.Height;
if (!Double.IsNaN(DesiredHeight))
{
h = DesiredHeight;
foreach (UIElement child in Children)
{
if (child.DesiredSize.Height > h)
h = child.DesiredSize.Height;
}
}
return base.MeasureOverride(new Size(ret.Width, h));
}
protected override System.Windows.Size ArrangeOverride(Size finalSize)
{
double h = finalSize.Height;
if (!Double.IsNaN(DesiredHeight))
{
h = DesiredHeight;
foreach (UIElement child in Children)
{
if (child.DesiredSize.Height > h)
h = child.DesiredSize.Height;
}
}
return base.ArrangeOverride(new Size(finalSize.Width, h));
}
}

Thanks for answering, David.
When the binding is removed, no wrapping occurs. The WrapPanel puts every group into a single vertical column.
The binding is meant to force the WrapPanel to actually wrap. If no binding is set, the WrapPanel assumes the height is infinite and never wraps.
Binding to MinHeight results in an empty listbox. I can see how the VerticalAlignment property could seem to be a solution, but alignment itself prevents any wrapping from occurring. When binding and alignment are used together, the alignment has no effect on the problem.

I would think that you are correct that it has to do with the binding. What happens when you remove the binding? With the binding are you trying to fill up at least the entire height of the list box? If so, consider binding to MinHeight instead, or try using the VerticalAlignment property.

Related

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.
Most of the check boxes have a width <=100
One check box has a width equal to 280
In the future I might have a check box with a width >300 and <=400
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.
Width <= 100: Occupy 1 item space (Width = 100)
Width <= 280: Occupy 3 item spaces (Width = 300)
Width > 300 and <= 400: Occupy 4 item spaces (Width = 400)
How can I achieve that?
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.

Fluent Ribbon: Specify position of contextual tab groups?

In the Fluent Ribbon Control Suite, is there a way to make the contextual tab groups show up first instead of last? I was using an older build and had a contextual tab group for the first three tab items, and two more with no group. Downloaded and built the newest source; the tabs in the group are now on the right end. I want them to show in the same order as I have them specified in the xaml. I don't see any obvious properties that would allow me to specify the order.
Since no one had an answer to this, and I just got a Tumbleweed badge for it :), I decided to post my solution, which was to modify the Fluent Ribbon Control Suite code. I modified ArrangeOverride in the RibbonTabsContainer class. This caused the Grouped tabs to be rendered before the ones not in a group:
/// <summary>
/// Positions child elements and determines
/// a size for the control
/// </summary>
/// <param name="finalSize">The final area within the parent
/// that this element should use to arrange
/// itself and its children</param>
/// <returns>The actual size used</returns>
protected override Size ArrangeOverride(Size finalSize)
{
var finalRect = new Rect(finalSize)
{
X = -this.HorizontalOffset
};
var orderedChildren = this.InternalChildren.OfType<RibbonTabItem>()
.OrderByDescending(x => x.Group != null); // <==== originally .OrderBy
foreach (var item in orderedChildren)
{
finalRect.Width = item.DesiredSize.Width;
finalRect.Height = Math.Max(finalSize.Height, item.DesiredSize.Height);
item.Arrange(finalRect);
finalRect.X += item.DesiredSize.Width;
}
var ribbonTabItemsWithGroups = this.InternalChildren.OfType<RibbonTabItem>()
.Where(item => item.Group != null);
var ribbonTitleBar = ribbonTabItemsWithGroups.Select(ribbonTabItemsWithGroup => ribbonTabItemsWithGroup.Group.Parent)
.OfType<RibbonTitleBar>()
.FirstOrDefault();
if (ribbonTitleBar != null)
{
ribbonTitleBar.InvalidateMeasure();
}
return finalSize;
}

Resizing WPF listview columns with width proportional to text width

I have a WPF ListView table that on default sizes the column headers to the width of each header's text. This does not fill the entire table evenly; I have a mass of squeezed columns on the left and a vacant space on the right.
I would like to be able to resize the columns to a width proportional to the longest item in the column's text, of the header or the rows. How would I do this?
What I am currently doing is initially resizing the headers to widths proportional to the headers' text, using the following code (where SearchResultsTable is the ListView):
private void Resize()
{
var width = SearchResultsTable.ActualWidth;
var gridView = SearchResultsTable.View as GridView;
var columns = new GridViewColumnCollection();
if (gridView != null)
{
columns = gridView.Columns;
}
var initialColumnWidths = columns.Sum(column => column.ActualWidth);
var scale = width / initialColumnWidths;
foreach (var column in columns)
{
column.Width = column.ActualWidth * scale;
}
}
However, this only resizes based on the header text width; I don't know how to get the maximum width of the header and rows. Any suggestions?
Here is what I am doing. Hope this helps. You can get or set the width of the GridView columns in the ListView, and I am using the width of the ListView control itself to increase/decrease the relative width of each ListView column to proportionally fill the space - and then extending the last column.
What you could do is modify my code to distribute the difference between the total widths of the columns and the width of the ListView proportionally across the set of columns in the conditional "if (total_width < e.NewSize.Width" where I am just adding that difference to the last column width.
LV_FileList.SizeChanged += this.onLV_FileList_SizeChanged;
...
/// <summary>
/// Proportionally resize listview columns when listview size changes
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void onLV_FileList_SizeChanged(object sender, SizeChangedEventArgs e)
{
if ((sender is ListView) &&
(e.PreviousSize.Width > 0))
{
double total_width = 0;
GridViewColumnCollection gvcc = ((GridView)(sender as ListView).View).Columns;
foreach (GridViewColumn gvc in gvcc)
{
gvc.Width = (gvc.Width / e.PreviousSize.Width) * e.NewSize.Width;
total_width += gvc.Width;
}
//Increase width of last column to fit width of listview if integer division made the total width to small
if (total_width < e.NewSize.Width)
{
gvcc[gvcc.Count - 1].Width += (e.NewSize.Width - total_width);
}
//Render changes to ListView before checking for horizontal scrollbar
this.AllowUIToUpdate();
//Decrease width of last column to eliminate scrollbar if it is displayed now
ScrollViewer svFileList = this.FindVisualChild<ScrollViewer>(LV_FileList);
while ((svFileList.ComputedHorizontalScrollBarVisibility != Visibility.Collapsed) && (gvcc[gvcc.Count - 1].Width > 1))
{
gvcc[gvcc.Count - 1].Width--;
this.AllowUIToUpdate();
}
}
}
/// <summary>
/// Threaded invocation to handle updating UI in resize loop
/// </summary>
private void AllowUIToUpdate()
{
DispatcherFrame dFrame = new DispatcherFrame();
Dispatcher.CurrentDispatcher.BeginInvoke(DispatcherPriority.Render, new DispatcherOperationCallback(delegate(object parameter)
{
dFrame.Continue = false;
return null;
}), null);
Dispatcher.PushFrame(dFrame);
}

Can a WPF ListBox's height be set to a multiple of its item height?

Is there some way to set the Height attribute of a WPF multi-select ListBox to be a multiple of the item height, similar to setting the size attribute of an html select element?
I have a business requirement to not have half an item showing at the bottom of the list (if it's a long list with a scrollbar), and not have extra white space at the bottom (if it's a short list with all items showing), but the only method I can find to do this is to just keep tweaking the Height until it looks about right.
(What else have I tried? I've asked colleagues, searched MSDN and StackOverflow, done some general Googling, and looked at what VS Intellisense offered as I edited the code. There's plenty of advice out there about how to set the height to fit the ListBox's container, but that's the opposite of what I'm trying to do.)
Yeah, one could imagine there would be an easier way to do it (a single snapToWholeElement property). I couldn't find this property as well.
To achieve your requirement, I've wrote a little logic. Basically, In my Windows object I've a public property lbHeight which is calculate the listbox height by calculating the height of each individual item.
First, let's take a look at the XAML:
<Window
x:Class="SO.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Width="120" SizeToContent="Height"
Title="SO Sample"
>
<StackPanel>
<ListBox x:Name="x_list" Height="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=Window}, Path=lbHeight}" >
<ListBox.ItemTemplate>
<DataTemplate>
<Border x:Name="x" Background="Gray" Margin="4" Padding="3">
<TextBlock Text="{Binding}" />
</Border>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</StackPanel>
</Window>
Note that the ItemTemplate is somewhat non trivial. One important thing to notice is that I gave this item a Name - so I can find it later.
In the code-behind constructor I put some data in the list box:
public MainWindow( )
{
InitializeComponent( );
this.x_list.ItemsSource = Enumerable.Range( 0, 100 );
}
next, I'm implementing a findVisualItem - to find the root element of the data template. I've made this function a little generic, so it get a predicate p which identify whether this is the element I want to find:
private DependencyObject findVisualItem( DependencyObject el, Predicate<DependencyObject> p )
{
DependencyObject found = null;
if( p(el) ) {
found = el;
}
else {
int count = VisualTreeHelper.GetChildrenCount( el );
for( int i=0; i<count; ++i ) {
DependencyObject c = VisualTreeHelper.GetChild( el, i );
found = findVisualItem( c, p );
if( found != null )
break;
}
}
return found;
}
I'll use the following predicate, which returns true if the element I'm looking for is a border, and its name is "x". You should modify this predicate to match your root element of your ItemTemplate.
findVisualItem(
x_list,
el => { return ( el is Border ) ? ( (FrameworkElement)el ).Name == "x" : false; }
);
Finally, the lbHeight property:
public double lbHeight
{
get {
FrameworkElement item = findVisualItem(
x_list,
el => { return ( el is Border ) ? ( (FrameworkElement)el ).Name == "x" : false; }
) as FrameworkElement;
if( item != null ) {
double h = item.ActualHeight + item.Margin.Top + item.Margin.Bottom;
return h * 12;
}
else {
return 120;
}
}
}
I've also made the Window implementing INotifyPropertyChanged, and when the items of the list box were loaded (Loaded event of ListBox) I fired a PropertyChanged event for the 'lbHeight' property. At some point it was necessary, but at the end WPF fetched the lbHeight property when I already have a rendered Item.
It is possible your Items aren't identical in Height, in which case you'll have to sum all the Items in the VirtualizedStackPanel. If you have a Horizontal scroll bar, you'll have to consider it for the total height of course. But this is the overall idea. It is only 3 hours since you published your question - I hope someone will come with a simpler answer.
This is done by setting parent control Height property to Auto, without setting any size to the Listbox itself (or also setting to Auto).
To limit the list size you should also specify MaxHeight Property

Update UI when adding to observable collection

iv'e got 2 itemscontrols bound to 2 observable collections
my ItemsControls :
<ItemsControl Grid.Column="4" Name="Pipe19" ItemsSource="{Binding Path=Pipes[19].Checkers}" Style="{StaticResource ItemsControlStyle}" ItemsPanel="{StaticResource TopPipePanelTemplate}" />
<ItemsControl Grid.Column="5" Name="Pipe18" ItemsSource="{Binding Path=Pipes[18].Checkers}" Style="{StaticResource ItemsControlStyle}" ItemsPanel="{StaticResource TopPipePanelTemplate}" />
their definitions through style :
<Style TargetType="{x:Type ItemsControl}" x:Key="ItemsControlStyle">
<Setter Property="ItemTemplate" Value="{StaticResource PipeDataItem}"></Setter>
<Setter Property="AllowDrop" Value="True"></Setter>
<EventSetter Event="PreviewMouseLeftButtonDown" Handler="ItemsControl_MouseLeftButtonDown"></EventSetter>
<EventSetter Event="Drop" Handler="ItemsControl_Drop"></EventSetter>
</Style>
both are considered drop targets , they contain ellipse items which i need to drag from one to the other . the problem is the Add part of the operation doesn't get rendered to the UI .
Visual Helper :
when drag drop as ellipse from the right one to the left :
this is the code which grabs the ellipse and saves the source control
private void ItemsControl_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
ItemsControl control = (ItemsControl)sender;
UIElement element = control.InputHitTest(e.GetPosition(control)) as UIElement;
if (element != null)
{
Ellipse ellipse = element as Ellipse;
DragSource = control;
DragDrop.DoDragDrop(ellipse, ellipse, DragDropEffects.Copy);
}
}
this is the code where the ellipse is dropped onto it's target :
private void ItemsControl_Drop(object sender, DragEventArgs e)
{
ItemsControl target = (ItemsControl)sender;
Ellipse ellipse = (Ellipse)e.Data.GetData(typeof(Ellipse));
((ObservableCollection<Checker>)DragSource.ItemsSource).Remove(ellipse.DataContext as Checker);
((ObservableCollection<Checker>)target.ItemsSource).Add(ellipse.DataContext as Checker);
}
the Checkers collections are described as follows : (Take notice that they are the itemscontrols ItemsSource :
public class Pipe
{
private ObservableCollection<Checker> checkers;
public ObservableCollection<Checker> Checkers
{
get
{
if (checkers == null)
checkers = new ObservableCollection<Checker>();
return checkers;
}
}
}
after the ItemsControl_Drop event the result is that only the remove updated the UI but the add on the target add not (i would expect that a new item would appear on the left one which i called Add on it's itemsource :
Another Visual Aid :
any ideas ?
Maybe the problem is that your adding to the ItemsSource of the ItemsControl instead of directly to the Pipes[#].Checkers.
I notice that the remove method is a remove from DragSource.ItemsSource.
You probably have a DragTargetControl which ItemsSource you should add to, which will update the Binding.
In the way your doing it you might be undoing the Binding if you cast the Binded ItemsSource to an ObservableCollection.
I think I know what is going on. I bet Pipes[] is not an ObservableCollection. Try it with Pipe1 and Pipe2. Remove from Pipe1 and add to Pipe2.
it turns out the ui element some how loses it's reference if you remove it
from one observable collection before adding it to another ,
so i added the ellipse to the target and then removed it from the source .
private void ItemsControl_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
ItemsControl control = (ItemsControl)sender;
UIElement element = control.InputHitTest(e.GetPosition(control)) as UIElement;
if (element != null && (element is Ellipse))
{
Ellipse ellipse = element as Ellipse;
DragSource = control;
DragDrop.DoDragDrop(ellipse, ellipse, DragDropEffects.Move);
}
}
private void ItemsControl_Drop(object sender, DragEventArgs e)
{
ItemsControl target = (ItemsControl)sender;
Ellipse ellipse = (Ellipse)e.Data.GetData(typeof(Ellipse));
ObservableCollection<Checker> collection = target.ItemsSource as ObservableCollection<Checker>;
Checker checker = ellipse.DataContext as Checker;
collection.Add(checker);
((ObservableCollection<Checker>)DragSource.ItemsSource).Remove(ellipse.DataContext as Checker);
}

Resources