Shrink ItemsControl items when visible space is filled - c#

I want to create a data-binded horizontal layout ItemsControl where for each item there would be a Button. When I add new items to the collection the ItemsControl should grow, relative to the Window it is in, until it reaches it's MaxWidth property. Then all buttons should shrink equally to fit inside MaxWidth. Something similar to the tabs of a Chrome browser.
Tabs with space:
Tabs with no empty space:
So far I've gotten to this:
<ItemsControl Name="ButtonsControl" MaxWidth="400">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type dataclasses:TextNote}">
<Button Content="{Binding Title}" MinWidth="80"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
When adding items the expansion of the StackPanel and Window are fine, but when MaxWidth is reached the items just start to disappear.

I don't think it is possible to produce that behaviour using any combination of the standard WPF controls, but this custom StackPanel control should do the job:
public class SqueezeStackPanel : Panel
{
private const double Tolerance = 0.001;
public static readonly DependencyProperty OrientationProperty = DependencyProperty.Register
("Orientation", typeof (Orientation), typeof (SqueezeStackPanel),
new FrameworkPropertyMetadata(Orientation.Horizontal, FrameworkPropertyMetadataOptions.AffectsMeasure,
OnOrientationChanged));
private readonly Dictionary<UIElement, Size> _childToConstraint = new Dictionary<UIElement, Size>();
private bool _isMeasureDirty;
private bool _isHorizontal = true;
private List<UIElement> _orderedSequence;
private Child[] _children;
static SqueezeStackPanel()
{
DefaultStyleKeyProperty.OverrideMetadata
(typeof (SqueezeStackPanel),
new FrameworkPropertyMetadata(typeof (SqueezeStackPanel)));
}
protected override bool HasLogicalOrientation
{
get { return true; }
}
protected override Orientation LogicalOrientation
{
get { return Orientation; }
}
public Orientation Orientation
{
get { return (Orientation) GetValue(OrientationProperty); }
set { SetValue(OrientationProperty, value); }
}
protected override Size ArrangeOverride(Size finalSize)
{
var size = new Size(_isHorizontal ? 0 : finalSize.Width, !_isHorizontal ? 0 : finalSize.Height);
var childrenCount = Children.Count;
var rc = new Rect();
for (var index = 0; index < childrenCount; index++)
{
var child = _orderedSequence[index];
var childVal = _children[index].Val;
if (_isHorizontal)
{
rc.Width = double.IsInfinity(childVal) ? child.DesiredSize.Width : childVal;
rc.Height = Math.Max(finalSize.Height, child.DesiredSize.Height);
size.Width += rc.Width;
size.Height = Math.Max(size.Height, rc.Height);
child.Arrange(rc);
rc.X += rc.Width;
}
else
{
rc.Width = Math.Max(finalSize.Width, child.DesiredSize.Width);
rc.Height = double.IsInfinity(childVal) ? child.DesiredSize.Height : childVal;
size.Width = Math.Max(size.Width, rc.Width);
size.Height += rc.Height;
child.Arrange(rc);
rc.Y += rc.Height;
}
}
return new Size(Math.Max(finalSize.Width, size.Width), Math.Max(finalSize.Height, size.Height));
}
protected override Size MeasureOverride(Size availableSize)
{
for (var i = 0; i < 3; i++)
{
_isMeasureDirty = false;
var childrenDesiredSize = new Size();
var childrenCount = Children.Count;
if (childrenCount == 0)
return childrenDesiredSize;
var childConstraint = GetChildrenConstraint(availableSize);
_children = new Child[childrenCount];
_orderedSequence = Children.Cast<UIElement>().ToList();
for (var index = 0; index < childrenCount; index++)
{
if (_isMeasureDirty)
break;
var child = _orderedSequence[index];
const double minLength = 0.0;
const double maxLength = double.PositiveInfinity;
MeasureChild(child, childConstraint);
if (_isHorizontal)
{
childrenDesiredSize.Width += child.DesiredSize.Width;
_children[index] = new Child(minLength, maxLength, child.DesiredSize.Width);
childrenDesiredSize.Height = Math.Max(childrenDesiredSize.Height, child.DesiredSize.Height);
}
else
{
childrenDesiredSize.Height += child.DesiredSize.Height;
_children[index] = new Child(minLength, maxLength, child.DesiredSize.Height);
childrenDesiredSize.Width = Math.Max(childrenDesiredSize.Width, child.DesiredSize.Width);
}
}
if (_isMeasureDirty)
continue;
var current = _children.Sum(s => s.Val);
var target = GetSizePart(availableSize);
var finalSize = new Size
(Math.Min(availableSize.Width, _isHorizontal ? current : childrenDesiredSize.Width),
Math.Min(availableSize.Height, _isHorizontal ? childrenDesiredSize.Height : current));
if (double.IsInfinity(target))
return finalSize;
RecalcChilds(current, target);
current = 0.0;
for (var index = 0; index < childrenCount; index++)
{
var child = _children[index];
if (IsGreater(current + child.Val, target, Tolerance) &&
IsGreater(target, current, Tolerance))
{
var rest = IsGreater(target, current, Tolerance) ? target - current : 0.0;
if (IsGreater(rest, child.Min, Tolerance))
child.Val = rest;
}
current += child.Val;
}
RemeasureChildren(finalSize);
finalSize = new Size
(Math.Min(availableSize.Width, _isHorizontal ? target : childrenDesiredSize.Width),
Math.Min(availableSize.Height, _isHorizontal ? childrenDesiredSize.Height : target));
if (_isMeasureDirty)
continue;
return finalSize;
}
return new Size();
}
public static double GetHeight(Thickness thickness)
{
return thickness.Top + thickness.Bottom;
}
public static double GetWidth(Thickness thickness)
{
return thickness.Left + thickness.Right;
}
protected override void OnVisualChildrenChanged(DependencyObject visualAdded, DependencyObject visualRemoved)
{
base.OnVisualChildrenChanged(visualAdded, visualRemoved);
var removedUiElement = visualRemoved as UIElement;
if (removedUiElement != null)
_childToConstraint.Remove(removedUiElement);
}
private Size GetChildrenConstraint(Size availableSize)
{
return new Size
(_isHorizontal ? double.PositiveInfinity : availableSize.Width,
!_isHorizontal ? double.PositiveInfinity : availableSize.Height);
}
private double GetSizePart(Size size)
{
return _isHorizontal ? size.Width : size.Height;
}
private static bool IsGreater(double a, double b, double tolerance)
{
return a - b > tolerance;
}
private void MeasureChild(UIElement child, Size childConstraint)
{
Size lastConstraint;
if ((child.IsMeasureValid && _childToConstraint.TryGetValue(child, out lastConstraint) &&
lastConstraint.Equals(childConstraint))) return;
child.Measure(childConstraint);
_childToConstraint[child] = childConstraint;
}
private static void OnOrientationChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var panel = (SqueezeStackPanel) d;
panel._isHorizontal = panel.Orientation == Orientation.Horizontal;
}
private void RecalcChilds(double current, double target)
{
var shouldShrink = IsGreater(current, target, Tolerance);
if (shouldShrink)
ShrinkChildren(_children, target);
}
private void RemeasureChildren(Size availableSize)
{
var childrenCount = Children.Count;
if (childrenCount == 0)
return;
var childConstraint = GetChildrenConstraint(availableSize);
for (var index = 0; index < childrenCount; index++)
{
var child = _orderedSequence[index];
if (Math.Abs(GetSizePart(child.DesiredSize) - _children[index].Val) > Tolerance)
MeasureChild(child, new Size(_isHorizontal ? _children[index].Val : childConstraint.Width,
!_isHorizontal ? _children[index].Val : childConstraint.Height));
}
}
private static void ShrinkChildren(IEnumerable<Child> children, double target)
{
var sortedChilds = children.OrderBy(v => v.Val).ToList();
var minValidTarget = sortedChilds.Sum(s => s.Min);
if (minValidTarget > target)
{
foreach (var child in sortedChilds)
child.Val = child.Min;
return;
}
do
{
var tmpTarget = target;
for (var iChild = 0; iChild < sortedChilds.Count; iChild++)
{
var child = sortedChilds[iChild];
if (child.Val*(sortedChilds.Count - iChild) >= tmpTarget)
{
var avg = tmpTarget/(sortedChilds.Count - iChild);
var success = true;
for (var jChild = iChild; jChild < sortedChilds.Count; jChild++)
{
var tChild = sortedChilds[jChild];
tChild.Val = Math.Max(tChild.Min, avg);
// Min constraint skip success expand on this iteration
if (Math.Abs(avg - tChild.Val) <= Tolerance) continue;
target -= tChild.Val;
success = false;
sortedChilds.RemoveAt(jChild);
jChild--;
}
if (success)
return;
break;
}
tmpTarget -= child.Val;
}
} while (sortedChilds.Count > 0);
}
private class Child
{
public readonly double Min;
public double Val;
public Child(double min, double max, double val)
{
Min = min;
Val = val;
Val = Math.Max(min, val);
Val = Math.Min(max, Val);
}
}
}
Try using it as your ItemsPanelTemplate:
<ItemsControl Name="ButtonsControl" MaxWidth="400">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<local:SqueezeStackPanel />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type dataclasses:TextNote}">
<Button Content="{Binding Title}" MinWidth="80"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
I can't be sure based on the code that you have supplied, but I think you will have better layout results by removing your MaxWidth on the ItemsControl.

