I need a horizontally-organized ItemsControl
that constrains all of its items to the same width. The items I'm using are UserControl
s and builds an auto-sized TextBlock
showing an int value (contained in a dependency property) with a Border
around it. The trouble is that smaller values result in a narrower item, and I need all of the items to be the same. I've considered a few solutions, but I can't seem to make any work.
The first is by setting the ItemsPanel
template to a Grid
. This way I can use the code-behind to generate the number of columns required based on the data source and set all of the column widths to *
. The puzzle was figuring out how to set the grid column attached property for each item.
The second solution is by setting the ItemsPanel
template to a StackPanel
. This automatically arranges the items correctly, but I can't set the width of each item to that of the widest item.
The last solution is by setting the ItemsPanel
template to a UniformGrid
with code-behind to set the number of columns whenever the data source changes. This would automatically arrange the items correctly and provide uniform width. My problem here is that I can't get any items to show at all. I've tried manually adding buttons, and they show fine. My UserControl
won't appear. I've listed this solution below.
<Window x:Class="Learning_WPF.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:my="clr-namespace:Learning_WPF"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
Title="DateTape" Height="176" Width="500">
<Window.Resources>
<my:DateList x:Key="dateList" CollectionChanged="DateList_CollectionChanged" />
</Window.Resources>
<ItemsControl x:Name="itemsControl1" ItemsSource="{Binding Source={StaticResource dateList}, Path=/}" Grid.Row="1">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid x:Name="daysGrid" Rows="1" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</Window>
public partial class MainWindow : Window
{
UniformGrid daysGrid;
DateList dateList;
public MainWindow()
{
InitializeComponent();
daysGrid = (UniformGrid)itemsControl1.ItemsPanel.LoadContent();
dateList = (DateList)FindResource("dateList");
dateList.Fill(DateTime.Today, DateTime.Today.AddDays(10));
}
private void DateList_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
daysGrid.Columns = dateList.Count;
}
}
public class DateList : ObservableCollection<Date>
{
public void Fill(DateTime first, DateTime last)
{
// implementation fills the array with all of the days between first and last, inclusively
}
}
Maybe there's a better way to do what I want to achieve (perhaps a bit more graphically than with controls)...
I finally got it to work using a Grid
. I had to do some significant code-behind and the interface is noticeably sluggish due to the triple layout, but it works. Suggestions on improving this are welcome. When inserting this control into a window, change the StartDate
property from its default value BEFORE the EndDate
property; otherwise the VS will lock up.
XAML
<UserControl x:Class="MyProject.DateTape"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:my="clr-namespace:MyProject"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
mc:Ignorable="d"
d:DesignHeight="45" d:DesignWidth="188">
<UserControl.Resources>
<Thickness x:Key="bottomThickness" Bottom="1" Top="1" Left="0.5" Right="0.5" />
<Thickness x:Key="topThickness" Bottom="0" Top="1" Left="0.5" Right="0.5" />
<Style TargetType="Border">
<Setter Property="BorderBrush" Value="#FFBEBEBE"/>
</Style>
<Style TargetType="TextBlock">
<Setter Property="TextAlignment" Value="Center"/>
<Setter Property="Margin" Value="6,0"/>
</Style>
</UserControl.Resources>
<Grid x:Name="mainGrid"/>
</UserControl>
C#
public partial class DateTape : UserControl
{
public static readonly DependencyProperty StartDateProperty =
DependencyProperty.Register("StartDate", typeof(DateTime), typeof(DateTape),
new PropertyMetadata(new PropertyChangedCallback(OnDatesChanged)));
public static readonly DependencyProperty EndDateProperty =
DependencyProperty.Register("EndDate", typeof(DateTime), typeof(DateTape),
new PropertyMetadata(new PropertyChangedCallback(OnDatesChanged)));
[Category("Common"),
TypeConverter(typeof(DateConverter))]
public DateTime StartDate
{
get { return (DateTime)GetValue(StartDateProperty); }
set { SetValue(StartDateProperty, value); }
}
[Category("Common"),
TypeConverter(typeof(DateConverter))]
public DateTime EndDate
{
get { return (DateTime)GetValue(EndDateProperty); }
set { SetValue(EndDateProperty, value); }
}
public DateTape()
{
InitializeComponent();
}
private void Layout()
{
int dayCount = (EndDate - StartDate).Days;
double max = 0;
DateTime current = StartDate;
ColumnDefinition columnDefinition;
Border border;
TextBlock textBlock;
Binding binding;
Size infinity = new Size(double.PositiveInfinity, double.PositiveInfinity);
Thickness bottomThickness = (Thickness)Resources["bottomThickness"],
topThickness = (Thickness)Resources["topThickness"];
mainGrid.ColumnDefinitions.Clear();
for (int i = 0; i <= dayCount; i++, current += TimeSpan.FromDays(1))
{
mainGrid.ColumnDefinitions.Add(columnDefinition = new ColumnDefinition());
// Add a day
border = new Border();
textBlock = new TextBlock();
textBlock.Text = current.Day.ToString();
border.Child = textBlock;
binding = new Binding();
binding.Source = bottomThickness;
border.SetBinding(Border.BorderThicknessProperty, binding);
mainGrid.Children.Add(border);
border.Measure(infinity);
max = Math.Max(max, border.DesiredSize.Width);
Grid.SetRow(border, 2);
Grid.SetColumn(border, i);
}
foreach (ColumnDefinition cd in mainGrid.ColumnDefinitions)
{
cd.MinWidth = max;
}
}
private static void OnDatesChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
(d as DateTape).Layout();
}
Edit:
I found a way to do it without using multiple layout passes by querying the DesiredSize of each Border item as I add them to the Grid and keeping track of the maximum width.