Tuesday, July 06, 2010

Silverlight 4.0 Tutorial (3 of N): Working with the DataForm Control

Read Part 1

Read Part 2

Continuing our RegistrationBooth Application, in the previous post we ended having a data grid that displays all the attendees, and a user control to add a new attendee.

The user control for adding a new attendee didn’t contain any validation logic, of course this is a bad practice, in this post we will add the necessary validation when creating adding new attendees.

- First we will need to define our validation logic for the Attendee entity, if you remember our first post when we created the Domain Service class we choose to create metadata classes for our entities, you can read more about metadata classes in the RIA services documentation (How To Add Metadata Classes).


- In the RegistrationBooth.Web project the file RegistrationDomainService.metadata.cs contains the metadata classes, we will change the AttendeeMetadata class to add some validation attributes to the different Attendee properties, you can read more about validation attributes at How To Validate Data, the final AttendeeMetadata should be as follows

  1. internal sealed class AttendeeMetadata
  2. {
  3. private AttendeeMetadata()
  4. {
  5. }
  6. [Required(ErrorMessage = "You must enter company name", AllowEmptyStrings = false)]
  7. [Display( Name="Company Name",Description="Name of your company")]
  8. public string Company { get; set; }
  9. public DateTime CreationDate { get; set; }
  10. [Required(ErrorMessage = "You must enter email", AllowEmptyStrings = false)]
  11. [Display( Name = "Email",Description="your Email Address")]
  12. [RegularExpression(@"\w+([-+.']\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*", ErrorMessage="Invalid email format")]
  13. public string Email { get; set; }
  14. [Required(ErrorMessage="You must enter first name",AllowEmptyStrings=false)]
  15. [Display( Name = "First Name",Description="Your First Name")]
  16. public string FirstName { get; set; }
  17. public int Id { get; set; }
  18. [Required(ErrorMessage = "You must enter last name", AllowEmptyStrings = false)]
  19. [Display( Name = "Last Name",Description="Your Last Name")]
  20. public string LastName { get; set; }
  21. public byte[] Photo { get; set; }
  22. public EntityCollection<SessionAttendee> SessionAttendees { get; set; }
  23. }

- We added RequriedAttribute for the required properties and specified the error messages that should appear if a required property is missing, the DisplayAttribute is used to provide the name (that is used as the label of the corresponding control) and the description (which appears as a ToolTip against the corresponding control), we also used a RegularExpressionAttribute for the email property to make sure that the users enter only valid email addresses.

- These metadata classes are automatically copied to the Silverlight project and compiled as part of the Silverlight application, to see this go to the RegistrationBooth project and show all the files, you will find a folder called Generated_Code, inside this folder you will find a file RegistrationBooth.Web.g.cs that contains all the classes defined on the server and shared by the client, this sharing of code allows the validation that we specified by the attributes to run directly on the client side.

- Now our entity class has the necessary metadata required for validation, let’s see how we will make use of this in creating our UI.

- We will use the DataForm control, the DataForm control can read the data annotations specified on the object class that this control is bound to.

- So to start open Home.xaml and delete the user control and the Save button, also delete the btnSave_Click method.

- From the toolbox drag and drop a DataForm control on the page, name it dfAttendee.

- Set the ItemsSource property to a binding to the element attendeeDomainDataSource and the Path is the Data property

DataForm ItemsSource Binding

- Because we will use the DataForm only for adding new items, we need to show only the save button, we can control what buttons (Add, Edit, Delete, Navigation, etc) should appear on the DataForm using the CommandButtonsVisibility property, so we will set this property to Commit.

- To make the DataForm open in “AddNew” mode, write the following line of code in the Page_Loaded event handler

  1. private void Page_Loaded(object sender, System.Windows.RoutedEventArgs e)
  2. {
  3. dataForm1.AddNewItem();
  4. }

- The DataForm control can automatically create a UI based on the properties of the entity that it’s bound too, but we will not use this default template because for the Photo property of the Attendee class we need to make use of the Web Cam to capture the image, so we will create a custom template for Adding Items, in the DataForm control you can specify different template for each mode so you can have a template for the view, template for the edit and a template for adding new item, in our case we need only the new item template.

- We will copy the same controls we had in the RegisterAttendee user control, we will bound the text boxes to the corresponding attendee properties, we will put each text box inside a DataField.