You can achieve something like this using a UniformGrid with Rows="1". The problem is that you can either have it stretched or not and neither of these options will do exactly what you want:
If it's stretched, then your "tabs" will always fill the whole available width. So, if you only have 1, it will be stretched across the whole width. If you set MaxWidth for the "tab", then if you have 2 they will not be adjacent but floating each in the middle of its column.
If it's left-aligned, then it will be difficult to get any padding/margin in your control, because when it shrinks, the padding will stay, making the actual content invisible.
So basically you need a control that has a "preferred" width:
When it has more space available than this preferred width, it sets itself to the preferred width.
When it has less space, it just takes up all the space it has.
This cannot be achieved using XAML (as far as I can tell), but it's not too difficult to do in code-behind. Let's create a custom control for the "tab" (namespaces omitted):
<ContentControl x:Class="WpfApplication1.UserControl1">
<ContentControl.Template>
<ControlTemplate TargetType="ContentControl">
<Border BorderBrush="Black" BorderThickness="1" Padding="0,5">
<ContentPresenter HorizontalAlignment="Center" Content="{TemplateBinding Content}"></ContentPresenter>
</Border>
</ControlTemplate>
</ContentControl.Template>
Code behind:
public partial class UserControl1 : ContentControl
{
public double DefaultWidth
{
get { return (double)GetValue(DefaultWidthProperty); }
set { SetValue(DefaultWidthProperty, value); }
}
public static readonly DependencyProperty DefaultWidthProperty =
DependencyProperty.Register("DefaultWidth", typeof(double), typeof(UserControl1), new PropertyMetadata(200.0));
public UserControl1()
{
InitializeComponent();
}
protected override Size MeasureOverride(Size constraint)
{
Size baseSize = base.MeasureOverride(constraint);
baseSize.Width = Math.Min(DefaultWidth, constraint.Width);
return baseSize;
}
protected override Size ArrangeOverride(Size arrangeBounds)
{
Size baseBounds = base.ArrangeOverride(arrangeBounds);
baseBounds.Width = Math.Min(DefaultWidth, arrangeBounds.Width);
return baseBounds;
}
}
Then, you can create your ItemsControl, using a UniformGrid as the container:
<ItemsControl ItemsSource="{Binding Items}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<local:UserControl1 Content="{Binding}" Margin="0,0,5,0" DefaultWidth="150"></local:UserControl1>
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid Rows="1" HorizontalAlignment="Left"></UniformGrid>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
Here's a screenshot of the result with 3 items and many items (don't feel like counting them :)

Related

How to auto-adjust GridView item appearance and width depending on window width

I'm trying to get my GridView to automatically adjust the appearance of its items depending on screen width but every time I snap my app's window, the GridView items keep getting bunched too close together. How can I ensure that each GridView item will appear on its own line and highlight the entire row like a ListView item when hovered over when the window is snapped to 1 side or less than a certain width (say 720 - which is often used for the MasterDetailsView - CompactModeThresholdWidth="720")?
Wide window
Snapped window
Snapped window (expected result)
MainPage.xaml
<Page
x:Class="MyApp.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:data="using:MyApp.Models"
mc:Ignorable="d">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<StackPanel Margin="0,0,0,20" Grid.Row="0">
<TextBlock Text="Hello World"/>
</StackPanel>
<GridView Grid.Row="1"
IsItemClickEnabled="True"
ItemsSource="{x:Bind GridItems}"
ItemClick="GridView_ItemClick">
<GridView.ItemContainerStyle>
<Style TargetType="GridViewItem">
<Setter Property="HorizontalContentAlignment" Value="Left"/>
</Style>
</GridView.ItemContainerStyle>
<GridView.ItemsPanel>
<ItemsPanelTemplate>
<data:MyGridViewPanel/>
</ItemsPanelTemplate>
</GridView.ItemsPanel>
<GridView.ItemTemplate>
<DataTemplate x:DataType="data:GridItemMain">
<StackPanel Orientation="Horizontal">
<TextBlock Grid.Column="0"
Text="{Binding Icon}"
Style="{StaticResource TitleTextBlockStyle}"
FontFamily="Segoe MDL2 Assets"/>
<TextBlock Grid.Column="1"
Text="{Binding Title}"
Style="{StaticResource TitleTextBlockStyle}"/>
</StackPanel>
</DataTemplate>
</GridView.ItemTemplate>
</GridView>
</Grid>
</Page>
MainPage.xaml.cs
public sealed partial class MainPage : Page
{
public List<GridItemMain> GridItems;
public MainPage()
{
this.InitializeComponent();
GridItems = GridItemManager.GetGridItems();
}
}
MainGridItem.cs
public class MainGridItem
{
public string Icon { get; set; }
public string Title { get; set; }
}
public class GridItemManager
{
public static List<MainGridItem> GetGridItems()
{
var gridItems = new List<MainGridItem>();
gridItems.Add(new MainGridItem { Icon = "\uE770", Title = "System" });
gridItems.Add(new MainGridItem { Icon = "\uE772", Title = "Devices" });
gridItems.Add(new MainGridItem { Icon = "\uEC75", Title = "Phone" });
gridItems.Add(new MainGridItem { Icon = "\uE774", Title = "Network & Internet" });
gridItems.Add(new MainGridItem { Icon = "\uE771", Title = "Personalisation" });
return gridItems;
}
}
MyGridViewPanel.cs
public class MyGridViewPanel : Panel
{
private double _maxWidth;
private double _maxHeight;
protected override Size ArrangeOverride(Size finalSize)
{
var x = 0.0;
var y = 0.0;
double width = Window.Current.Bounds.Width;
if (width <= 720)
{
foreach (var child in Children)
{
var newpos = new Rect(0, y, width, _maxHeight);
child.Arrange(newpos);
y += _maxHeight;
}
return finalSize;
}
else {
foreach (var child in Children)
{
if ((_maxWidth + x) > finalSize.Width)
{
x = 0;
y += _maxHeight;
}
var newpos = new Rect(x, y, _maxWidth, _maxHeight);
child.Arrange(newpos);
x += _maxWidth;
}
return finalSize;
}
}
protected override Size MeasureOverride(Size availableSize)
{
double width = Window.Current.Bounds.Width;
if (width <= 720)
{
foreach (var child in Children)
{
child.Measure(new Size(width, availableSize.Height));
var desiredheight = child.DesiredSize.Height;
if (desiredheight > _maxHeight)
_maxHeight = desiredheight;
}
return new Size(width, _maxHeight * Children.Count);
}
else {
foreach (var child in Children)
{
child.Measure(availableSize);
var desirtedwidth = child.DesiredSize.Width;
if (desirtedwidth > _maxWidth)
_maxWidth = desirtedwidth;
var desiredheight = child.DesiredSize.Height;
if (desiredheight > _maxHeight)
_maxHeight = desiredheight;
}
var itemperrow = Math.Floor(availableSize.Width / _maxWidth);
var rows = Math.Ceiling(Children.Count / itemperrow);
return new Size(itemperrow * _maxWidth, _maxHeight * rows);
}
}
}
Update
If you want to display an item on each line when the window is less than 720px, you could get the current window's width in the ArrangeOverride and MeasureOverride method, then resize the item and re-layout each item to show them in each row. For example:
MyGridViewPanel.cs
public class MyGridViewPanel : Panel
{
private double _maxWidth;
private double _maxHeight;
protected override Size ArrangeOverride(Size finalSize)
{
var x = 0.0;
var y = 0.0;
double width = Window.Current.Bounds.Width;
if (width <= 720)
{
foreach (var child in Children)
{
var newpos = new Rect(0, y, width, _maxHeight);
child.Arrange(newpos);
y += _maxHeight;
}
return finalSize;
}
else {
foreach (var child in Children)
{
if ((_maxWidth + x) > finalSize.Width)
{
x = 0;
y += _maxHeight;
}
var newpos = new Rect(x, y, _maxWidth, _maxHeight);
child.Arrange(newpos);
x += _maxWidth;
}
return finalSize;
}
}
protected override Size MeasureOverride(Size availableSize)
{
double width = Window.Current.Bounds.Width;
if (width <= 720)
{
foreach (var child in Children)
{
child.Measure(new Size(width, availableSize.Height));
var desiredheight = child.DesiredSize.Height;
if (desiredheight > _maxHeight)
_maxHeight = desiredheight;
}
return new Size(width, _maxHeight * Children.Count);
}
else {
foreach (var child in Children)
{
child.Measure(availableSize);
var desirtedwidth = child.DesiredSize.Width;
if (desirtedwidth > _maxWidth)
_maxWidth = desirtedwidth;
var desiredheight = child.DesiredSize.Height;
if (desiredheight > _maxHeight)
_maxHeight = desiredheight;
}
var itemperrow = Math.Floor(availableSize.Width / _maxWidth);
var rows = Math.Ceiling(Children.Count / itemperrow);
return new Size(itemperrow * _maxWidth, _maxHeight * rows);
}
}
}

