I'm working on a progress wizard. I defined it as a style based on the ItemsControl. I have an ItemTemplateSelector with two DataTemplates, one for the first item and one for the rest of the items. I have it working correctly except for one really small issue that is super tricky to fix. There is a gap between the first item and the second item.
This is what the control should look like: The gap occurs because I'm using a uniform grid and so all of the columns are sized the same, even though the first one has no line. Using a uniform grid is important though because I want everything on one row and I want the control to stretch to fill the available space as it grows. I've tried not using the uniform grid but I end up either having issues with the margins or with not filling the available space. how can I fix this gap?
Here's the code:
<Style x:Key="WizardProgressBar" TargetType="{x:Type ItemsControl}">
<Style.Resources>
<DataTemplate x:Key="FirstItem">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Ellipse Name="ellipse" HorizontalAlignment="Left" Height="32" Width="32" />
</Grid>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding Completed}" Value="False">
<Setter TargetName="ellipse" Property="Stroke" Value="{DynamicResource DisabledBrush}" />
</DataTrigger>
<DataTrigger Binding="{Binding InProgress}" Value="True">
<Setter TargetName="ellipse" Property="Stroke" Value="{DynamicResource PrimaryTextBrush}"/>
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
<DataTemplate x:Key="OtherItem">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Ellipse Name="ellipse" Grid.Column="1" HorizontalAlignment="Left" Height="32" Width="32" />
<Line Name="leftPath" Grid.Column="0" X1="0" Y1="16"
X2="{Binding ActualWidth, Mode=OneWay, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Grid}}}" Y2="16" />
</Grid>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding Completed}" Value="False">
<Setter TargetName="ellipse" Property="Stroke" Value="{DynamicResource DisabledBrush}" />
<Setter TargetName="leftPath" Property="Stroke" Value="{DynamicResource DisabledBrush}"/>
</DataTrigger>
<DataTrigger Binding="{Binding InProgress}" Value="True">
<Setter TargetName="ellipse" Property="Stroke" Value="{DynamicResource PrimaryTextBrush}"/>
<Setter TargetName="leftPath" Property="Stroke" Value="{DynamicResource PrimaryTextBrush}"/>
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</Style.Resources>
<Setter Property="ItemsPanel">
<Setter.Value>
<ItemsPanelTemplate>
<UniformGrid Rows="1"/>
</ItemsPanelTemplate>
</Setter.Value>
</Setter>
<Setter Property="ItemTemplateSelector">
<Setter.Value>
<wpf:ItemsDataTemplateSelector FirstItem="{StaticResource FirstItem}" OtherItem="{StaticResource OtherItem}" />
</Setter.Value>
</Setter>
</Style>
public class ItemsDataTemplateSelector : DataTemplateSelector
{
public DataTemplate FirstItem { get; set; }
public DataTemplate OtherItem { get; set; }
public override DataTemplate SelectTemplate(object item, DependencyObject container)
{
var itemsControl = ItemsControl.ItemsControlFromItemContainer(container);
var returnTemplate = (itemsControl.ItemContainerGenerator.IndexFromContainer(container) == 0) ? FirstItem : OtherItem;
return returnTemplate;
}
}
The main problem is that the first item in your setup actually must have a different width compared to the other items. This is impossible with an UniformGrid
.
I can suggest you the following solution.
The target configuration will look like this:
|··O––|––O––|––O––|––O··|
You will have a half-cell wide margin on the left and on the right (represented by the dots above). If you want, you can leverage them by specifying the margins of your control.
Also, we can simplify your data templates. Actually, we don't need separate templates for the first and the other items. See below.
There are three tricks we're going to use here:
ItemsControl.AlternationIndex
property to obtain the first element in the ItemsControl
Canvas
to allow the connecting lines to be drawn outside of the corresponding UniforGrid
's cellsIValueConverter
that will help us calculate the needed line positionNow, this is the style for your ItemsControl
:
<Style x:Key="WizardProgressBar" TargetType="{x:Type ItemsControl}">
<Style.Resources>
<local:LinearConverter x:Key="Multiplier" Scale="-0.5" Offset="16"/>
<DataTemplate DataType="{x:Type local:YourItemType}">
<Grid>
<Canvas>
<Rectangle x:Name="leftPath" Height="2" Stroke="Blue" Canvas.Top="16"
Canvas.Left="{Binding Width, RelativeSource={RelativeSource Self}, Converter={StaticResource Multiplier}}"
Width="{Binding ActualWidth, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ContentPresenter}}}"/>
</Canvas>
<Ellipse Name="ellipse" HorizontalAlignment="Center" Height="32" Width="32" Stroke="Blue"/>
</Grid>
<DataTemplate.Triggers>
<Trigger Property="ItemsControl.AlternationIndex" Value="0">
<Setter TargetName="leftPath" Property="Visibility" Value="Collapsed"/>
</Trigger>
<DataTrigger Binding="{Binding Completed}" Value="False">
<Setter TargetName="ellipse" Property="Stroke" Value="{DynamicResource DisabledBrush}" />
<Setter TargetName="leftPath" Property="Stroke" Value="{DynamicResource DisabledBrush}" />
</DataTrigger>
<DataTrigger Binding="{Binding InProgress}" Value="True">
<Setter TargetName="ellipse" Property="Stroke" Value="{DynamicResource PrimaryTextBrush}"/>
<Setter TargetName="leftPath" Property="Stroke" Value="{DynamicResource PrimaryTextBrush}" />
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</Style.Resources>
<Setter Property="ItemsPanel">
<Setter.Value>
<ItemsPanelTemplate>
<UniformGrid Rows="1"/>
</ItemsPanelTemplate>
</Setter.Value>
</Setter>
<Setter Property="AlternationCount" Value="100"/>
</Style>
Note the following changes:
DataTemplate
, no need for two different onesEllipse
and the line (as a Rectangle
) are placed in an 1x1 Grid
into the same cellCanvas
Ellipse
is centered horizontallyTrigger
that catches the AlternationIndex
value 0 and hides the line - it's for the first elementAlternationCount
to 100 supposing you will have 100 wizard pages at maximumHere is the converter:
class LinearConverter : IValueConverter
{
public double Scale { get; set; }
public double Offset { get; set; }
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
// TODO: exception handling
return System.Convert.ToDouble(value) * Scale + Offset;
}
// ConvertBack just throws a NotImplementedException
}
Each Ellipse
represents a wizard page and is placed in center of the corresponding UniformGrid
's cell. The lines are placed to the left of the ellipses. The lines' width is set to the width of the UnifiormGrid
's single cell, and their horizontal position in the Canvas
is set according to the formula: WidthOfEllipse / 2 - WidthOfCell / 2
. This ensures the correct placement.
For the first wizard page, the line will be hidden.
Note that you might want to use Fill
for the ellipses to hide the underlying lines.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With