Share via


WPF: Property List Editing


**Introduction    **

A very common User Interface layout for editing data is a list of labelled fields/properties.  The label describes what the field is and a textbox ( combo, check box or datepicker et al ) is used to view and edit values.   

Sometimes there are multiple columns of these paired controls to fit on a window.   

Another variant has the label above the TextBox.  

Whichever variation, this pair of controls is a very common pattern. 
In other technologies there are ready rolled controls such as the Asp.Net : DetailsView
In WPF there is no real equivalent.  
The aim of this article is to explain how to easily build this style of UI.  

The gallery sample referred to is available here.
                                

The Usual WPF Approach        

The obvious way to arrange a set of controls like this in WPF is to use a Grid with two columns and each pair of controls goes in a row.

Something like:

                  <Grid> 
                      <Grid.ColumnDefinitions> 
                          <ColumnDefinition Width="2*" /> 
                          <ColumnDefinition Width="3*" /> 
                      </Grid.ColumnDefinitions> 
                      <Grid.RowDefinitions> 
                          <RowDefinition Height="*" /> 
                          <RowDefinition Height="24" /> 
                          <RowDefinition Height="24" /> 
                          <RowDefinition Height="24" /> 
                          <RowDefinition Height="24" /> 
                          <RowDefinition Height="24" /> 
                          <RowDefinition Height="24" /> 
                          <RowDefinition Height="24" /> 
                          <RowDefinition Height="24" /> 
                          <RowDefinition Height="3*" /> 
                          <RowDefinition Height="24" /> 
                          <RowDefinition Height="12" /> 
                      </Grid.RowDefinitions> 
 
                      <TextBlock Text="Name:" Grid.Row="1" /> 
                      <TextBlock Text="Address:" Grid.Row="2" /> 
                      <TextBlock Text="Town/City:" Grid.Row="5" /> 
                      <TextBlock Text="Post Code:" Grid.Row="6" /> 
                      <TextBlock Text="Credit Limit:" Grid.Row="7" /> 
                      <TextBlock Text="Outstanding Amount:" Grid.Row="8" /> 
                      <!-- -->
                      <TextBox Text="{Binding CustomerName,   Mode=TwoWay}"  Grid.Column="1" Grid.Row="1"/> 
 
                     <TextBox Text="{Binding Address1,  Mode=TwoWay}"  Grid.Column="1"  Grid.Row="2" /> 
                    <TextBox Text="{Binding  EditVM.TheEntity.Address2,  Mode=TwoWay}" Grid.Column="1" Grid.Row="3" /> 
                     <TextBox Text="{Binding  Address3, Mode=TwoWay}" Grid.Column="1"   Grid.Row="4" /> 
                     <TextBox Text="{Binding TownCity, Mode=TwoWay}"  Grid.Column="1"  Grid.Row="5"  /> 
                     <TextBox Text="{Binding  PostCode,   Mode=TwoWay}"  Grid.Column="1" Grid.Row="6" /> 
                           <TextBox  Text="{Binding  EditVM.TheEntity.CreditLimit, StringFormat={}{0:C0}, 
                                                     Mode=TwoWay}"  Grid.Column="1"  Grid.Row="7" />

This is fine for just a few properties but as the list grows it’s increasingly fiddly and error prone arranging everything in the right row.

What we could do with would be some way of wrapping any old editing sort of a control with a label whilst allowing you to standardise formatting of these so they all line up nicely.

Anyone who has read the previous article on Keeping your MVVM views DRY or who knows WPF well might by now realise a content control can be used to encapsulate such formatting.

Template the contentcontrol.  Put a Grid in there with two columns.

A TextBlock acts as the label in the first column, that editing control is accepted as the content in the other and that's one of these things encapsulated in a re-usable fashion.

That's one done, great. But we need. 

A List of Them

The part which is particularly tedious and error prone is defining and setting the rows.

What we want is some sort of a list of these content controls so we don’t need to worry about which index for what, how many rows you have, and such "fun" as ending up with the wrong label on a TextBox so the user is editing the wrong property.

WPF allows very flexible formatting and one option is to present a list of Controls in an ItemsControl or ListBox.

The sample uses a ListBox and looks like this:

OK, not the most interesting choice of field names but this is after all just a sample.

Show Me the Code

Both ListBox and ItemsControl have their own set of advantages so you could make a case for either.  The Listbox chosen for the sample makes any scrollviewer easier than an ItemsControl.  The downside is the mouseover and selection effects which you get as a "bonus".  Unless you like them, some extra markup is required to suppress these.

MainWindow

There's not much markup in this:

<Grid>
    <Grid.Resources>
        <!-- The ListboxItem template has a blue mouseover effect, replace the template to avoid that -->
        <Style TargetType="{x:Type ListBoxItem}">
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type ListBoxItem}">
                        <Grid Background="{TemplateBinding Background}">
                            <ContentPresenter 
                                    ContentTemplate="{TemplateBinding ContentTemplate}"
                                    Content="{TemplateBinding Content}"
                                    HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
                                    Margin="{TemplateBinding Padding}">
                            </ContentPresenter>
                        </Grid>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
        <Style TargetType="DatePicker">
            <Setter Property="Margin" Value="0,0,0,4"/>
        </Style>
    </Grid.Resources>
 
    <ListBox HorizontalContentAlignment="Stretch" Background="AliceBlue">
        <ListBox.Resources>
            <Style TargetType="{x:Type TextBox}">
                <Setter Property="HorizontalAlignment" Value="Stretch"/>
            </Style>
        </ListBox.Resources>
        <!--<local:EditRow LabelFor="Label for property One:"  LabelWidth="120" PropertyWidth="200">
            <TextBox Text="aaaa"/>
        </local:EditRow>-->
        <local:EditRow LabelFor="Label for property One:" >
            <TextBox Text="aaaa"/>
        </local:EditRow>
        <local:EditRow  LabelFor="Label for Property Two:">
            <TextBox Text="bbbb"/>
        </local:EditRow>
        <local:EditRow  LabelFor="Label for Property Three:">
            <DatePicker/>
        </local:EditRow>
        <local:EditRow  LabelFor="Label for Property Four:">
            <TextBox Text="dddddd"/>
        </local:EditRow>
        <local:EditRow  LabelFor="Label for Property Five:">
            <TextBox Text="eeee"/>
        </local:EditRow>
        <local:EditRow  LabelFor="Label for Property Six:">
            <TextBox Text="fffffff"/>
        </local:EditRow>
        <local:EditRow  LabelFor="Label for Property Seven:">
            <TextBox Text="ggggg"/>
        </local:EditRow>
        <local:EditRow  LabelFor="Label for Property Eight:">
            <TextBox Text="hhhhhhhhhhh"/>
        </local:EditRow>
    </ListBox>