How to recreate MenuItem ItemTapped and Command with a Button

I have a model called "ListItem", and I'm printing a list of them. I used to just use a ListView, but I wanted to print them horizontally, so I created a custom WrapLayout.
The ListView had simple ways to perform actions when the items were tapped or long-pressed. When a MenuItem was tapped, PutBack was called, and the ListItem was passed through as parameters. When a MenuItem was long-pressed, a button reading "delete" appeared in the toolbar, and when that was tapped, DeleteListItem was called and the ListItem was passed through as parameters.
Old Xaml:
<ListView x:Name="inactiveList" ItemTapped="PutBack" >
<ListView.ItemTemplate>
<DataTemplate>
<TextCell Text="{Binding Name}">
<TextCell.ContextActions>
<MenuItem Command="{Binding Source={x:Reference ListPage}, Path=DeleteListItem}"
CommandParameter="{Binding .}" Text="delete" />
</TextCell.ContextActions>
</TextCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
I'm having trouble recreating this functionality with the buttons generated by my custom WrapLayout. Is this possible to do? I tried adding a Clicked property to the buttons, but VS tells me I can only set Clicked with += or -=, which doesn't make any sense to me.
New Xaml:
<local:WrapLayout x:Name="inactiveList" Spacing="5" />
New Code-Behind:
namespace Myapp
{
public partial class ListPage
{
public Command DeleteListItem { get; set; }
public ListPage()
{
InitializeComponent();
ObservableCollection<ListItem> inactiveItems =
new ObservableCollection<ListItem>(
App.ListItemRepo.GetInactiveListItems());
inactiveList.ItemsSource = inactiveItems;
DeleteListItem = new Command(async (parameter) => {
ListItem item = (ListItem)parameter as ListItem;
App.ListItemRepo.DeleteListItemAsync(item);
ObservableCollection<ListItem> deleteInactiveItems =
new ObservableCollection<ListItem>(
await App.ListItemRepo.GetInactiveListItemsAsync());
inactiveList.ItemsSource = deleteInactiveItems;
});
}
public async void PutBack(object sender, ItemTappedEventArgs e)
{
var selectedListItem = e.Item as ListItem;
await App.ListItemRepo.SetListItemActive(selectedListItem);
ObservableCollection<ListItem> inactiveItems =
new ObservableCollection<ListItem>(
await App.ListItemRepo.GetInactiveListItemsAsync());
inactiveList.ItemsSource = inactiveItems;
}
}
public class WrapLayout : Layout<View>
{
public ObservableCollection<ListItem> ItemsSource
{
get { return (ObservableCollection<ListItem>)GetValue(ItemSourceProperty); }
set { SetValue(ItemSourceProperty, value); }
}
public static readonly BindableProperty ItemSourceProperty =
BindableProperty.Create
(
"ItemsSource",
typeof(ObservableCollection<ListItem>),
typeof(WrapLayout),
propertyChanged: (bindable, oldvalue, newvalue) => ((WrapLayout)bindable).AddViews()
);
// THIS IS WHERE THE BUTTONS ARE SET
void AddViews()
{
Children.Clear();
foreach (ListItem s in ItemsSource)
{
Button button = new Button();
button.Text = s.Name;
Children.Add(button);
}
}
public static readonly BindableProperty SpacingProperty =
BindableProperty.Create
(
"Spacing",
typeof(double),
typeof(WrapLayout),
10.0,
propertyChanged: (bindable, oldvalue, newvalue) => ((WrapLayout)bindable).OnSizeChanged()
);
public double Spacing
{
get { return (double)GetValue(SpacingProperty); }
set { SetValue(SpacingProperty, value); }
}
private void OnSizeChanged()
{
this.ForceLayout();
}
protected override SizeRequest OnMeasure(double widthConstraint, double heightConstraint)
{
if (WidthRequest > 0)
widthConstraint = Math.Min(widthConstraint, WidthRequest);
if (HeightRequest > 0)
heightConstraint = Math.Min(heightConstraint, HeightRequest);
double internalWidth = double.IsPositiveInfinity(widthConstraint) ? double.PositiveInfinity : Math.Max(0, widthConstraint);
double internalHeight = double.IsPositiveInfinity(heightConstraint) ? double.PositiveInfinity : Math.Max(0, heightConstraint);
return DoHorizontalMeasure(internalWidth, internalHeight);
}
private SizeRequest DoHorizontalMeasure(double widthConstraint, double heightConstraint)
{
int rowCount = 1;
double width = 0;
double height = 0;
double minWidth = 0;
double minHeight = 0;
double widthUsed = 0;
foreach (var item in Children)
{
var size = item.Measure(widthConstraint, heightConstraint);
height = Math.Max(height, size.Request.Height);
var newWidth = width + size.Request.Width + Spacing;
if (newWidth > widthConstraint)
{
rowCount++;
widthUsed = Math.Max(width, widthUsed);
width = size.Request.Width;
}
else
width = newWidth;
minHeight = Math.Max(minHeight, size.Minimum.Height);
minWidth = Math.Max(minWidth, size.Minimum.Width);
}
if (rowCount > 1)
{
width = Math.Max(width, widthUsed);
height = (height + Spacing) * rowCount - Spacing; // via MitchMilam
}
return new SizeRequest(new Size(width, height), new Size(minWidth, minHeight));
}
protected override void LayoutChildren(double x, double y, double width, double height)
{
double rowHeight = 0;
double yPos = y, xPos = x;
foreach (var child in Children.Where(c => c.IsVisible))
{
var request = child.Measure(width, height);
double childWidth = request.Request.Width;
double childHeight = request.Request.Height;
rowHeight = Math.Max(rowHeight, childHeight);
if (xPos + childWidth > width)
{
xPos = x;
yPos += rowHeight + Spacing;
rowHeight = 0;
}
var region = new Rectangle(xPos, yPos, childWidth, childHeight);
LayoutChildIntoBoundingRegion(child, region);
xPos += region.Width + Spacing;
}
}
}
}

