Friday, January 14, 2011

Silverlight, PRISM, MVVM, MEF – Part 3

Your view model or model will often be required to perform data validation and to signal any data validation errors to the view so that the user can act to correct them.
Silverlight and WPF provide support for managing data validation errors that occur when changing individual properties that are bound to controls in the view. For single properties that are data-bound to a control, the view model or model can signal a data validation error within the property setter by rejecting an incoming bad value and throwing an exception. If the ValidatesOnExceptions property on the data binding is true, the data binding engine in WPF and Silverlight will handle the exception and display a visual cue to the user that there is a data validation error.
However, throwing exceptions with properties in this way should be avoided where possible. An alternative approach is to implement the IDataErrorInfo or INotifyDataErrorInfo interfaces on your view model or model classes. These interfaces allow your view model or model to perform data validation for one or more property values and to return an error message to the view so that the user can be notified of the error.
If you are using WCF RIA services for your silverlight application, the rich built-in support for the validation attributes in the System.ComponentModel.DataAnnotation namespace can be used to implement the validation rules via attributes. Just by having that attribute on the server entity or its metadata class, it shows up on the code generated client entity. Additionally, the WCF RIA Services Entity base class that is added to the client entity has an implementation of INotifyDataErrorInfo that uses the data annotation attributes to perform validation when data bound in the UI. So just by adding these attributes with appropriate error messages, you get validation indications for the user in the UI.
The validation attributes in the System.ComponentModel.DataAnnotation namespace include the ability to specify simple forms of validation on entity properties by adding an attribute to an entity property. For e.g.  The below sample shows data validations applied on the Customer entity.
[MetadataTypeAttribute(typeof(Customer.CustomerMetadata))]
public partial class Customer
{
    internal sealed class CustomerMetadata
    {
        private CustomerMetadata()
        {
        }

        [Required(ErrorMessage="FirstName should be provided")]
        [StringLength(100, ErrorMessage="FirstName max length is 100")]
        public string FirstName { get; set; }

        [Key]
        [Required]
        public int Id { get; set; }

        [Required(ErrorMessage = "LastName should be provided")]
        [StringLength(100, ErrorMessage = "LastName max length is 100")]
        public string LastName { get; set; }

        [Range(typeof(DateTime), "01/01/1990", "01/01/2011", ErrorMessage="Date of joining should be in the range 01-01-1990 - 01-01-2011")]
        public Nullable<DateTime> DateOfJoining { get; set; }

        public EntityCollection<Order> Orders { get; set; }
    }
}
The XAML code looks like.
<StackPanel Orientation="Vertical">
    <telerik:RadGridView AutoGenerateColumns="False" ItemsSource="{Binding Customers}" SelectedItem="{Binding CurrentCustomer, Mode=TwoWay}"  HorizontalAlignment="Left" Margin="0 20 0 0">
        <telerik:RadGridView.Columns>
            <telerik:GridViewDataColumn DataMemberBinding="{Binding FirstName}" Header="First Name" Width="200" />
            <telerik:GridViewDataColumn DataMemberBinding="{Binding LastName}" Header="Last Name" Width="200"/>
            <gridViewColumns:DateTimePickerColumn DataMemberBinding="{Binding DateOfJoining}" Header="DOJ" Width="150" />
        telerik:RadGridView.Columns>
    telerik:RadGridView>
    <Grid x:Name="LayoutRoot" Background="White">
        <Grid.Resources>
            <Style x:Key="TextBlockStyle" TargetType="TextBlock" >
                <Setter Property="HorizontalAlignment" Value="Right" />
                <Setter Property="VerticalAlignment" Value="Center" />
                <Setter Property="Height" Value="25" />
                <Setter Property="Margin" Value="0 0 10 0" />
                <Setter Property="FontSize" Value="14" />
            Style>
        Grid.Resources>
        <Grid.RowDefinitions>
            <RowDefinition Height="35" />
            <RowDefinition Height="35" />
            <RowDefinition Height="35" />
            <RowDefinition Height="35" />
        Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="250" />
            <ColumnDefinition Width="*" />
        Grid.ColumnDefinitions>
        <TextBlock Text="First Name" Style="{StaticResource TextBlockStyle}" />
        <TextBlock Text="Last Name" Style="{StaticResource TextBlockStyle}" Grid.Row="1" />
        <TextBlock Text="Date Of Joining" Style="{StaticResource TextBlockStyle}" Grid.Row="2" />
        <TextBox Text="{Binding CurrentCustomer.FirstName, Mode=TwoWay, NotifyOnValidationError=True}" Grid.Column="1" Width="250" HorizontalAlignment="Left" VerticalAlignment="Center" Height="25" />
        <TextBox Text="{Binding CurrentCustomer.LastName, Mode=TwoWay, NotifyOnValidationError=True}" Grid.Column="1" Grid.Row="1" Width="250" HorizontalAlignment="Left" VerticalAlignment="Center" Height="25" />
        <TextBox Text="{Binding CurrentCustomer.LastName, Mode=TwoWay, NotifyOnValidationError=True}" Grid.Column="1" Grid.Row="1" Width="250" HorizontalAlignment="Left" VerticalAlignment="Center" Height="25" />
        <Button Content="Update" Width="100" Height="25" Grid.Row="3" HorizontalAlignment="Right" prism:Click.Command="{Binding UpdateCustomerCommand}" />
        <telerik:RadDatePicker Grid.Column="1" Grid.Row="2" HorizontalAlignment="Left" VerticalAlignment="Center" SelectedValue="{Binding CurrentCustomer.DateOfJoining, Mode=TwoWay, NotifyOnValidationError=True}" />
    Grid>
StackPanel>
ViewModel implementation
[Export(typeof(CustomerViewModel))]
[PartCreationPolicy(CreationPolicy.NonShared)]
public class CustomerViewModel : NotificationObject
{
    public ObservableCollection<Customer> Customers { get; set; }

    public Customer CurrentCustomer
    {
        get { return _customer; }
        set
        {
            _customer = value;
            RaisePropertyChanged("CurrentCustomer");
        }
    }
       
    [ImportingConstructor]
    public CustomerViewModel(ICustomerRepository repository)
    {
        _customerRepository = repository;
        Customers = new ObservableCollection<Customer>();
        WireEvents();
        LoadCustomers();
    }

    private void WireEvents()
    {
        _customerRepository.OnCustomersLoaded += new EventHandler<EventArgs<IEnumerable<Customer>>>((x, y) =>
            {
                foreach (var customer in y.Data) Customers.Add(customer);
            });
    }

    private void LoadCustomers()
    {
        _customerRepository.GetAll();
    }
}
When invalid data is entered in the view, you can see the validation errors on the properties as in the figure below.

3 comments:

Abc said...

Hi there,

Is it possible that you can post the full source code here

Prajeesh said...

Hi I'm working on cleaning up the code from the samples. Will post that to codeplex this weekend.

Prajeesh said...

Added the code to codeplex..
http://blogsprajeesh.blogspot.com/2011/02/prism-mef-mvvm-toc.html