05 August 2009

Putting multiple elements in a Silverlight 3 DataForm DataField while retaining auto Label and Description generation

Summary: if you want to use a DataForm, want to put multiple controls in one DataField, and still want Silverlight 3 to generate Label and Description from the attributes applied to the properties you bind, you need to
  • Name your DataField
  • Add an ItemsControl as wrapping element inside the DataField
  • Bind the property to the ItemsControl
  • Bind the property of your controls inside the ItemsControl to the ItemsControls' DataContext
Bear with me: I started out with the Mike Taulty video and made a nice little business class like this:
using System;
using System.ComponentModel.DataAnnotations;

namespace DataFormDemo
{
  public class Coordinate
  {   
    private double _xmin;
    public double XMin { get { return _xmin; } }

    private double _ymin;
    public double YMin { get { return _ymin; } }

    private double _xmax;
    public double XMax { get { return _xmax; } }

    private double _ymax;
    public double YMax { get { return _ymax; } }

    public Coordinate(double x, double y,
                      double xLow, double yLow,
                      double xHigh, double yHigh)
    {
      _x = x;
      _y = y;
      _xmax =  xHigh;
      _ymax =  yHigh;
      _xmin =  xLow;
      _ymin =  yLow;
    }

    private double _x;
    [Display(Name = "X Coordinate:",
      Description = "The X coordinate of the point")]
    public double X
    {
      get
      {
        return _x;
      }
      set
      {
        ValidateValue(value, XMin, XMax);
        _x = value;
      }
    }

    private double _y;
    [Display(Name = "Y Coordinate:",
      Description = "The Y coordinate of the point")]
    public double Y
    {
      get
      {
        return _y;
      }
      set
      {
        ValidateValue(value, YMin, YMax);
        _y = value;
      }
    }
   
    private void ValidateValue(double value, 
     double minValue, double maxValue)
    {
      if (value < minValue || value > maxValue)
      {
        throw new ArgumentException(
            string.Format("Valid value {0} - {1}", minValue,maxValue));
      }
    }
  }
}
and just like Mike did, I added some labels and descriptions (in red) to get a nicer looking form. The purpose of this class is that a user can enter an X and Y value that fall in a range, which is validated. All parameters are set by the constructor. Then I made some minimal XAML
<UserControl x:Class="DataFormDemo.MainPage"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
  xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
  xmlns:dataFormToolkit="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Data.DataForm.Toolkit"
  mc:Ignorable="d" d:DesignWidth="640" d:DesignHeight="480">
 <Grid x:Name="LayoutRoot">
    <dataFormToolkit:DataForm x:Name="MyForm"
    HorizontalAlignment="Center" VerticalAlignment="Top">
    </dataFormToolkit:DataForm>
  </Grid>
</UserControl>
Then I bound a Coordinate in the constructor
public MainPage()
{
  InitializeComponent();
  MyForm.CurrentItem = new Coordinate(
      150000, 450000, 
      0, 300000, 
      300000, 600000);
}
and got the screen left, with a wee bit more than I bargained for. I was planning to customize the default dataform layout anyway, so I changed the DataForm to:

 

 

 

 

<dataFormToolkit:DataForm x:Name="MyForm" 
  HorizontalAlignment="Center" VerticalAlignment="Top">
  <dataFormToolkit:DataForm.EditTemplate>
    <DataTemplate>
      <StackPanel>
        <dataFormToolkit:DataField >
          <TextBox Text="{Binding X,Mode=TwoWay}"/>
        </dataFormToolkit:DataField>
        <dataFormToolkit:DataField >
          <TextBox Text="{Binding Y,Mode=TwoWay}"/>
        </dataFormToolkit:DataField>
      </StackPanel>
    </DataTemplate>
  </dataFormToolkit:DataForm.EditTemplate>
</dataFormToolkit:DataForm>

That worked out nicely, but wanting to make this a little more slickey and user friendly I introduced a slider connected to the text box with the new Silverlight 3 component-to-component binding:

 

<dataFormToolkit:DataForm x:Name="MyForm" 
  HorizontalAlignment="Center" VerticalAlignment="Top">
  <dataFormToolkit:DataForm.EditTemplate>
    <DataTemplate>
      <StackPanel>
        <dataFormToolkit:DataField >
          <StackPanel>
            <TextBox x:Name="tbX" Text="{Binding X, Mode=TwoWay}"/>
            <Slider Maximum="{Binding XMax}" Minimum="{Binding XMin}"
              Value="{Binding Text, ElementName=tbX, Mode=TwoWay}"/>
          </StackPanel>
        </dataFormToolkit:DataField>
        <dataFormToolkit:DataField >
          <StackPanel>
            <TextBox x:Name="tbY" Text="{Binding Y, Mode=TwoWay}"/>
            <Slider Maximum="{Binding YMax}" Minimum="{Binding YMin}"
              Value="{Binding Text, ElementName=tbY, Mode=TwoWay}"/>
          </StackPanel>
        </dataFormToolkit:DataField>
      </StackPanel>
    </DataTemplate>
  </dataFormToolkit:DataForm.EditTemplate>
