5

I am looking for a less cumbersome way of laying out Key / Value pairs (e.g. Label saying "First Name" followed by a Label with the First Name) on a screen. If it were just a simple grouping I would throw it in a Grid and be done. However the layout 2 or 3 pairs, followed by some type of grouping container with 4 or 5 more pairs, followed by a different grouping container etc.

It feels very cumbersome to have to do something like:

<Grid>
     <Grid.ColumnDefinitions>
         <ColumnDefinition Width=".2*" />
         <ColumnDefinition Width="*" />
      </Grid.ColumnDefinitions>
      <Grid.RowDefinitions>
         <RowDefinition />
         <RowDefinition />
      </Grid.RowDefinitions>

                <Label Grid.Column="0" Grid.Row="0" Content="Element ID:" HorizontalAlignment="Right" />
                <Label Grid.Column="1" Grid.Row="0" Content="{Binding Path=ElementId}" HorizontalAlignment="Left" FontWeight="Bold" />

                <Label Grid.Column="0" Grid.Row="1" Content="Element Description:" HorizontalAlignment="Right" />
                <Label Grid.Column="1" Grid.Row="1" Content="{Binding Path=ElementDescription}" HorizontalAlignment="Left" FontWeight="Bold" />                    
</Grid>

Just to render out:

_______Element ID: ABC123

Element Description: Spiffy!

This is especially true when you have several sections like this.

UniformGrid won't let you fix one column so that the Label can be right aligned. I tried a combination of StackPanel and WrapPanel but you end up with almost as much overhead and a lot of margin fiddling.

Is there a better way to do this?

jrandomuser
  • 1,510
  • 19
  • 50

4 Answers4

4

As far as I know you should be able to do something like this which is pretty handy when you have tons of them;

<UniformGrid Columns="2">

  <UniformGrid.Resources>

    <!-- Set your properties once, in one place, 
         and control your children like a good parent. -->

    <Style TargetType="Label">
      <Setter Property="HorizontalAlignment" Value="Right"/>
    </Style>
    <Style TargetType="TextBlock">
      <Setter Property="FontWeight" Value="Bold"/>
      <!-- If you want to control your cell width so the descriptions doesn't
           offset the size of the ID's you could just enable/edit these setters.
      <Setter Property="TextWrapping" Value="Wrap"/>
      <Setter Property="MaxWidth" Value="150"/>
      -->
    </Style>

  </UniformGrid.Resources>

  <Label Content="Element ID:"/>
  <TextBlock Text="{Binding Path=ElementId}"/>

  <Label Content="Element Description:"/>
  <TextBlock Text="{Binding Path=ElementDescription}"/>

  <!-- etc, etc, etc. -->

</UniformGrid>

PS - Avoid Label and use TextBlock instead unless it's actually beneficial as Label is heavier than TextBlock.

Hope this helps, cheers.