</Grid>

As mentioned earlier, the listbox item is re-templated purely to avoid the blue MouseOver background on a selected item.

Comment that out and see if you prefer  much more obvious ListBox behaviour.

If you do so and set the height of the Window to 150 as well you can see another aspect of using a ListBox.

The blue background will appeal to some.  Similarly you might prefer to try and line up the label text with the text in the textboxes.

The ListBox gives us a vertical slider for zero effort which is handy if the list of properties may not fit in the available space.  You don't even need worry about how many properties you throw in there.

Back to the details of MainWindow. 

Since this is ( obviously ) only some generated test data rather than a database or some such, there is no viewmodel or model.

Let's move on to the Content Control.

EditRow

This inherits from ContentControl for the reasons touched on earlier.  

public class  EditRow : ContentControl
{
    public string  LabelFor
    {
        get { return (string)GetValue(LabelForProperty); }
        set { SetValue(LabelForProperty, value); }
    }
    public static  readonly DependencyProperty LabelForProperty = DependencyProperty.RegisterAttached(
                      "LabelFor",
                      typeof(string),
                      typeof(EditRow));
    public string  LabelWidth
    {
        get { return (string)GetValue(LabelWidthProperty); }
        set { SetValue(LabelWidthProperty, value); }
    }
    public static  readonly DependencyProperty LabelWidthProperty = DependencyProperty.RegisterAttached(
                      "LabelWidth",
                      typeof(string),
                      typeof(EditRow)
                      );
    public string  PropertyWidth
    {
        get { return (string)GetValue(PropertyWidthProperty); }
        set { SetValue(PropertyWidthProperty, value); }
    }
    public static  readonly DependencyProperty PropertyWidthProperty = DependencyProperty.RegisterAttached(
                      "PropertyWidth",
                      typeof(string),
                      typeof(EditRow)
                     );
    public EditRow()
    {
        this.IsTabStop = false;
    }
}

The code there exposes three dependency properties.

LabelFor is the string which will be displayed as the label.

LabelWidth and PropertyWidth are strings and you can set them like regular measures to proportional such as "2*" or fixed like "200".  These are given default values so you only need to set these in exceptional circumstances - they're not set in MainWindow.

You can experiment with fixed widths easily by removing the angle comments to make this piece of code active:

<local:EditRow LabelFor="Label for property One:"  LabelWidth="120" PropertyWidth="200">
         <TextBox Text="aaaa"/>
</local:EditRow>

Try that then play around a bit with * and 3* etc to prove these are definitely being used if you set them.

If you thought about it, you might be wondering how EditRow gets any sort of format at all.

That's in:

Dictionary1

There's a generic style ( no key ) which will therefore apply to all EditRows.  This resource dictionary is merged in App.xaml.

<Style TargetType="{x:Type local:EditRow}">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type local:EditRow}">
                <Grid>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="{Binding RelativeSource={
                                              RelativeSource FindAncestor,
                                              AncestorType=local:EditRow}, 
                                              Path=LabelWidth, TargetNullValue=2*}"/>
             
                    <ColumnDefinition Width="{Binding RelativeSource={
                                              RelativeSource FindAncestor,
                                              AncestorType=local:EditRow}, 
                                              Path=PropertyWidth, TargetNullValue=3*}"/>
          
                </Grid.ColumnDefinitions>
                    <TextBlock Text="{Binding RelativeSource={
                                              RelativeSource FindAncestor,
                                              AncestorType=local:EditRow}, 
                                              Path=LabelFor}"
                                       HorizontalAlignment="Right"
                                       VerticalAlignment="Center"
                                       Margin="0,4,8,6"
                                       />
                    <Border Padding="6,4,6,0" Grid.Column="1">
                        <ContentPresenter HorizontalAlignment="Stretch"/>
                    </Border>
                </Grid>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

As described in  Keeping your MVVM views DRY the template is used on a ContentControl to give us the Grid containing a Label and whatever is put in the ContentControl as it's content when it is used. That will often be a TextBox for the user to edit a property.

RelativeSource bindings are used to get the values of those three dependency properties in the control the template is applied to - EditRow.  

If either width is not set then they will be null and the default values come from TargetNullValue.  It is these default values the sample uses for all the EditRows.

Conclusion

Laying out a list of labelled properties to edit is rather tiresome if you go with the conventional Grid based approach.

This is far from being a DRY approach with a lot of repetitive and error prone markup.

Using a list of a custom content control makes things rather more straightforward.

Leaving the developer free to decide exactly which format they prefer for each item rather than worry about which index goes where for what.

 

Other Alternatives

Creating Custom Panels in WPF 

FishEyePanel/FanPanel - Examples of custom layout panels in WPF 

Workflow Foundation's property grid

Extended Toolkit Property grid