ListBox ItemsPanel Style

I'm trying to stylize my ListBox and I would like that my Items like this:
1 4 7 10 13 16 19 22 25 28
2 5 8 11 14 17 20 23 26 29
3 6 9 12 15 18 21 24 27 30
Only 3 items vertically and the ScrollBar horizontal.
What I tried:
<Setter Property="ItemsPanel">
<Setter.Value>
<ItemsPanelTemplate>
<WrapPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</Setter.Value>
</Setter>
But is not working and I have no idea what to do
This seems to be the solution:
public class UniformGridWithOrientation : UniformGrid
{
#region Orientation (Dependency Property)
public static readonly DependencyProperty OrientationProperty =
DependencyProperty.Register("Orientation", typeof(System.Windows.Controls.Orientation), typeof(UniformGridWithOrientation),
new FrameworkPropertyMetadata(
System.Windows.Controls.Orientation.Vertical,
FrameworkPropertyMetadataOptions.AffectsMeasure),
new ValidateValueCallback(UniformGridWithOrientation.IsValidOrientation));
internal static bool IsValidOrientation(object o)
{
System.Windows.Controls.Orientation orientation = (System.Windows.Controls.Orientation)o;
if (orientation != System.Windows.Controls.Orientation.Horizontal)
{
return (orientation == System.Windows.Controls.Orientation.Vertical);
}
return true;
}
public System.Windows.Controls.Orientation Orientation
{
get { return (System.Windows.Controls.Orientation)GetValue(OrientationProperty); }
set { SetValue(OrientationProperty, value); }
}
#endregion
protected override Size MeasureOverride(Size constraint)
{
this.UpdateComputedValues();
Size availableSize = new Size(constraint.Width / ((double)this._columns), constraint.Height / ((double)this._rows));
double width = 0.0;
double height = 0.0;
int num3 = 0;
int count = base.InternalChildren.Count;
while (num3 < count)
{
UIElement element = base.InternalChildren[num3];
element.Measure(availableSize);
Size desiredSize = element.DesiredSize;
if (width < desiredSize.Width)
{
width = desiredSize.Width;
}
if (height < desiredSize.Height)
{
height = desiredSize.Height;
}
num3++;
}
return new Size(width * this._columns, height * this._rows);
}
private int _columns;
private int _rows;
private void UpdateComputedValues()
{
this._columns = this.Columns;
this._rows = this.Rows;
if (this.FirstColumn >= this._columns)
{
this.FirstColumn = 0;
}
if (FirstColumn > 0)
throw new NotImplementedException("There is no support for seting the FirstColumn (nor the FirstRow).");
if ((this._rows == 0) || (this._columns == 0))
{
int num = 0; // Visible children
int num2 = 0;
int count = base.InternalChildren.Count;
while (num2 < count)
{
UIElement element = base.InternalChildren[num2];
if (element.Visibility != Visibility.Collapsed)
{
num++;
}
num2++;
}
if (num == 0)
{
num = 1;
}
if (this._rows == 0)
{
if (this._columns > 0)
{
this._rows = ((num + this.FirstColumn) + (this._columns - 1)) / this._columns;
}
else
{
this._rows = (int)Math.Sqrt((double)num);
if ((this._rows * this._rows) < num)
{
this._rows++;
}
this._columns = this._rows;
}
}
else if (this._columns == 0)
{
this._columns = (num + (this._rows - 1)) / this._rows;
}
}
}
protected override Size ArrangeOverride(Size arrangeSize)
{
Rect finalRect = new Rect(0.0, 0.0, arrangeSize.Width / ((double)this._columns), arrangeSize.Height / ((double)this._rows));
double height = finalRect.Height;
double numX = arrangeSize.Height - 1.0;
finalRect.X += finalRect.Width * this.FirstColumn;
foreach (UIElement element in base.InternalChildren)
{
element.Arrange(finalRect);
if (element.Visibility != Visibility.Collapsed)
{
finalRect.Y += height;
if (finalRect.Y >= numX)
{
finalRect.X += finalRect.Width;
finalRect.Y = 0.0;
}
}
}
return arrangeSize;
}
}
Put this class in one of your namespaces.
This is my XAML:
<Window x:Class="ListItemsVerticaly3.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:ListItemsVerticaly3"
Title="MainWindow" Height="350" Width="525">
<Grid>
<ListView ItemsSource="{Binding numbers}">
<ListView.ItemsPanel>
<ItemsPanelTemplate>
<local:UniformGridWithOrientation Rows="3" />
</ItemsPanelTemplate>
</ListView.ItemsPanel>
</ListView>
</Grid>
And this is the result i got:
Forgot my code behind, simple but it does the job for this example:
public partial class MainWindow : Window
{
public ObservableCollection<int> numbers { get; set; }
public MainWindow()
{
InitializeComponent();
numbers = new ObservableCollection<int>();
IEnumerable<int> generatedNumbers = Enumerable.Range(1, 20).Select(x => x);
foreach (int nr in generatedNumbers)
{
numbers.Add(nr);
}
this.DataContext = this;
}
}
Try this solution:
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal"></StackPanel>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
With ListView also we can achieve
<ListView.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Width="{Binding (FrameworkElement.ActualWidth),
RelativeSource={RelativeSource AncestorType=ScrollContentPresenter}}"
ItemWidth="{Binding (ListView.View).ItemWidth,
RelativeSource={RelativeSource AncestorType=ListView}}"
MinWidth="{Binding ItemWidth, RelativeSource={RelativeSource Self}}"
ItemHeight="{Binding (ListView.View).ItemHeight,
RelativeSource={RelativeSource AncestorType=ListView}}" />
</ItemsPanelTemplate>
</ListView.ItemsPanel>

WrapPanel with last-in-row fill

