views:

49

answers:

3

I have a lot of XAML that is in a complete mess and it's usually due to one thing. GRID, Grid.Column and Grid.Row craziness!!

Now theres a few common ways you can make labelled lists in XAML:.

With a <Grid>

Advantages: Autosizing width of columns and rows
Disadvantages: No easy way to insert rows/columns either in VS or Blend [STILL!!], potentially messy XAML if you're not careful, tonnes of attached properties, margins need to be set on each control, grid is meant for everything and definitely not designed for this purpose!

Yes I know there are handy tools to insert rows and columns without having to manually renumber all the fields such as XAML Powertools (see video). But its still clumsy in my opinion (despite being life saver at the same time.)

<Grid Grid.Row="6" Grid.Column="1">

    <Grid.RowDefinitions>
        <RowDefinition Height="Auto" />
        <RowDefinition Height="Auto" />
    </Grid.RowDefinitions>

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

    <!-- Name -->
    <TextBlock Text="Name:" Grid.Row="0" Grid.Column="0" VerticalAlignment="Center" HorizontalAlignment="Right"/>
    <TextBox Name="txtName" Grid.Row="0" Grid.Column="1" Width="100" Margin="5,5,0,0"/>

    <!-- Address -->
    <TextBlock Text="Address:" Grid.Row="1" Grid.Column="0" VerticalAlignment="Center" HorizontalAlignment="Right"/>
    <TextBox Name="txtAddress" Grid.Row="1" Grid.Column="1" Width="100" Margin="5,5,0,0"/>

</Grid>

With nested <Stackpanel> controls

(This is what I always seem to end up using)

Advantages: Very easy to reorder items, relatively clean XAML
Disadvantages: Need to know the width of the label column and set it on each item, still need to mess with margins

<StackPanel>

    <!-- Name -->
    <StackPanel Orientation="Horizontal">
        <TextBlock Text="Name:" Width="60" VerticalAlignment="Center" TextAlignment="Right"/>
        <TextBox Name="txtName" Width="100" Margin="5,5,0,0"/>
    </StackPanel>

    <!-- Address -->
    <StackPanel Orientation="Horizontal">
        <TextBlock Text="Address:" Width="60" VerticalAlignment="Center" TextAlignment="Right"/>
        <TextBox Name="txtAddress" Grid.Row="1" Grid.Column="1" Width="100" Margin="5,5,0,0"/>
    </StackPanel>

</StackPanel>

With a DataForm

Advantages: Handles editing, different views for different edit states etc.
Disadvantages: Still in preview quality band, too heavy for most of the time you need a simple list of items

            <dataform:DataForm x:Name="dataForm" Width="350" ItemsSource="{Binding}" HorizontalAlignment="Left" MaxWidth="500" Margin="4" Grid.Column="1"/>

See http://www.silverlightshow.net/items/Creating-Rich-Data-Forms-in-Silverlight-3-Customization.aspx


What I want!

I'd like some kind of grid that still works in Blend but allows me to set the text of the label easily with an attached property - or whatever the 'best' XAML way is. Allowing for multiple columns would be great but probably a lot more complicated.

Advantages: Easy to reorder items, clean XAML, set 'cellpadding' in one place
Disadvantages: None yet!

<LabelledGrid CellPadding="5" LabelStyle="{StaticResource labelStyle}>

   <TextBox LabelledGrid.Label="Name" Name="txtName" />
   <TextBox LabelledGrid.Label="Address" Name="txtAddress" />

</LabelledGrid>

or maybe

    <LabelledGrid CellPadding="5" LabelStyle="{StaticResource labelStyle}>

    <LabelledGrid.GridItem>
        <LabelledGrid.Label>
            Name:
        </LabelledGrid.Label>
        <TextBox Name="txtName" />
    </LabelledGrid.GridItem>

    <LabelledGrid.GridItem>
        <LabelledGrid.Label>
            Address:
        </LabelledGrid.Label>
        <TextBox Name="txtAddress" />
    </LabelledGrid.GridItem>

</LabelledGrid>

Are there any existing controls like how I'm describing LabelledGrid - or how should I go about creating this?

+1  A: 

You could do something as simple as changing the TextBox template and setting Tag as your label.

<ControlTemplate TargetType="{x:Type TextBox}" x:Key="LabeledTextBox">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="100"/>
            <ColumnDefinition/>
        </Grid.ColumnDefinitions>

        <TextBlock Text="{TemplateBinding Tag}" VerticalAlignment="Center" HorizontalAlignment="Right" Margin="5,0"/>

        <Border x:Name="Bd" Grid.Column="1" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" SnapsToDevicePixels="true">
            <ScrollViewer x:Name="PART_ContentHost" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
        </Border>
    </Grid>
    <ControlTemplate.Triggers>
        <Trigger Property="IsEnabled" Value="false">
            <Setter Property="Background" TargetName="Bd" Value="{DynamicResource {x:Static SystemColors.ControlBrushKey}}"/>
            <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}"/>
        </Trigger>
    </ControlTemplate.Triggers>
