Monday, November 9, 2009

Sizing Up WPF Container Tags and Other XAML Tips

One of the more irritating things so far in learning XAML has been the confusing array of control container tags.  These tags are meant to help your controls arrange themselves on the form and respond appropriately to changes in style or window size.  Of course you could just use the <Canvas> tag to lay out your controls the old fashioned way (specifying x and y coordinates), but why not let WPF do the layout for you?

When I started creating forms in XAML, I was trying to use as few container tags as possible (why be wasteful?).  If this is your mindset, let me just encourage you to get over it.  The container tags are your friends and to get them to work well, you need to get used to the idea of sprinkling them liberally throughout your forms.  Here are the container tags that I am aware of so far and some of the comments that I have on each.

<Grid>: Your New Best Friend
I wasn't really sure what to use this tag for in the beginning.  Probably because in Adobe Flex (which I am more familiar with) you can live a productive life without its counterpart (mostly due to the fact that in Flex, height and width can be specified in percentages of available space!).  I kept trying to get my controls to layout appropriately by just using a combination of <StackPanel> and <DockPanel> tags.  This was working fine, until I tried to re-size my window container.  That's when I went back and re-examined the <Grid> tag.Just for review, here is a bare bones <Grid> tag:

   <Grid>
     <Grid.RowDefinitions>
       <RowDefinition/>
       <RowDefinition/>
     </Grid.RowDefinitions>
     ...
   </Grid>


Now, the <Grid> tag will automatically size its rows (and columns) evenly, thus if the Grid is 200 pixels tall and you have two rows, each row will be 100 pixels tall.  Grids will then re-size accordingly when the Window container is re-sized (unless you specify a fixed Height/Width for the Grid itself).  If you want the row (or column) to match the size of the elements it contains the set Height="Auto".  This will force the specific row/column to adopt the minimum size necessary to display it contained controls.  The remaining rows/columns will split up the remaining space.  For example, I had a column of form controls in a form with a large, scrollable text box in the middle that I wanted to expand if the user stretched window vertically.  I accomplished this as follows:


  <Grid>
    <Grid.RowDefinitions>
      <RowDefinition Height="Auto"/>
      <RowDefinition/>
      <RowDefinition Height="Auto"/>
    </Grid.RowDefinitions>
    <StackPanel Grid.Row="0">
     ... SOME FIXED HEIGHT CONTROLS ...
    </StackPanel>
    <DockPanel Grid.Row="1" LastChildFill="True">
      <Label DockPanel.Dock="Top" Content="Stretchable:" Width="Auto" HorizontalAlignment="left"/>
      <TextBox/>
    </DockPanel>
    <GroupBox Grid.Row="2" Header="Contact">
     ... SOME FIXED HEIGHT CONTROLS ...
    </GroupBox>
  </Grid>

Bingo! Note that DockPanel has LastChildFill="True".  This forces the TextBox to fill the width of the container (see the next section). The middle Grid row re-sizes automatically to fit the remaining space and the top and bottom rows size themselves (due to the Height="Auto" attribute) to fit their content.  Now that I had figured this out I started using <Grid> all over the place.  Even nesting <Grid> tags inside of <Grid> tags. 


<DockPanel>: Two's Company
The main use that I have found for the <DockPanel> is a Label-Control combination.  Because this container has the "LastChildFill" attribute, you can pair a fixed width <Label> with another control (such as a <TextBox>) and the second control will expand to fill the available space.  The <DockPanel> default stacking behavior is horizontal, meaning that multiple controls line up from left to right like text.  If you want the <Label> to sit above your control (like the stretching <TextBox> in the <Grid> example above) then set the <Label> tag's DockPanel.Dock="Top" attribute.  Because I most often use the <DockPanel> tag in this manner, I usually set default styles for it and the <label> tag:
  <Style TargetType="{x:Type DockPanel}">
    <Setter Property="LastChildFill" Value="True"/>
  </Style>
  <Style TargetType="{x:Type Label}">
    <Setter Property="Width" Value="60"/>
    <Setter Property="FontWeight" Value="Bold"/>
  </Style>

Incidentally, the LastChildFill property refers to the last XAML child not it's visual position in the DockPanel.  For example, if you want a expanding TextBox above a row of buttons, then place the buttons in a StackPanel (Orientation="Horizontal") with DockPanel.Dock="Bottom" and as the last XAML element in the DockPanel place your TextBox. Since I'm on the topic, order is extremely important in the DockPanel layout.  Try re-ordering controls inside a DockPanel tag while keeping their Dock attribute fixed (i.e., Top, Bottom, Left or Right) and notice how the layouts differ.

<StackPanel>: No Surprises Here
I generally use <StackPanel> as a container for other containers.  Usually the child container of choice is a <DockPanel> which is used as a Label-Control pair as described above. 


<GroupBox>: Bold That Header, Darn It!
Call me crazy, but I prefer all my control labels to be bold.  The <GroupBox> control gave me a little trouble at first because I couldn't figure out how to set a style for this.  If you just try to set the "FontWeight" attribute then ALL of the text inside your <GroupBox> will be bold in addition to the header.  Fortunately, I was able to stumble onto the following code on the web posted by some generous and like-minded soul whose name escapes me.

  <Style TargetType="{x:Type GroupBox}">
    <Setter Property="HeaderTemplate">
      <Setter.Value>
        <DataTemplate>
          <TextBlock Text="{Binding}" FontWeight="Bold" HorizontalAlignment="Stretch"/>
        </DataTemplate>
      </Setter.Value>
    </Setter>
  </Style>

Binding Made Easy(er)
You may have discovered that Data Binding statements can be a little confusing and tricky.  This is mostly due to the bare bones intellisense that VS2008 offers for them.  In this regard I have found Microsoft Expression Blend 3 to be invaluable. Data Binding is accomplished through a slick user interface that writes the binding statement for you.  Nice. It's pretty spendy, but it does have a 60-day full featured trial that will allow you to get a good idea of what it can do as well as get a good handle on Data Binding expressions.

No comments:

Post a Comment