Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Uniform Grid as Panel Template for All Items in ItemsControl Except the First

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. enter image description here This is what the control should look like: enter image description here 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;
        }
    }
like image 404
user2481095 Avatar asked Nov 17 '22 00:11

user2481095


1 Answers

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.

Setup

There are three tricks we're going to use here:

  • the ItemsControl.AlternationIndex property to obtain the first element in the ItemsControl
  • a Canvas to allow the connecting lines to be drawn outside of the corresponding UniforGrid's cells
  • a special IValueConverter that will help us calculate the needed line position

Now, 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:

  • there is only one DataTemplate, no need for two different ones
  • no need for template selector anymore
  • we need a special converter in our style (code below)
  • the Ellipse and the line (as a Rectangle) are placed in an 1x1 Grid into the same cell
  • the line itself sits in a Canvas
  • the Ellipse is centered horizontally
  • there is an additional Trigger that catches the AlternationIndex value 0 and hides the line - it's for the first element
  • the style sets the AlternationCount to 100 supposing you will have 100 wizard pages at maximum

Here 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
}

Explanation

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.

like image 134
dymanoid Avatar answered Dec 21 '22 19:12

dymanoid