</ControlTemplate>

Now you can just put your TextBoxes in a StackPanel:

<StackPanel>
    <TextBox Template="{StaticResource LabeledTextBox}" Tag="Name"/>
    <TextBox Template="{StaticResource LabeledTextBox}" Tag="Title"/>
    <TextBox Template="{StaticResource LabeledTextBox}" Tag="Description"/>
</StackPanel>

If you want a more configurable solution you can use a HeaderedContentControl with a similar template that uses the Header property instead of Tag and has a ContentPresenter in place of the PART_ContentHost ScrollViewer. Usage for this would then be more like what your GridItem example shows, where each item would declare a TextBox as its Content. You could also use DataTemplates and non-Text content as for the labels.

John Bowen
thanks john. i like the idea of using 'HeaderedContentControl' because symantically it makes sense. yes i was looking for something more generic, but i'm also looking for all possibilities here - to help both myself and others. i feel like i'm missing something but i dont know if what i'm ultimately looking for is some autonumbering extension of Grid or something completely different
Simon_Weaver
+2  A: 

Here's how I deal with it: I create a collection of Field objects in my view model, each of which exposes a Label and Value property. Then I do this:

<DataTemplate DataType="{x:Type local:Field}">
   <Grid>
      <Grid.ColumnDefinitions>
         <ColumnDefinition SharedSizeGroup="Label"/>
         <ColumnDefinition SharedSizeGroup="Value"/>
      </Grid.ColumnDefinitions>
      <Label Content="{Binding Label}"/>
      <TextBox Grid.Column="1" Text="{Binding Value, Mode=TwoWay}"/>
   </Grid>
</DataTemplate>

<ItemsControl Grid.IsSharedSizeScope="True" ItemsSource="{Binding FieldCollection}"/>

This takes issues of field ordering out of the XAML entirely; fields show up in the order you added them to the FieldCollection. This approach doesn't give you fine-grained control over the formatting of individual fields, and you can't maintain the labels in the XAML (not without doing something sneaky), but if that's not something you need, it really simplifies the problem.

Robert Rossney
+1 I dig this, you could even then make a collection of field collections where the parent collection type had a horrizontally arranged collection of downwardly drawn lists.
Jimmy Hoffa
+1  A: 

After reading Dr. WPF's "'I' is for Item Container" article, I figured I'd give this a try. It's still rough, but I think it fits the bill, and perhaps someone who knows more WPF than me could improve it.

In your Window.Resources or App.Resources, add the following:

<Style x:Key="NamedControls" TargetType="{x:Type ListBox}">
  <Setter Property="BorderThickness" Value="0"/>
  <Setter Property="Grid.IsSharedSizeScope" Value="True"/>
  <Setter Property="ItemContainerStyle">
    <Setter.Value>
      <Style TargetType="{x:Type ListBoxItem}">
        <Setter Property="Focusable" Value="False"/>
        <Setter Property="Template">
          <Setter.Value>
            <ControlTemplate TargetType="{x:Type ListBoxItem}">
              <Grid>
                <Grid.ColumnDefinitions>
                  <ColumnDefinition SharedSizeGroup="NamedControls" Width="Auto"/>
                  <ColumnDefinition/>
                </Grid.ColumnDefinitions>
                <Label Content="{Binding Tag}" Padding="5 3"/>
                <ContentPresenter Grid.Column="1"/>
              </Grid>
            </ControlTemplate>
          </Setter.Value>
        </Setter>
      </Style>
    </Setter.Value>
  </Setter>
</Style>

That's a depressing lot of muck, but you only need it once. Then, whenever you want a name/control group, just do this:

<ListBox Style="{StaticResource NamedControls}">
  <TextBox Tag="Name:"/>
  <TextBox Tag="Address:"/>
  <TextBox Tag="City:"/>
</ListBox>

It automatically shows the Tag to the left of each control, and lines the controls up even if the labels are different lengths.

Some notes:

  • Yes, it really should be ItemsControl instead of ListBox -- but if you put a <TextBox/> inside an ItemsControl, it becomes a child control of the ItemsControl, so this styling technique no longer works. I need it to be an element in the Items collection instead. ListBox doesn't have this problem; anything you put inside it is automatically added to Items after being wrapped in a ListBoxItem, which can then be styled appropriately.
  • There's a 1-pixel blank margin around the entire group of controls (probably because of something in the ListBox's template). I was able to turn off the ListBox's border and prevent it from getting focus, but I'm not sure what to do about that margin.
  • This really all needs to be wrapped up in its own ItemsControl descendant (which would fix all the problems from the ListBox template), but I don't know enough about custom controls and default styles to get that up and running. This would also let you define an attached property for the label, instead of using Tag.
Joe White
+1 for referencing an article that uses simpson characters in listboxes instead of product images!
Simon_Weaver