- The complete markup of the DataForm is shown below

  1. <toolkit:DataForm x:Name="dfAttendee" CommandButtonsVisibility="Commit"
  2. ItemsSource="{Binding ElementName=attendeeDomainDataSource, Path=Data}"
  3. Height="273" Width="545" >
  4. <toolkit:DataForm.NewItemTemplate>
  5. <DataTemplate>
  6. <Grid>
  7. <Grid.RowDefinitions>
  8. <RowDefinition Height="10*" />
  9. <RowDefinition Height="10*" />
  10. <RowDefinition Height="10*" />
  11. <RowDefinition Height="10*" />
  12. <RowDefinition Height="25*"/>
  13. </Grid.RowDefinitions>
  14. <toolkit:DataField Grid.Row="0" >
  15. <TextBox Height="23" HorizontalAlignment="Left" Margin="0,12,0,0" Text="{Binding FirstName,Mode=TwoWay}"
  16. VerticalAlignment="Top" Width="120" />
  17. </toolkit:DataField>
  18. <toolkit:DataField Grid.Row="1" >
  19. <TextBox Height="23" HorizontalAlignment="Left" Margin="0,12,0,0" Text="{Binding LastName,Mode=TwoWay}"
  20. VerticalAlignment="Top" Width="120" />
  21. </toolkit:DataField>
  22. <toolkit:DataField Grid.Row="2" >
  23. <TextBox Height="23" HorizontalAlignment="Left" Margin="0,12,0,0" Text="{Binding Email,Mode=TwoWay}"
  24. VerticalAlignment="Top" Width="120" />
  25. </toolkit:DataField>
  26. <toolkit:DataField Grid.Row="3" >
  27. <TextBox Height="23" HorizontalAlignment="Left" Margin="0,12,0,0" Text="{Binding Company,Mode=TwoWay}"
  28. VerticalAlignment="Top" Width="120" />
  29. </toolkit:DataField>
  30. <toolkit:DataField x:Name="dfPhoto" Grid.Row="4" >
  31. <Grid>
  32. <Grid.ColumnDefinitions>
  33. <ColumnDefinition Width="20*" />
  34. <ColumnDefinition Width="10*" />
  35. <ColumnDefinition Width="20*" />
  36. </Grid.ColumnDefinitions>
  37. <Rectangle x:Name="videoStream" Grid.Column="0" ></Rectangle>
  38. <ToggleButton Grid.Column="1" Height="30" Margin="0,41,0,62" Name="btnStart" Click="btnStart_Click">Start</ToggleButton>
  39. <Button Grid.Column="1" Height="30" Margin="0,77,0,26" Name="btnCapture" Click="btnCapture_Click">Capture</Button>
  40. <Image x:Name="capturedImage" Source="{Binding Path=Photo,Mode=TwoWay, Converter={StaticResource imageConverter}}" Grid.Column="2" ></Image>
  41. </Grid>
  42. </toolkit:DataField>
  43. </Grid>
  44. </DataTemplate>
  45. </toolkit:DataForm.NewItemTemplate>
  46. </toolkit:DataForm>

- if you examine the Image control at the end of the template you can see that this control is bound to the Photo property and it uses the ImageConverter class to do the conversion, we introduced the ImageConverter class in the first post and we implemented the Convert method that converts the Photo property which is a byte array to a bitmap that can be displayed in the Image control, now when we are adding a new item there will be already an image in the Image control (that we will capture using the web cam) so now we need to convert the bitmap from the Image control to a byte array that will be assigned to the Photo property, so we need to implement the second method of the Converter which is the ConvertBack method

  1. public object ConvertBack(object value, Type targetType, object parameter,
  2. System.Globalization.CultureInfo culture)
  3. {
  4. var bitmap = value as WriteableBitmap;
  5. var img = bitmap.ToImage();
  6. byte[] ba;
  7. var encoder = new PngEncoder();
  8. MemoryStream stream = new MemoryStream();
  9. encoder.Encode(img, stream);
  10. ba = stream.ToArray();
  11. stream.Close();
  12. return ba;
  13. }