</dataFormToolkit:DataForm>
That works swell, but where are my label and my description? Turns out that the Silverlight 3 DataForm shows label and description if and only if there is one element inside that DataField to which the property is bound. You use a StackPanel or something, and the DataForm seems to loose track of where to put which label and which description, so it does not show them at all. After some experimenting around, I found the following work around using the ItemsControl:
<dataFormToolkit:DataForm x:Name="MyForm" 
  HorizontalAlignment="Center" VerticalAlignment="Top">
  <dataFormToolkit:DataForm.EditTemplate>
    <DataTemplate>
      <StackPanel>
        <dataFormToolkit:DataField>
          <ItemsControl x:Name="icX" DataContext="{Binding X,Mode=TwoWay}">
            <StackPanel>
              <TextBox x:Name="tbX" 
               Text="{Binding DataContext, ElementName=icX, Mode=TwoWay}"/>
              <Slider Maximum="{Binding XMax}" Minimum="{Binding XMin}"
               Value="{Binding Text, ElementName=tbX, Mode=TwoWay}"/>
            </StackPanel>
          </ItemsControl>
        </dataFormToolkit:DataField>
        <dataFormToolkit:DataField >
          <ItemsControl x:Name="icY" DataContext="{Binding Y,Mode=TwoWay}">
            <StackPanel>
              <TextBox x:Name="tbY" 
               Text="{Binding DataContext, ElementName=icY, Mode=TwoWay}"/>
              <Slider Maximum="{Binding YMax}" Minimum="{Binding YMin}"
                Value="{Binding Text, ElementName=tbY, Mode=TwoWay}"/>
            </StackPanel>
          </ItemsControl>
        </dataFormToolkit:DataField>
      </StackPanel>
    </DataTemplate>
  </dataFormToolkit:DataForm.EditTemplate>
</dataFormToolkit:DataForm>
But now my sliders do not work anymore: they are locked in the right position. This is logical, because the DataContext of anything inside the ItemsControl is now no longer the Coordinate object, but its X or Y property. So XMin, XMax, YMin and YMax are now unknown (and thus zero). This, too, can be solved by naming your DataFields, using the Path binding syntax and then once again using control-to-control binding:
<dataFormToolkit:DataForm x:Name="MyForm" 
  HorizontalAlignment="Center" VerticalAlignment="Top">
  <dataFormToolkit:DataForm.EditTemplate>
    <DataTemplate>
      <StackPanel>
        <dataFormToolkit:DataField x:Name="dfX">
          <ItemsControl x:Name="icX" DataContext="{Binding X,Mode=TwoWay}">
            <StackPanel>
              <TextBox x:Name="tbX" 
               Text="{Binding DataContext, ElementName=icX, Mode=TwoWay}"/>
              <Slider 
               Maximum="{Binding Path=DataContext.XMax,ElementName=dfX}" 
               Minimum="{Binding Path=DataContext.XMin,ElementName=dfX}"
               Value="{Binding Text, ElementName=tbX, Mode=TwoWay}"/>
            </StackPanel>
          </ItemsControl>
        </dataFormToolkit:DataField>
        <dataFormToolkit:DataField x:Name="dfY">
          <ItemsControl x:Name="icY" DataContext="{Binding Y,Mode=TwoWay}">
            <StackPanel>
              <TextBox x:Name="tbY" 
               Text="{Binding DataContext, ElementName=icY, Mode=TwoWay}"/>
             <Slider 
               Maximum="{Binding Path=DataContext.YMax,ElementName=dfY}"
               Minimum="{Binding Path=DataContext.YMin,ElementName=dfY}"
               Value="{Binding Text, ElementName=tbY, Mode=TwoWay}"/>
             </StackPanel>
          </ItemsControl>
        </dataFormToolkit:DataField>
      </StackPanel>
    </DataTemplate>
  </dataFormToolkit:DataForm.EditTemplate>
</dataFormToolkit:DataForm>
Which gives the resulting - and fully functional - screen to the right. A prime example of something that started out so simple, and then became just a teeny bit more complex ;-) So, using the ItemsControl and control-to-control binding makes the DataForm retain its ability to generate Labels and Descriptions automatically. Which shows that Silverlight 3 databinding is about as flexible as a rubber band. One note of caution: when binding Maximum, Minimum and Value to a slider, this must be done in exactly the right order, as shown here. I blogged about this earlier this week.

2 comments:

Unknown said...

Another great post there!

It makes me feel more and more that Silverlight is extremely powerful and flexible, but it is not easy to get some relatively simple things done.

It goes to show that a lot of the programming takes place in the XAML and less and less in the C# code. Which means, once more as a developer there's another language to learn.

Keep them coming. One day it will pay off.

Joost van Schaik said...

Rob, my most faithful fan :'-).