Chris W.
  • 22,835
  • 3
  • 60
  • 94
  • 1
    The only problem with using a `UniformGrid` is the two "columns" need to be the exact same width. But I agree with you on the use of implicit styles and using `TextBlock` over `Label` :) – Rachel Aug 11 '14 at 21:51
  • @Rachel Eh no worries, since you can text wrap either (in this case I would assume the description part which is a `TextBlock`) he could control that pretty easy with another Setter on there to set the MaxWidth and the opposite side cells would reflect it. Thank you though, and btw, your posts have helped me before too, cheers ;) – Chris W. Aug 12 '14 at 01:42
  • Nice idea! The `UniformGrid` also works as the `ItemsPanel` in an `ItemsControl` with a bound stream of cells, as in my answer. You'd have to bind to cells instead of rows; I wasn't able to do "header" rows because `UniformGrid` [has no `ColumnSpan` attached property](http://stackoverflow.com/a/10603726/1174169); and all the cell heights and widths are the same, as @Rachel mentioned (so when I tried to make the header row have a margin, all the cells grew. I imagine the same would happen when the text wraps in a cell?). – cod3monk3y Aug 12 '14 at 14:01
  • 1
    Also +1 to all who support implicit styles (and `DataTemplate`s) and `TextBlock` over `Label`; the former having transformed my application considerably upon discovery of its powers. – cod3monk3y Aug 12 '14 at 14:06
  • @cod3monk3y ah ya good point, I didn't think about Headers or anything in this example. I couldn't say by any means it's the best of the best routes this way but it's the type of way I've found handy in the past. Also yes the cell heights would grow in lieu of the wrapping unless that was addressed specifically too. Though I'd hate to set a Grid for each pair as a template and reiterate it over and over in the tree as an items template. It's one of those things where I'm sure there's probably other good ideas we could all benefit from. :) – Chris W. Aug 12 '14 at 14:46
  • @ChrisW. I really like the simplicity of your answer. I wonder if something like an `ItemsControl` with a `WrapPanel` with cells as `ItemsSource` (as in my first solution) with the data template binding the Width to "KEY" or "VALUE" would work... and the header cells would have width specified to KEY_WIDTH+VALUE_WIDTH. Don't have time to mock this right now, but may take a swing at it tonight. – cod3monk3y Aug 12 '14 at 18:25
  • @cod3monk3y Ya it's one of those instances you'd think folks like us would have taken a more in-depth stab at this sort of issue right now. There's also other ways it could be done I suppose using just other panels and their contentalignments instead of this way, I'm sure it will come up again either way, may be an opportunity for a codeplex proj or something in the future. – Chris W. Aug 12 '14 at 19:25
3

You could use Autogrid for xaml that looks like this:

<AutoGrid RowCount="2" RowHeight="35" Columns="100,auto">
  <Label />
  <TextBox />
  <Label />
  <TextBox />
</AutoGrid>
Wouter
  • 2,170
  • 1
  • 28
  • 58
  • 1
    I was pleasantly surprised to click on the link for `AutoGrid` and see a note from the author saying it was partially based off one of my blog posts! Glad they're useful to someone somewhere :) – Rachel Aug 11 '14 at 21:52
  • I too was very pleased with this solution. The AutoGrid achieved exactly what I wanted with a very simple and intuitive implementation. This will be one of my "GoTo" solutions for quick and efficient layouts from now on. – jrandomuser Sep 03 '14 at 13:11
1

You can create a shared size group which allows multiple columns in separate Grids to have the same size. Then you can use an ItemsControl with the Grid.IsSharedSizeScope attached property set to true to display your items. It may be a little heavy handed because it creates a new Grid for every row, but here's the result I was able to get (full code here):

enter image description here

In my view-model, I created a base class Row and two sub-classes DividerRow and KeyValueRow. The main view-model has an ObservableCollection<Row> of rows, populated like so:

public class ViewModel : BaseVM
{
    ObservableCollection<Row> _rows = new ObservableCollection<Row> ();
    public ObservableCollection<Row> Rows { get { return _rows; } }

    public ViewModel()
    {
        _rows.Add (new DividerRow () { Title = "Fruit" });
        _rows.Add (new KeyValueRow () { Key = "Apple", Value = "$1.01" });
        _rows.Add (new KeyValueRow () { Key = "Apricot", Value = "$2.01" });
        _rows.Add (new KeyValueRow () { Key = "Pineapple", Value = "$3.01" });

        _rows.Add (new DividerRow () { Title = "Meat" });
        _rows.Add (new KeyValueRow () { Key = "Bacon", Value = "$4.01" });
        _rows.Add (new KeyValueRow () { Key = "Ground-Turkey", Value = "$5.01" });
        _rows.Add (new KeyValueRow () { Key = "Sausage", Value = "$6.01" });
        _rows.Add (new KeyValueRow () { Key = "Andmorereallytastystuff", Value = "$7.01" });
    }
}

The XAML defines an ItemsControl bound to the Rows and defines itself as a shared size scope:

<Canvas>
    <ItemsControl ItemsSource="{Binding Rows}" Margin="50" Grid.IsSharedSizeScope="True" />
</Canvas>

And each individual row is defined using the nameless, type-based DataTemplates in the resource dictionary for the window. The divider row isn't very interseting. It's just a TextBlock:

<DataTemplate DataType="{x:Type l:DividerRow}">
    <TextBlock HorizontalAlignment="Stretch" 
               TextAlignment="Center" 
               FontWeight="Bold"  
               Padding="5,5,5,0" 
               Margin="0,10,0,0" 
               Background="LightBlue" 
               Text="{Binding Title}" />
</DataTemplate>

The interesting part that gets the columns to be the same width is in the template for the row, which defines the columns as SharedSizeGroups A and B.

<DataTemplate DataType="{x:Type l:KeyValueRow}">
    <Grid >
        <Grid.ColumnDefinitions>
            <ColumnDefinition SharedSizeGroup="A" Width="Auto" />
            <ColumnDefinition SharedSizeGroup="B" Width="Auto" />
        </Grid.ColumnDefinitions>

        <TextBlock Grid.Column="0" 
                   HorizontalAlignment="Right" 
                   Text="{Binding Key}" />
        <TextBlock Grid.Column="1" 
                   Margin="5,0,5,0" 
                   FontWeight="Bold" 
                   Text="{Binding Value}" />
    </Grid>
</DataTemplate>

Note that without the Grid.IsSharedSizeScope in the ItemsControl, this won't wont work properly. Hope that helps! Sorry for the formatting verbosity, but I wanted the XAML to match the sample image provided.

Community
  • 1
  • 1
cod3monk3y
  • 9,508
  • 6
  • 39
  • 54
0

Create a style for each label type which defines the alignment and font weight. That will reduce down the repetition and give you a single point to manage the look and feel.

Alternatively, expose them as a dictionary in your view model and use an items control with a template to display them.

kidshaw
  • 3,423
  • 2
  • 16
  • 28