- Now we need to write the event handlers for the two buttons (btnStart, btnCapture) which are inside the new item template, the code is shown below.

  1. private void btnStart_Click(object sender, RoutedEventArgs e)
  2. {
  3. ToggleButton btnStart = sender as ToggleButton;
  4. if (btnStart.IsChecked.Value)
  5. {
  6. StartWebcam();
  7. btnStart.Content = "Stop";
  8. }
  9. else
  10. {
  11. StopWebcam();
  12. btnStart.Content = "Start";
  13. }
  14. }
  15. private CaptureSource cs = null;
  16. private void btnCapture_Click(object sender, RoutedEventArgs e)
  17. {
  18. cs.CaptureImageAsync();
  19. }
  20. private void StartWebcam()
  21. {
  22. if (CaptureDeviceConfiguration.AllowedDeviceAccess
  23. CaptureDeviceConfiguration.RequestDeviceAccess())
  24. {
  25. VideoCaptureDevice vcd = CaptureDeviceConfiguration.GetDefaultVideoCaptureDevice();
  26. if (null != vcd)
  27. {
  28. cs = new CaptureSource();
  29. cs.VideoCaptureDevice = vcd;
  30. cs.Start();
  31. VideoBrush videoBrush = new VideoBrush();
  32. videoBrush.Stretch = Stretch.UniformToFill;
  33. videoBrush.SetSource(cs);
  34. Rectangle videoStream = dfAttendee.FindNameInContent("videoStream") as Rectangle;
  35. videoStream.Fill = videoBrush;
  36. cs.CaptureImageCompleted += new EventHandler<CaptureImageCompletedEventArgs>(cs_CaptureImageCompleted);
  37. }
  38. else
  39. {
  40. MessageBox.Show("Error initializing Webcam");
  41. }
  42. }
  43. }
  44. private void StopWebcam()
  45. {
  46. if (null != cs)
  47. {
  48. cs.Stop();
  49. }
  50. }
  51. void cs_CaptureImageCompleted(object sender, CaptureImageCompletedEventArgs e)
  52. {
  53. Image capturedImage = dfAttendee.FindNameInContent("capturedImage") as Image;
  54. capturedImage.Source = e.Result;
  55. }

- The code is almost identical to the code that we had inside the user control, except for the way we access the controls inside the template, for example the rectangle that displays the live stream of the web cam (videoStream) to get a reference to this rectangle we use the DataForm method FindNameInContent.

- As we mentioned previously to push the changes done locally to the server we need to call the domain data source SubmitChanges method, so whenever we click the Ok button in the DataForm we will call the SubmitChanges method to immediately submit the new attendee to the server, we can do this by adding an event handler to the DataForm EditEnded event, as shown below

  1. private void dfAttendee_EditEnded(object sender, DataFormEditEndedEventArgs e)
  2. {
  3. if (e.EditAction == DataFormEditAction.Commit)
  4. {
  5. attendeeDomainDataSource.SubmitChanges();
  6. }
  7. }

- Run the application and examine the labels of the controls and the tooltip that shows the description of each property.

Automatic Description Tooltip

- Try to save without entering any data to see the client side validation in action.

Automatic Validation

- The only thing missing is printing the name tag for the attendee, to do this add event handler for the domain data source SubmittedChanges event, this event is called after the SubmitChanges method is executed at the server, in this event handler we check to see if the attendee was successfully saved then we print the name tag, the code is shown below

  1. Attendee newAttendee;
  2. private void attendeeDomainDataSource_SubmittedChanges(object sender, SubmittedChangesEventArgs e)
  3. {
  4. if (e.ChangeSet.AddedEntities.Count > 0 && e.HasError ==false)
  5. {
  6. newAttendee = e.ChangeSet.AddedEntities[0] as Attendee;
  7. if (newAttendee != null)
  8. {
  9. PrintDocument document = new PrintDocument();
  10. document.PrintPage += new System.EventHandler<PrintPageEventArgs>(document_PrintPage);
  11. document.Print("Attendee_" + newAttendee.Id);
  12. }
  13. }
  14. }

- Unfortunately this code will not work, you will get Exception “Dialogs must be user-initiated” this is a Silverlight 4 built-in security measure (read more here), to resolve this issue we will change our code to show a button once the attendee is saved and we will move the printing code to this button Click event handler, the modified code is shown below

  1. <Button Content="Print Name Tag" Width="200"
  2. Click="btnPrintNameTag_Click" x:Name="btnPrintNameTag"
  3. Visibility="Collapsed" ></Button>

  1. Attendee newAttendee;
  2. private void attendeeDomainDataSource_SubmittedChanges(object sender, SubmittedChangesEventArgs e)
  3. {
  4. if (e.ChangeSet.AddedEntities.Count > 0 && e.HasError ==false)
  5. {
  6. newAttendee = e.ChangeSet.AddedEntities[0] as Attendee;
  7. if (newAttendee != null)
  8. {
  9. btnPrintNameTag.Visibility = Visibility.Visible;
  10. }
  11. }
  12. }
  13. private void btnPrintNameTag_Click(object sender, RoutedEventArgs e)
  14. {
  15. PrintDocument document = new PrintDocument();
  16. document.PrintPage += new System.EventHandler<PrintPageEventArgs>(document_PrintPage);
  17. document.Print("Attendee_" + newAttendee.Id);
  18. btnPrintNameTag.Visibility = Visibility.Collapsed;
  19. }

That’s all for this post, next time we will continue :).

You can download the code from here.

1 comment:

Anonymous said...

Thank you keap it up!