Search code examples
wpfxamlwindows-phone-7windows-phone-8

Align Buttons Horizontally and Spread Equally with Xaml


I have an ObservableCollection which contains ViewModel which in turns defines my buttons definitions.

I've been at it for hours, reading articles after articles but to no avail. I've tried using a Listbox and this is the closest I've got to. My buttons are getting build horizontally but assuming I'm displaying 3 buttons, I want one displayed on the left, one displayed in the middle and one displayed on the right.

<ListBox Grid.Row="2" ItemsSource="{Binding Links}">
    <ListBox.ItemsPanel>
        <ItemsPanelTemplate ScrollViewer.HorizontalScrollBarVisibility="Disabled">
            <StackPanel Background="Beige" Orientation="Horizontal"/>
        </ItemsPanelTemplate>
    </ListBox.ItemsPanel>
    <ListBox.ItemTemplate>
        <DataTemplate>
            <Button Grid.Column="{Binding Column}" 
                    Grid.Row="0" 
                    Width="90" 
                    Height="90">
                <ContentControl>
                    <StackPanel Orientation="Vertical">
                        <Image Source="{Binding Image}" Width="36" Height="36"
                               Margin="5" Stretch="UniformToFill" 
                               HorizontalAlignment="Center"/>
                        <TextBlock Text="{Binding Description}" 
                                   Foreground="#0F558E" 
                                   FontSize="18" 
                                   HorizontalAlignment="Center"/>
                    </StackPanel>
                </ContentControl>
            </Button>
        </DataTemplate>
    </ListBox.ItemTemplate>
    <ListBox.ItemContainerStyle>
        <Style TargetType="ListBoxItem">
            <Setter Property="HorizontalContentAlignment" Value="Stretch"/>
        </Style>
    </ListBox.ItemContainerStyle>
</ListBox>

As you can see, I set the column dynamically using a property from my ViewModel but I no longer have a grid as I couldn't get it to work, but ideally I'd like to use a grid so that I can specify in which Column to set the button.

When I use a StackPanel, it works but the buttons are right next to each other rather than being split evenly through the entire width of the screen.

I've done something similar to the above using ItemsControl and using a grid, and I can see each button getting added but they overlap each other in row 0, col 0. If I bind the row to the Column property for testing purposes, it build it correctly but my buttons are displayed on different rows which is not what I want. I want each button to be aligned horizontally.

How can I achieve this? I know I could just hard code 3 buttons and just change their icons and text and handle the relevant code by passing the relevant button as binded parameter, but ideally I'd like to build the buttons dynamically in a grid and position them using the column.

Note that the number of column would be fixed i.e. 3, as I'll never have more than this.

Thanks.


Solution

  • I eventually found the answer to my problem in an article I found on the web.

    You can check it out here: Using Grids with ItemsControl in XAML

    In short, you need to subclass the itemsControl and you need overwrite the GetContainerForItemOverride method which will take care of copying the "data" of the ItemTemplate to the ContentPresenter. In this instance, the row and column, but for my requirement, it is just the Column, since my row will always be 0.

    Here is core part of the code if you don't want to check the full article which resolve the problem of setting controls horizontally in a grid using ItemsControl but note the article takes care of creating rows & columns dynamically as well, which I'm not interested in for my project.

    public class GridAwareItemsControl : ItemsControl
    {
        protected override DependencyObject GetContainerForItemOverride()
        {
            ContentPresenter container = (ContentPresenter)base.GetContainerForItemOverride();
            if (ItemTemplate == null)
            {
                return container;
            }
    
            FrameworkElement content = (FrameworkElement)ItemTemplate.LoadContent();
            BindingExpression rowBinding = content.GetBindingExpression(Grid.RowProperty);
            BindingExpression columnBinding = content.GetBindingExpression(Grid.ColumnProperty);
    
            if (rowBinding != null)
            {
                container.SetBinding(Grid.RowProperty, rowBinding.ParentBinding);
            }
    
            if (columnBinding != null)
            {
                container.SetBinding(Grid.ColumnProperty, columnBinding.ParentBinding);
            }
    
            return container;
        }
    }
    

    The final xaml looks like this:

    <controls:GridAwareItemsControl ItemsSource="{Binding Links}">
        <ItemsControl.ItemsPanel>
            <ItemsPanelTemplate>
                <Grid Background="Pink">
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="*"/>
                        <ColumnDefinition Width="Auto"/>
                        <ColumnDefinition Width="*"/>
                    </Grid.ColumnDefinitions>
                </Grid>
           </ItemsPanelTemplate>
        </ItemsControl.ItemsPanel>
        <ItemsControl.ItemTemplate>
            <DataTemplate>
                <Button
                     Grid.Column="{Binding Column}" 
                     Grid.Row="0" 
                     Width="120" Height="120">
                     <ContentControl>
                         <StackPanel Orientation="Vertical">
                             <Image Source="{Binding Image}" Width="36" Height="36" Margin="5"
                                    Stretch="UniformToFill" HorizontalAlignment="Center"/>
                             <TextBlock Text="{Binding Description}" Foreground="#0F558E" 
                              FontSize="18" HorizontalAlignment="Center"/>
                         </StackPanel>
                     </ContentControl>
                </Button>
            </DataTemplate>
        </ItemsControl.ItemTemplate>
    </controls:GridAwareItemsControl>
    

    Once I used the new control, my buttons were correctly placed inside the grid, and therefore were evenly spaced out as the grid took care of that wit the ColumnDefinitions.

    If anyone knows how to achieve the same without having to create a new control and overriding the method (in other words, pure XAML), please post it as I'd be very interested to see how this can be done.

    Thanks and thank you to Robert Garfoot for sharing this great code!

    PS: Note that I've simplified my xaml in order to create a test sample without any style on the buttons, so these are rather large if you try based on this sample.

    UPDATE:

    Small typo correction but my grid column definition was defined as

     <Grid.ColumnDefinitions>
         <ColumnDefinition Width="*"/>
         <ColumnDefinition Width="Auto"/>
         <ColumnDefinition Width="*"/>
     </Grid.ColumnDefinitions>
    

    but as @OmegaMan suggested, to be evenly spread, it should have been defined as

     <Grid.ColumnDefinitions>
         <ColumnDefinition Width="Auto"/>
         <ColumnDefinition Width="*"/>
         <ColumnDefinition Width="Auto"/>
     </Grid.ColumnDefinitions>