What I need: listbox with textboxes inside, textboxes wraps, and last in row fills remaining space:
|word 1||word 2___|
|word 3___________|
I'm trying to implement this behaviour using that advice. My xaml:
<ListBox ScrollViewer.HorizontalScrollBarVisibility="Disabled"
ItemsSource="{Binding Tags}"
HorizontalContentAlignment="Stretch">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<controls:WrapPanelLastChildFill />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemTemplate>
<DataTemplate>
<TextBox Text="{Binding Text}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
MyWrapPanel (inherits form WrapPanel) code:
protected override Size MeasureOverride(Size constraint)
{
Size curLineSize = new Size();
Size panelSize = new Size(constraint.Width, 0);
UIElementCollection children = base.InternalChildren;
for (int i = 0; i < children.Count; i++)
{
UIElement child = children[i] as UIElement;
child.Measure(constraint);
Size sz = child.DesiredSize;
if (curLineSize.Width + sz.Width > constraint.Width) // new line
{
panelSize.Width = Math.Max(curLineSize.Width, panelSize.Width);
panelSize.Height += curLineSize.Height;
if (i > 0)
{
// change width of prev control here
var lastChildInRow = children[i - 1] as Control;
lastChildInRow.Width = lastChildInRow.ActualWidth + panelSize.Width - curLineSize.Width;
}
curLineSize = sz;
}
else
{
curLineSize.Width += sz.Width;
curLineSize.Height = Math.Max(sz.Height, curLineSize.Height);
}
}
panelSize.Width = Math.Max(curLineSize.Width, panelSize.Width);
panelSize.Height += curLineSize.Height;
return panelSize;
}
Thats work, but in one side only - textbox width never shrinks.
Any help appreciated.
I have done it recently.
You can see my code at: CodeProject
or use this class directly as shown:
Usage:
<TextBox MinWidth="120" wrapPanelWithFill:WrapPanelFill.UseToFill="True">*</TextBox>
Code:
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Windows;
using System.Windows.Controls;
namespace WrapPanelWithFill
{
public class WrapPanelFill : WrapPanel
{
// ******************************************************************
public static readonly DependencyProperty UseToFillProperty = DependencyProperty.RegisterAttached("UseToFill", typeof(Boolean),
typeof(WrapPanelFill), new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.AffectsRender));
// ******************************************************************
public static void SetUseToFill(UIElement element, Boolean value)
{
element.SetValue(UseToFillProperty, value);
}
// ******************************************************************
public static Boolean GetUseToFill(UIElement element)
{
return (Boolean)element.GetValue(UseToFillProperty);
}
// ******************************************************************
const double DBL_EPSILON = 2.2204460492503131e-016; /* smallest such that 1.0+DBL_EPSILON != 1.0 */
// ******************************************************************
private static bool DoubleAreClose(double value1, double value2)
{
//in case they are Infinities (then epsilon check does not work)
if (value1 == value2) return true;
// This computes (|value1-value2| / (|value1| + |value2| + 10.0)) < DBL_EPSILON
double eps = (Math.Abs(value1) + Math.Abs(value2) + 10.0) * DBL_EPSILON;
double delta = value1 - value2;
return (-eps < delta) && (eps > delta);
}
// ******************************************************************
private static bool DoubleGreaterThan(double value1, double value2)
{
return (value1 > value2) && !DoubleAreClose(value1, value2);
}
// ******************************************************************
private bool _atLeastOneElementCanHasItsWidthExpanded = false;
// ******************************************************************
/// <summary>
/// <see cref="FrameworkElement.MeasureOverride"/>
/// </summary>
protected override Size MeasureOverride(Size constraint)
{
UVSize curLineSize = new UVSize(Orientation);
UVSize panelSize = new UVSize(Orientation);
UVSize uvConstraint = new UVSize(Orientation, constraint.Width, constraint.Height);
double itemWidth = ItemWidth;
double itemHeight = ItemHeight;
bool itemWidthSet = !Double.IsNaN(itemWidth);
bool itemHeightSet = !Double.IsNaN(itemHeight);
Size childConstraint = new Size(
(itemWidthSet ? itemWidth : constraint.Width),
(itemHeightSet ? itemHeight : constraint.Height));
UIElementCollection children = InternalChildren;
// EO
LineInfo currentLineInfo = new LineInfo(); // EO, the way it works it is always like we are on the current line
_lineInfos.Clear();
_atLeastOneElementCanHasItsWidthExpanded = false;
for (int i = 0, count = children.Count; i < count; i++)
{
UIElement child = children[i] as UIElement;
if (child == null) continue;
//Flow passes its own constrint to children
child.Measure(childConstraint);
//this is the size of the child in UV space
UVSize sz = new UVSize(
Orientation,
(itemWidthSet ? itemWidth : child.DesiredSize.Width),
(itemHeightSet ? itemHeight : child.DesiredSize.Height));
if (DoubleGreaterThan(curLineSize.U + sz.U, uvConstraint.U)) //need to switch to another line
{
// EO
currentLineInfo.Size = curLineSize;
_lineInfos.Add(currentLineInfo);
panelSize.U = Math.Max(curLineSize.U, panelSize.U);
panelSize.V += curLineSize.V;
curLineSize = sz;
// EO
currentLineInfo = new LineInfo();
var feChild = child as FrameworkElement;
if (GetUseToFill(feChild))
{
currentLineInfo.ElementsWithNoWidthSet.Add(feChild);
_atLeastOneElementCanHasItsWidthExpanded = true;
}
if (DoubleGreaterThan(sz.U, uvConstraint.U)) //the element is wider then the constrint - give it a separate line
{
currentLineInfo = new LineInfo();
panelSize.U = Math.Max(sz.U, panelSize.U);
panelSize.V += sz.V;
curLineSize = new UVSize(Orientation);
}
}
else //continue to accumulate a line
{
curLineSize.U += sz.U;
curLineSize.V = Math.Max(sz.V, curLineSize.V);
// EO
var feChild = child as FrameworkElement;
if (GetUseToFill(feChild))
{
currentLineInfo.ElementsWithNoWidthSet.Add(feChild);
_atLeastOneElementCanHasItsWidthExpanded = true;
}
}
}
if (curLineSize.U > 0)
{
currentLineInfo.Size = curLineSize;
_lineInfos.Add(currentLineInfo);
}
//the last line size, if any should be added
panelSize.U = Math.Max(curLineSize.U, panelSize.U);
panelSize.V += curLineSize.V;
// EO
if (_atLeastOneElementCanHasItsWidthExpanded)
{
return new Size(constraint.Width, panelSize.Height);
}
//go from UV space to W/H space
return new Size(panelSize.Width, panelSize.Height);
}
// ************************************************************************
private struct UVSize
{
internal UVSize(Orientation orientation, double width, double height)
{
U = V = 0d;
_orientation = orientation;
Width = width;
Height = height;
}
internal UVSize(Orientation orientation)
{
U = V = 0d;
_orientation = orientation;
}
internal double U;
internal double V;
private Orientation _orientation;
internal double Width
{
get { return (_orientation == Orientation.Horizontal ? U : V); }
set { if (_orientation == Orientation.Horizontal) U = value; else V = value; }
}
internal double Height
{
get { return (_orientation == Orientation.Horizontal ? V : U); }
set { if (_orientation == Orientation.Horizontal) V = value; else U = value; }
}
}
// ************************************************************************
private class LineInfo
{
public List<UIElement> ElementsWithNoWidthSet = new List<UIElement>();
// public double SpaceLeft = 0;
// public double WidthCorrectionPerElement = 0;
public UVSize Size;
public double Correction = 0;
}
private List<LineInfo> _lineInfos = new List<LineInfo>();
// ************************************************************************
/// <summary>
/// <see cref="FrameworkElement.ArrangeOverride"/>
/// </summary>
protected override Size ArrangeOverride(Size finalSize)
{
int lineIndex = 0;
int firstInLine = 0;
double itemWidth = ItemWidth;
double itemHeight = ItemHeight;
double accumulatedV = 0;
double itemU = (Orientation == Orientation.Horizontal ? itemWidth : itemHeight);
UVSize curLineSize = new UVSize(Orientation);
UVSize uvFinalSize = new UVSize(Orientation, finalSize.Width, finalSize.Height);
bool itemWidthSet = !Double.IsNaN(itemWidth);
bool itemHeightSet = !Double.IsNaN(itemHeight);
bool useItemU = (Orientation == Orientation.Horizontal ? itemWidthSet : itemHeightSet);
UIElementCollection children = InternalChildren;
for (int i = 0, count = children.Count; i < count; i++)
{
UIElement child = children[i] as UIElement;
if (child == null) continue;
UVSize sz = new UVSize(
Orientation,
(itemWidthSet ? itemWidth : child.DesiredSize.Width),
(itemHeightSet ? itemHeight : child.DesiredSize.Height));
if (DoubleGreaterThan(curLineSize.U + sz.U, uvFinalSize.U)) //need to switch to another line
{
arrangeLine(lineIndex, accumulatedV, curLineSize.V, firstInLine, i, useItemU, itemU, uvFinalSize);
lineIndex++;
accumulatedV += curLineSize.V;
curLineSize = sz;
if (DoubleGreaterThan(sz.U, uvFinalSize.U)) //the element is wider then the constraint - give it a separate line
{
//switch to next line which only contain one element
arrangeLine(lineIndex, accumulatedV, sz.V, i, ++i, useItemU, itemU, uvFinalSize);
accumulatedV += sz.V;
curLineSize = new UVSize(Orientation);
}
firstInLine = i;
}
else //continue to accumulate a line
{
curLineSize.U += sz.U;
curLineSize.V = Math.Max(sz.V, curLineSize.V);
}
}
//arrange the last line, if any
if (firstInLine < children.Count)
{
arrangeLine(lineIndex, accumulatedV, curLineSize.V, firstInLine, children.Count, useItemU, itemU, uvFinalSize);
}
return finalSize;
}
// ************************************************************************
private void arrangeLine(int lineIndex, double v, double lineV, int start, int end, bool useItemU, double itemU, UVSize uvFinalSize)
{
double u = 0;
bool isHorizontal = (Orientation == Orientation.Horizontal);
Debug.Assert(lineIndex < _lineInfos.Count);
LineInfo lineInfo = _lineInfos[lineIndex];
double lineSpaceAvailableForCorrection = Math.Max(uvFinalSize.U - lineInfo.Size.U, 0);
double perControlCorrection = 0;
if (lineSpaceAvailableForCorrection > 0 && lineInfo.Size.U > 0)
{
perControlCorrection = lineSpaceAvailableForCorrection / lineInfo.ElementsWithNoWidthSet.Count;
if (double.IsInfinity(perControlCorrection))
{
perControlCorrection = 0;
}
}
int indexOfControlToAdjustSizeToFill = 0;
UIElement uIElementToAdjustNext = indexOfControlToAdjustSizeToFill < lineInfo.ElementsWithNoWidthSet.Count ? lineInfo.ElementsWithNoWidthSet[indexOfControlToAdjustSizeToFill] : null;
UIElementCollection children = InternalChildren;
for (int i = start; i < end; i++)
{
UIElement child = children[i] as UIElement;
if (child != null)
{
UVSize childSize = new UVSize(Orientation, child.DesiredSize.Width, child.DesiredSize.Height);
double layoutSlotU = (useItemU ? itemU : childSize.U);
if (perControlCorrection > 0 && child == uIElementToAdjustNext)
{
layoutSlotU += perControlCorrection;
indexOfControlToAdjustSizeToFill++;
uIElementToAdjustNext = indexOfControlToAdjustSizeToFill < lineInfo.ElementsWithNoWidthSet.Count ? lineInfo.ElementsWithNoWidthSet[indexOfControlToAdjustSizeToFill] : null;
}
child.Arrange(new Rect(
(isHorizontal ? u : v),
(isHorizontal ? v : u),
(isHorizontal ? layoutSlotU : lineV),
(isHorizontal ? lineV : layoutSlotU)));
u += layoutSlotU;
}
}
}
// ************************************************************************
}
}
It was misunderstanding where to place width correction. This must be in ArrangeOverride:
private void ArrangeLine(double y, Size lineSize, double boundsWidth, int start, int end)
{
double x = 0;
UIElementCollection children = InternalChildren;
for (int i = start; i < end; i++)
{
UIElement child = children[i];
var w = child.DesiredSize.Width;
if (LastChildFill && i == end - 1) // last сhild fills remaining space
{
w = boundsWidth - x;
}
child.Arrange(new Rect(x, y, w, lineSize.Height));
x += w;
}
}

How to speed up rendering of vertical scrollbar markers

I have a customized vertical scrollbar which displays markers for selected items in a DataGrid.
The problem I'm facing is, when there are a great number of items (e.g. could be 5000 to 50000) there is a lag while it is rendering the markers.
With the following code it basically renders as per the selected items index, number of items and height of the track. Obviously this is inefficient and am looking for other solutions.
This is my customized vertical scrollbar
<helpers:MarkerPositionConverter x:Key="MarkerPositionConverter"/>
<ControlTemplate x:Key="VerticalScrollBar" TargetType="{x:Type ScrollBar}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition MaxHeight="18" />
<RowDefinition Height="0.00001*" />
<RowDefinition MaxHeight="18" />
</Grid.RowDefinitions>
<Border Grid.RowSpan="3"
CornerRadius="2"
Background="#F0F0F0" />
<RepeatButton Grid.Row="0"
Style="{StaticResource ScrollBarLineButton}"
Height="18"
Command="ScrollBar.LineUpCommand"
Content="M 0 4 L 8 4 L 4 0 Z" />
<!--START-->
<ItemsControl VerticalAlignment="Stretch" x:Name="ItemsSelected"
ItemsSource="{Binding ElementName=GenericDataGrid, Path=SelectedItems}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Rectangle Fill="SlateGray" Width="9" Height="4">
<Rectangle.RenderTransform>
<TranslateTransform>
<TranslateTransform.Y>
<MultiBinding Converter="{StaticResource MarkerPositionConverter}" FallbackValue="-1000">
<Binding/>
<Binding RelativeSource="{RelativeSource AncestorType={x:Type DataGrid}}" />
<Binding Path="ActualHeight" ElementName="ItemsSelected"/>
<Binding Path="Items.Count" ElementName="GenericDataGrid"/>
</MultiBinding>
</TranslateTransform.Y>
</TranslateTransform>
</Rectangle.RenderTransform>
</Rectangle>
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas ClipToBounds="True"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
<!--END-->
<Track x:Name="PART_Track" Grid.Row="1" IsDirectionReversed="true">
<Track.DecreaseRepeatButton>
<RepeatButton Style="{StaticResource ScrollBarPageButton}"
Command="ScrollBar.PageUpCommand" />
</Track.DecreaseRepeatButton>
<Track.Thumb>
<Thumb Style="{StaticResource ScrollBarThumb}" Margin="1,0,1,0">
<Thumb.BorderBrush>
<LinearGradientBrush StartPoint="0,0" EndPoint="1,0">
<LinearGradientBrush.GradientStops>
<GradientStopCollection>
<GradientStop Color="{DynamicResource BorderLightColor}" Offset="0.0" />
<GradientStop Color="{DynamicResource BorderDarkColor}" Offset="1.0" />
</GradientStopCollection>
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
</Thumb.BorderBrush>
<Thumb.Background>
<LinearGradientBrush StartPoint="0,0"
EndPoint="1,0">
<LinearGradientBrush.GradientStops>
<GradientStopCollection>
<GradientStop Color="{DynamicResource ControlLightColor}" Offset="0.0" />
<GradientStop Color="{DynamicResource ControlMediumColor}" Offset="1.0" />
</GradientStopCollection>
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
</Thumb.Background>
</Thumb>
</Track.Thumb>
<Track.IncreaseRepeatButton>
<RepeatButton Style="{StaticResource ScrollBarPageButton}" Command="ScrollBar.PageDownCommand" />
</Track.IncreaseRepeatButton>
</Track>
<RepeatButton Grid.Row="3" Style="{StaticResource ScrollBarLineButton}" Height="18" Command="ScrollBar.LineDownCommand" Content="M 0 0 L 4 4 L 8 0 Z" />
</Grid>
</ControlTemplate>
This is my converter that transforms the Y position and scales accordingly if the DataGrid height changes.
public class MarkerPositionConverter: IMultiValueConverter
{
//Performs the index to translate conversion
public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
try
{
//calculated the transform values based on the following
object o = (object)values[0];
DataGrid dg = (DataGrid)values[1];
double itemIndex = dg.Items.IndexOf(o);
double trackHeight = (double)values[2];
int itemCount = (int)values[3];
double translateDelta = trackHeight / itemCount;
return itemIndex * translateDelta;
}
catch (Exception ex)
{
Console.WriteLine("MarkerPositionConverter error : " + ex.Message);
return false;
}
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
[RE-EDIT] I have tried to create a separate class for a marker canvas, for use with ObservableCollection's. Note that at present, this does not work.
XAML still the same as yesterday:
<helpers:MarkerCollectionCanvas
x:Name="SearchMarkerCanvas"
Grid.Row="1"
Grid="{Binding RelativeSource={RelativeSource AncestorType={x:Type DataGrid}}}"
MarkerCollection="{Binding Source={x:Static helpers:MyClass.Instance}, Path=SearchMarkers}"/>
Canvas class, ObservableCollection changed to use object instead of double, there is a console.writeline in MarkerCollectionCanvas_CollectionChanged that never gets called:
class MarkerCollectionCanvas : Canvas
{
public DataGrid Grid
{
get { return (DataGrid)GetValue(GridProperty); }
set { SetValue(GridProperty, value); }
}
public static readonly DependencyProperty GridProperty =
DependencyProperty.Register("Grid", typeof(DataGrid), typeof(MarkerCollectionCanvas), new PropertyMetadata(null));
public ObservableCollection<object> MarkerCollection
{
get { return (ObservableCollection<object>)GetValue(MarkerCollectionProperty); }
set { SetValue(MarkerCollectionProperty, value); }
}
public static readonly DependencyProperty MarkerCollectionProperty =
DependencyProperty.Register("MarkerCollection", typeof(ObservableCollection<object>), typeof(MarkerCollectionCanvas), new PropertyMetadata(null, OnCollectionChanged));
private static void OnCollectionChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
MarkerCollectionCanvas canvas = d as MarkerCollectionCanvas;
if (e.NewValue != null)
{
(e.NewValue as ObservableCollection<object>).CollectionChanged += canvas.MarkerCollectionCanvas_CollectionChanged;
}
if (e.OldValue != null)
{
(e.NewValue as ObservableCollection<object>).CollectionChanged -= canvas.MarkerCollectionCanvas_CollectionChanged;
}
}
void MarkerCollectionCanvas_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
Console.WriteLine("InvalidateVisual");
InvalidateVisual();
}
public Brush MarkerBrush
{
get { return (Brush)GetValue(MarkerBrushProperty); }
set { SetValue(MarkerBrushProperty, value); }
}
public static readonly DependencyProperty MarkerBrushProperty =
DependencyProperty.Register("MarkerBrush", typeof(Brush), typeof(MarkerCollectionCanvas), new PropertyMetadata(Brushes.DarkOrange));
protected override void OnRender(System.Windows.Media.DrawingContext dc)
{
base.OnRender(dc);
if (Grid == null || MarkerCollection == null)
return;
//Get all items
object[] items = new object[Grid.Items.Count];
Grid.Items.CopyTo(items, 0);
//Get all selected items
object[] selection = new object[MarkerCollection.Count];
MarkerCollection.CopyTo(selection, 0);
Dictionary<object, int> indexes = new Dictionary<object, int>();
for (int i = 0; i < selection.Length; i++)
{
indexes.Add(selection[i], 0);
}
int itemCounter = 0;
for (int i = 0; i < items.Length; i++)
{
object item = items[i];
if (indexes.ContainsKey(item))
{
indexes[item] = i;
itemCounter++;
}
if (itemCounter >= selection.Length)
break;
}
double translateDelta = ActualHeight / (double)items.Length;
double width = ActualWidth;
double height = Math.Max(translateDelta, 4);
Brush dBrush = MarkerBrush;
double previous = 0;
IEnumerable<int> sortedIndex = indexes.Values.OrderBy(v => v);
foreach (int itemIndex in sortedIndex)
{
double top = itemIndex * translateDelta;
if (top < previous)
continue;
dc.DrawRectangle(dBrush, null, new Rect(0, top, width, height));
previous = (top + height) - 1;
}
}
}
This is my singleton class with SearchMarkers in it:
public class MyClass : INotifyPropertyChanged
{
public static ObservableCollection<object> m_searchMarkers = new ObservableCollection<object>();
public ObservableCollection<object> SearchMarkers
{
get
{
return m_searchMarkers;
}
set
{
m_searchMarkers = value;
NotifyPropertyChanged();
}
}
private static MyClass m_Instance;
public static MyClass Instance
{
get
{
if (m_Instance == null)
{
m_Instance = new MyClass();
}
return m_Instance;
}
}
private MyClass()
{
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
And this is a textbox text changed behavior. This is where the ObservableCollection SearchMarkers gets populated.
public class FindTextChangedBehavior : Behavior<TextBox>
{
protected override void OnAttached()
{
base.OnAttached();
AssociatedObject.TextChanged += OnTextChanged;
}
protected override void OnDetaching()
{
AssociatedObject.TextChanged -= OnTextChanged;
base.OnDetaching();
}
private void OnTextChanged(object sender, TextChangedEventArgs args)
{
var textBox = (sender as TextBox);
if (textBox != null)
{
DataGrid dg = DataGridObject as DataGrid;
string searchValue = textBox.Text;
if (dg.Items.Count > 0)
{
var columnBoundProperties = new List<KeyValuePair<int, string>>();
IEnumerable<DataGridColumn> visibleColumns = dg.Columns.Where(c => c.Visibility == System.Windows.Visibility.Visible);
foreach (var col in visibleColumns)
{
if (col is DataGridTextColumn)
{
var binding = (col as DataGridBoundColumn).Binding as Binding;
columnBoundProperties.Add(new KeyValuePair<int, string>(col.DisplayIndex, binding.Path.Path));
}
else if (col is DataGridComboBoxColumn)
{
DataGridComboBoxColumn dgcbc = (DataGridComboBoxColumn)col;
var binding = dgcbc.SelectedItemBinding as Binding;
columnBoundProperties.Add(new KeyValuePair<int, string>(col.DisplayIndex, binding.Path.Path));
}
}
Type itemType = dg.Items[0].GetType();
if (columnBoundProperties.Count > 0)
{
ObservableCollection<Object> tempItems = new ObservableCollection<Object>();
var itemsSource = dg.Items as IEnumerable;
Task.Factory.StartNew(() =>
{
ClassPropTextSearch.init(itemType, columnBoundProperties);
if (itemsSource != null)
{
foreach (object o in itemsSource)
{
if (ClassPropTextSearch.Match(o, searchValue))
{
tempItems.Add(o);
}
}
}
})
.ContinueWith(t =>
{
Application.Current.Dispatcher.Invoke(new Action(() => MyClass.Instance.SearchMarkers = tempItems));
});
}
}
}
}
public static readonly DependencyProperty DataGridObjectProperty =
DependencyProperty.RegisterAttached("DataGridObject", typeof(DataGrid), typeof(FindTextChangedBehavior), new UIPropertyMetadata(null));
public object DataGridObject
{
get { return (object)GetValue(DataGridObjectProperty); }
set { SetValue(DataGridObjectProperty, value); }
}
}
Here you go, I tried to attempt it for you.
Created a class MarkerCanvas deriving Canvas with a property to bind with the data grid
Attached SelectionChanged to listen to any change and requested the canvas to redraw itself by InvalidateVisual
overrided the method OnRender to take control of drawing and did the necessary check and calculation
finally rendered the rectangle on the calculated coordinates using the given brush
MarkerCanvas class
class MarkerCanvas : Canvas
{
public DataGrid Grid
{
get { return (DataGrid)GetValue(GridProperty); }
set { SetValue(GridProperty, value); }
}
// Using a DependencyProperty as the backing store for Grid. This enables animation, styling, binding, etc...
public static readonly DependencyProperty GridProperty =
DependencyProperty.Register("Grid", typeof(DataGrid), typeof(MarkerCanvas), new PropertyMetadata(null, OnGridChanged));
private static void OnGridChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
MarkerCanvas canvas = d as MarkerCanvas;
if (e.NewValue != null)
{
(e.NewValue as DataGrid).SelectionChanged += canvas.MarkerCanvas_SelectionChanged;
}
if (e.OldValue != null)
{
(e.NewValue as DataGrid).SelectionChanged -= canvas.MarkerCanvas_SelectionChanged;
}
}
void MarkerCanvas_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
InvalidateVisual();
}
public Brush MarkerBrush
{
get { return (Brush)GetValue(MarkerBrushProperty); }
set { SetValue(MarkerBrushProperty, value); }
}
// Using a DependencyProperty as the backing store for MarkerBrush. This enables animation, styling, binding, etc...
public static readonly DependencyProperty MarkerBrushProperty =
DependencyProperty.Register("MarkerBrush", typeof(Brush), typeof(MarkerCanvas), new PropertyMetadata(Brushes.SlateGray));
protected override void OnRender(System.Windows.Media.DrawingContext dc)
{
base.OnRender(dc);
if (Grid==null || Grid.SelectedItems == null)
return;
object[] markers = Grid.SelectedItems.OfType<object>().ToArray();
double translateDelta = ActualHeight / (double)Grid.Items.Count;
double width = ActualWidth;
double height = Math.Max(translateDelta, 4);
Brush dBrush = MarkerBrush;
for (int i = 0; i < markers.Length; i++)
{
double itemIndex = Grid.Items.IndexOf(markers[i]);
double top = itemIndex * translateDelta;
dc.DrawRectangle(dBrush, null, new Rect(0, top, width, height));
}
}
}
I have also adjusted the height of marker so it grows when there are less items, you can choose to fix it to specific value as per your needs
in XAML replace your items control with the new marker canvas with binding to the grid
<helpers:MarkerCanvas Grid.Row="1" Grid="{Binding RelativeSource={RelativeSource AncestorType={x:Type DataGrid}}}"/>
helpers: is referring to WpfAppDataGrid.Helpers where I create the class, you can choose your own namespace
also you can bind the MarkerBrush property for your desired effet, which defaulted to SlateGray
rendering is pretty fast now, perhaps could make it more fast by doing some work on indexof method.
Also to skip some of the overlapping rectangles to be rendered you can change the method like this. little buggy as of now
protected override void OnRender(System.Windows.Media.DrawingContext dc)
{
base.OnRender(dc);
if (Grid==null || Grid.SelectedItems == null)
return;
object[] markers = Grid.SelectedItems.OfType<object>().ToArray();
double translateDelta = ActualHeight / (double)Grid.Items.Count;
double width = ActualWidth;
double height = Math.Max(translateDelta, 4);
Brush dBrush = MarkerBrush;
double previous = 0;
for (int i = 0; i < markers.Length; i++)
{
double itemIndex = Grid.Items.IndexOf(markers[i]);
double top = itemIndex * translateDelta;
if (top < previous)
continue;
dc.DrawRectangle(dBrush, null, new Rect(0, top, width, height));
previous = (top + height) - 1;
}
}
Performance optimization
I tried to optimize the performance using a slight different approach, specially for the select all button
protected override void OnRender(System.Windows.Media.DrawingContext dc)
{
base.OnRender(dc);
if (Grid == null || Grid.SelectedItems == null)
return;
object[] items = new object[Grid.Items.Count];
Grid.Items.CopyTo(items, 0);
object[] selection = new object[Grid.SelectedItems.Count];
Grid.SelectedItems.CopyTo(selection, 0);
Dictionary<object, int> indexes = new Dictionary<object, int>();
for (int i = 0; i < selection.Length; i++)
{
indexes.Add(selection[i], 0);
}
int itemCounter = 0;
for (int i = 0; i < items.Length; i++)
{
object item = items[i];
if (indexes.ContainsKey(item))
{
indexes[item] = i;
itemCounter++;
}
if (itemCounter >= selection.Length)
break;
}
double translateDelta = ActualHeight / (double)items.Length;
double width = ActualWidth;
double height = Math.Max(translateDelta, 4);
Brush dBrush = MarkerBrush;
double previous = 0;
IEnumerable<int> sortedIndex = indexes.Values.OrderBy(v => v);
foreach (int itemIndex in sortedIndex)
{
double top = itemIndex * translateDelta;
if (top < previous)
continue;
dc.DrawRectangle(dBrush, null, new Rect(0, top, width, height));
previous = (top + height) - 1;
}
}
Reflection approach
in this I have tried to get the underlying selection list and attempted to retrieve the selected indexes from the same, also added even more optimization when doing select all
protected override void OnRender(System.Windows.Media.DrawingContext dc)
{
base.OnRender(dc);
if (Grid == null || Grid.SelectedItems == null)
return;
List<int> indexes = new List<int>();
double translateDelta = ActualHeight / (double)Grid.Items.Count;
double height = Math.Max(translateDelta, 4);
int itemInOneRect = (int)Math.Floor(height / translateDelta);
itemInOneRect -= (int)(itemInOneRect * 0.2);
if (Grid.SelectedItems.Count == Grid.Items.Count)
{
for (int i = 0; i < Grid.Items.Count; i += itemInOneRect)
{
indexes.Add(i);
}
}
else
{
FieldInfo fi = Grid.GetType().GetField("_selectedItems", BindingFlags.NonPublic | BindingFlags.FlattenHierarchy | BindingFlags.Instance);
IEnumerable<object> internalSelectionList = fi.GetValue(Grid) as IEnumerable<object>;
PropertyInfo pi = null;
int lastIndex = int.MinValue;
foreach (var item in internalSelectionList)
{
if (pi == null)
{
pi = item.GetType().GetProperty("Index", BindingFlags.Instance | BindingFlags.NonPublic);
}
int newIndex = (int)pi.GetValue(item);
if (newIndex > (lastIndex + itemInOneRect))
{
indexes.Add(newIndex);
lastIndex = newIndex;
}
}
indexes.Sort();
}
double width = ActualWidth;
Brush dBrush = MarkerBrush;
foreach (int itemIndex in indexes)
{
double top = itemIndex * translateDelta;
dc.DrawRectangle(dBrush, null, new Rect(0, top, width, height));
}
}

Categories