Designing and Developing Windows Applications Using Microsoft .NET Framework 4: Designing the Presentation Layer

  • 12/17/2011

Objective 2.4: Design Data Presentation and Input

Most of what your Presentation layer is doing in a distributed application is either presenting or receiving data. Your Presentation layer will be responsible for validating data, binding data and presenting it to the user, and presenting media to the user. In this section, you will learn how to handle data in the Presentation layer.

Designing Data Validation

An important part of designing any UI is how to handle data input. Users can be error-prone, and to ensure the integrity of your database, it is vital that data be validated for correctness. You might need to use several different data validation techniques, depending on the needs of your application.

Data Type Validation

The simplest form of data validation is data type validation. In this type of validation, data is simply checked to ensure that it is of the appropriate type. For example, input that should be a string is checked to see if it is a string, and numeric values are parsed to an integer or decimal. This type of validation can usually be accomplished with fairly simple methods in the UI.

Range Checking

An extension of data type validation, range checking ensures not only that data is of the appropriate data type, but also that it falls within an acceptable range of values. For example, a field that asked for the age of an employee might require a value between 18 and 100, or some other range that represented the actual range of working employees within the company. This type of validation is also not complex and usually can be accomplished within the UI.

Lookup Validation

Lookup validation can be thought of as a more specialized form of range checking. In lookup validation, fields are only allowed to be one of a certain set of values, which may or may not be sequential in any way. For example, consider an application that required the serial number of an item being sold to correspond to a serial number of an item in an inventory database. The typical solution to lookup validation is to create a lookup table and validate against the values contained in that table. Lookup validation can take place against a static set of values or against values that change over time, requiring dynamic generation of the lookup table. In either case, this kind of data validation usually requires a more complex business rule to validate against.

Complex Validation

Your input data might require a more complex type of validation than any of the types described here. For complex validation, separate business rules will be required.

Validating Data at the Client and Server

When validating user input, you should validate it on the client (for immediate responsiveness and to assist data entry) and again at the server (for security). Never trust data validation performed at the client because it is possible for users to bypass client-side validation controls by modifying the application, altering data after it is sent by the application, or creating an entirely different application that connects to the server.

The easiest way to validate data in a Windows Form application is to use the Validating event. The Validating event occurs before a control loses the focus. This event is raised only when the CausesValidation property of the control that is about to receive the focus is set to True. Thus, if you want to use the Validating event to validate data entered in your control, the CausesValidation of the next control in the tab order should be set to true. In order to use Validating events, the CausesValidation property of the control to be validated must also be set to True. By default, the CausesValidation property of all controls is set to True when controls are created at design time. Controls such as Help buttons are typically the only kind of controls that have CausesValidation set to False.

The Validating event allows you to perform sophisticated validation on your controls. You could, for example, implement an event handler that tested whether the value entered corresponded to a very specific format. Another possible use is an event handler that doesn’t allow the focus to leave the control until a value has been entered.

The Validating event includes an instance of the CancelEventArgs class. This class contains a single property, Cancel. If the input in your control does not fall within the required parameters, you can use the Cancel property within your event handler to cancel the Validating event and return the focus to the control.

The Validated event fires after a control has been validated successfully. You can use this event to perform any actions based upon the validated input.

The following example demonstrates a handler for the Validating event. This method requires an entry in TextBox1 before it will allow the focus to move to the next control.

Sample of Visual Basic.NET Code

Private Sub TextBox1_Validating(ByVal sender As Object, ByVal e As _
   System.ComponentModel.CancelEventArgs) Handles TextBox1.Validating
   ' Checks the value of TextBox1
   If TextBox1.Text = "" Then
      ' Resets the focus if there is no entry in TextBox1
      e.Cancel = True
   End If
End Sub

Sample of C# Code

private void textBox1_Validating(object sender,
   System.ComponentModel.CancelEventArgs e)
{
   // Checks the value of textBox1
   if (textBox1.Text == "")
      // Resets the focus if there is no entry in TextBox1
      e.Cancel = true;
}

Validating Data in a WPF Application

WPF allows you to set validation rules that define how your application validates its data. Each Binding object exposes a ValidationRules collection. You can add new rules to the ValidationCollection, as shown in bold in this example:

<TextBox>
   <TextBox.Text>
      <Binding Path="CandyBars">
         <Binding.ValidationRules>
            <local:CandyBarValidationRule />
            <local:SweetTreatsValidationRule />
         </Binding.ValidationRules>
      </Binding>
   </TextBox.Text>
</TextBox>

In this example, the CandyBarValidationRule and SweetTreatsValidationRule declarations represent two custom validation rules that have been defined in your application. When a new value is bound, each of the validation rules are evaluated in the order in which they are declared. In this example, the CandyBarValidationRule is evaluated first, followed by the SweetTreatsValidationRule. If there are no validation problems, the application proceeds normally. If there is a problem that violates a validation rule, however, the following things happen:

  • The element with the validation error is outlined in red.

  • The attached property Validation.HasError is set to True.

  • A new ValidationError object is created and added to the attached Validation.Errors collection.

  • If the Binding.NotifyOnValidationError property is set to True, the Validation.Error–attached event is raised.

  • The data-binding source is not updated with the invalid value and instead remains unchanged.

Implementing Custom Validation Rules

You can create specific validation rules by creating classes that inherit the abstract class ValidationRule. The ValidationRule class has one virtual method that must be overridden: the Validate method. The Validate method receives an object parameter, which represents the value that is being evaluated, and returns a ValidationResult object, which contains an IsValid property and an ErrorCondition property. The IsValid property represents a Boolean value that indicates whether or not the value is valid, and the ErrorCondition property is text that can be set to provide a descriptive error condition. If a ValidationResult with an IsValid value of True is returned, the value is considered to be valid and application execution proceeds normally. If a ValidationResult with an IsValid result of False is returned, a ValidationError is created as described previously.

The following example demonstrates a simple implementation of the ValidationRule abstract class:

Sample of Visual Basic.NET Code

Public Class NoNullStringsValidator
    Inherits ValidationRule

    Public Overrides Function Validate(ByVal value As Object, ByVal _
        cultureInfo As System.Globalization.CultureInfo) As _
        System.Windows.Controls.ValidationResult
        Dim astring As String = value.ToString
        If astring = "" Then
            Return New ValidationResult(False, "String cannot be empty")
        Else
            Return New ValidationResult(True, Nothing)
        End If
    End Function
End Class

Sample of C# Code

public class NoNullStringsValidator : ValidationRule
{
   public override ValidationResult Validate(object value,
      System.Globalization.CultureInfo cultureinfo)
   {
      string aString = value.ToString();
      if (aString == "")
         return new ValidationResult(false, "String cannot be empty");
      return new ValidationResult(true, null);
   }
}

In this example, the string contained in the value object is evaluated. If it is a zero-length string, the validation fails; otherwise, the validation succeeds.

Handling Validation Errors

Once validation errors are raised, you must decide how to respond to them. In some cases, the visual cues provided by the validation error are enough—the user can see that the element is surrounded by a red outline and can detect and fix the problem. In other cases, however, you might need to provide feedback to the user regarding the nature of the validation problem.

When validation is enabled for a binding, the Validation.Error event is attached to the bound element. Validation.Error includes an instance of ValidationErrorEventArgs, which contains two important properties, as described in Table 2-9.

Table 2-9 Important Properties of ValidationErrorEventArgs

Property

Description

Action

Describes whether the error in question is a new error or an old error that is being cleared

Error

Contains information about the error that occurred, the details of which are described in further detail in Table 5-7

The Error object of ValidationErrorEventArgs contains a host of useful information regarding the error that occurred. Important properties of the Error object are described in Table 2-10.

Table 2-10 Important Properties of the Error Object

Property

Description

BindingInError

Contains a reference to the Binding object that caused the validation error

ErrorContent

Contains the string set by the ValidationRule object that returned the validation error

Exception

Contains a reference to the Exception, if any, that caused the validation error

RuleInError

Contains a reference to the ValidationRule that caused the validation error

The Validation.Error event is not fired unless the NotifyOnValidationError property of the Binding object is specifically set to True, as shown in bold here:

<Binding NotifyOnValidationError="True" Mode="TwoWay"
   Source="{StaticResource StringCollection}" Path="name">
   <Binding.ValidationRules>
      <local:NoNullStringsValidator/>
   </Binding.ValidationRules>
</Binding>

When this property is set to True, the Validation.Error event is raised anytime any ValidationRule in the ValidationRules collection of the Binding object detects a validation error. The Validation.Error event is a bubbling event. It is raised first in the element where the validation error occurs and then in each higher-level element in the visual tree. Thus, you can create a local error-handling method that specifically handles validation errors from a single element, as shown in bold here:

<TextBox Validation.Error="TextBox1_Error" Height="21" Width="100"
    Name="TextBox1" >

Alternatively, you can create an error-handling routine that is executed higher in the visual tree to create a more generalized validation error handler, as shown in bold here:

<Grid Validation.Error="Grid_Error">

The Validation.Error event is fired both when a new validation error is detected and when an old validation error is cleared. Thus, it is important to check the e.Action property to determine whether the error is being cleared or it is a new error. The following example demonstrates a sample validation error handler that displays the error message to the user when a new error occurs and writes information to Trace when a validation error is cleared:

Sample of Visual Basic.NET Code

Private Sub Grid_Error(ByVal sender As System.Object, ByVal e As _
   System.Windows.Controls.ValidationErrorEventArgs)
   If e.Action = ValidationErrorEventAction.Added Then
      MessageBox.Show(e.Error.ErrorContent.ToString)
   Else
      Trace.WriteLine("Validation error cleared")
   End If
End Sub

Sample of C# Code

private void Grid_Error(object sender, ValidationErrorEventArgs e)
{
   if (e.Action == ValidationErrorEventAction.Added)
      MessageBox.Show(e.Error.ErrorContent.ToString());
   else
      System.Diagnostics.Trace.WriteLine("Validation error cleared");
}

Design a Data Binding Strategy

Data binding in the Presentation layer typically refers to binding presentation controls to a cached client-side copy of the bound data. This data can be presented to the user and changed or added to as need be, and then updated to or from the central data store when the application requires it. When deciding on a data binding strategy for your Presentation layer, you must decide how you want to store your local data, and then determine what component in the Presentation layer you want to use to connect the presentation logic to the local data.

Data Binding in Windows Forms

Local data in a Windows Forms application is typically held in a Dataset object. Dataset is a very versatile class that can handle multiple data tables. While you can bind your Presentation layer directly to a Dataset, a better strategy is to access the individual tables in a Dataset via a BindingSource component.

The BindingSource component manages data currency and navigation for the underlying data source. Thus, by binding your Presentation layer controls to a BindingSource object that refers to the local copy of data, you are able to manage the presentation of the underlying data easily. The BindingSource component contains the information that controls need to bind to a BindingSource by passing it a reference to a DataTable in a DataSet. By binding to the BindingSource instead of to the DataSet, you can redirect your application to another source of data easily without having to redirect all the data binding code to point to the new data source.

The following code shows how to create a BindingSource and assign it a reference to the Northwind Customers table in a Northwind database-based dataset named NorthwindDataSet1:

Sample of Visual Basic.NET Code

customersBindingSource = New BindingSource(NorthwindDataSet1, "Customers")

Sample of C# Code

customersBindingSource = new BindingSource(northwindDataSet1, "Customers");

Binding to Types Other Than Dataset

The BindingSource component can be used to bind to several different types of objects and will in most cases expose the underlying data of that object as an IBindingList interface. Table 2-11 explains the types that the BindingSource.DataSource property can be set to and what the result will be.

Table 2-11 Types for the DataSource Property of BindingSource

DataSource Property

List Results

Nothing

An empty IBindingList of objects. Adding an item sets the list to the type of the added item.

Nothing with DataMember set

Not supported; raises ArgumentException.

Non-list type or object of type “T”

Empty IBindingList of type “T”.

Array instance

IBindingList containing the array elements.

IEnumerable instance

An IBindingList containing the IEnumerable items.

List instance containing type “T”

IBindingList instance containing type “T”.

Data Binding in WPF

WPF has data binding built in at all levels, and you can bind a WPF Presentation layer easily to a variety of data sources, including datasets, objects, or XML representations in memory.

Binding to collections in WPF is handled in essentially the same way, whether the bound collection is a dataset, datatable, or other in-memory object. For simple displaying of bound members, you must set the ItemsSource property to the collection to which you are binding and set the DisplayMemberPath property to the collection member that is to be displayed. The following example demonstrates how to bind a ListBox to a static resource named myList and a display member called FirstName:

<ListBox Width="200" ItemsSource="{Binding Source={StaticResource myList}}"
   DisplayMemberPath="FirstName" />

A more common scenario when working with bound lists, however, is to bind to an object that is defined and filled with data in code. In this case the best way to bind to the list is to set the DisplayMemberPath in the XAML and then set the DataContext of the element or its container in code. The following example demonstrates how to bind a ListBox to an object called myCustomers that already has been created and populated. The ListBox displays the entries from the CustomerName property:

Sample of Visual Basic.NET Code

' Code to initialize and fill myCustomers has been omitted
grid1.DataContext = myCustomers;

Sample of Visual C# Code

// Code to initialize and fill myCustomers has been omitted
grid1.DataContext = myCustomers;

Sample of XAML Code

<Grid Name="grid1">
   <ListBox ItemsSource="{Binding}" DisplayMemberPath="CustomerName"
      Margin="92,109,66,53" Name="ListBox1" />
</Grid>

Note that in the XAML for this example, the ItemsSource property is set to a Binding object that has no properties initialized. The Binding object binds the ItemsSource of the ListBox, but because the Source property of the Binding is not set, WPF searches upward through the visual tree until it finds a DataContext that has been set. Because the DataContext for grid1 has been set in code to myCustomers, this then becomes the source for the binding.

Navigating Bound Data in WPF

WPF has a built-in navigation mechanism for data and collections. When a collection is bound to by a WPF Binding, an ICollectionView is created behind the scenes. The ICollectionView interface contains members that manage data currency, as well as managing views, grouping, and sorting. You can get a reference to the ICollectionView by calling the CollectionViewSource.GetDefaultView method, as shown here:

Sample of Visual Basic.NET Code

' This example assumes a collection named myCollection
Dim myView As System.ComponentModel.ICollectionView
myView = CollectionViewSource.GetDefaultView (myCollection)

Sample of Visual C# Code

// This example assumes a collection named myCollection
System.ComponentModel.ICollectionView myView;
myView = CollectionViewSource.GetDefaultView (myCollection);

When calling this method, you must specify the collection or list for which to retrieve the view (which is myCollection in the previous example). CollectionViewSource.GetDefaultView returns an ICollectionView object that is actually one of three different classes depending on the class of the source collection.

If the source collection implements IBindingList, the view returned is a BindingListCollectionView object. If the source collection implements IList but not IBindingList, the view returned is a ListCollectionView object. If the source collection implements IEnumerable but not IList or IBindingList, the view returned is a CollectionView object.

Binding to XML in WPF

The XmlDataProvider allows you to bind WPF elements to data in the XML format. The following example demonstrates an XmlDataProvider providing data from a source file called Items.xml:

<Window.Resources>
   <XmlDataProvider x:Key="Items" Source="Items.xml" />
</Window.Resources>

You can also provide the XML data inline as an XML data island. In this case you wrap the XML data in XData tags, as shown here:

<Window.Resources>
   <XmlDataProvider x:Key="Items">
      <x:XData>
         <!--XML Data omitted-->
      </x:XData>
   </XmlDataProvider>
</Window.Resources>

You can bind elements to the data provided by an XmlDataProvider in the same way that you would bind to any other data source—namely, using a Binding object and specifying the XmlDataProvider in the Source property, as shown here:

<ListBox ItemsSource="{Binding Source={StaticResource Items}}"
   DisplayMemberPath="ItemName" Name="listBox1" Width="100" Height="100"
   VerticalAlignment="Top" />

Using XPath When Binding to XML

You can use XPath expressions to filter the results exposed by the XmlDataProvider or to filter the records displayed in the bound controls. By setting the XPath property of the XmlDataProvider to an XPath expression, you can filter the data provided by the source. The following example filters the results exposed by an XmlDataProvider object to include only those nodes called <ExpensiveItems> in the <Items> top-level node:

<Window.Resources>
   <XmlDataProvider x:Key="Items" Source="Items.xml"
   XPath="Items/ExpensiveItems" />
</Window.Resources>

You also can apply XPath expressions in the bound controls. The following example sets the XPath property to Diamond (shown in bold), which indicates that only data contained in <Diamond> tags will be bound:

<ListBox ItemsSource="{Binding Source={StaticResource Items}
   XPath=Diamond}" DisplayMemberPath="ItemName" Name="listBox1" Width="100"
   Height="100" VerticalAlignment="Top" />

Using Data Templates in the WPF Presentation Layer

A data template is a bit of XAML that describes how bound data is displayed. A data template can contain elements that are bound to a data property, along with additional markup that describes layout, color, and other aspects of appearance. The following example demonstrates a simple data template that describes a Label element bound to the ContactName property. The Foreground, Background, BorderBrush, and BorderThickness properties are also set:

<DataTemplate>
   <Label Content="{Binding Path=ContactName}" BorderBrush="Black"
      Background="Yellow" BorderThickness="3" Foreground="Blue" />
</DataTemplate>

You set the data template on a control by setting one of two properties. For content controls, you set the ContentTemplate property, as shown in bold here:

<Label Height="23" HorizontalAlignment="Left" Margin="56,0,0,91"
   Name="label1" VerticalAlignment="Bottom" Width="120">
   <Label.ContentTemplate>
      <DataTemplate>
         <!--Actual data template omitted-->
      </DataTemplate>
   </Label.ContentTemplate>
</Label>

For item controls, you set the ItemsTemplate property, as shown in bold here:

<ListBox ItemsSource="{Binding}" IsSynchronizedWithCurrentItem="True"
   Margin="18,19,205,148" Name="listBox1">
   <ListBox.ItemTemplate>
      <DataTemplate>
         <!--Actual data template omitted-->
      </DataTemplate>
   </ListBox.ItemTemplate>
</ListBox>

Note that for item controls, the DisplayMemberPath and ItemTemplate properties are mutually exclusive—you can set one but not the other.

A frequent pattern with data templates is to define them in a resource collection and reference them in your element, rather than defining them inline as shown in the previous examples. All that is required to reuse a data template in this manner is to define the template in a resource collection and set a Key for the template, as shown here:

<Window.Resources>
   <DataTemplate x:Key="myTemplate">
      <Label Content="{Binding Path=ContactName}" BorderBrush="Black"
         Background="Yellow" BorderThickness="3" Foreground="Blue" />
   </DataTemplate>
</Window.Resources>

Then you can set the template by referring to the resource, as shown in bold here:

<ListBox ItemTemplate="{StaticResource myTemplate}"
   Name="ListBox1" />

Managing Data Shared Between Forms

If you must share data between forms in the Presentation layer, the best way to do so is by scoping the objects containing the data as application scope variables. This allows members in all forms in an application to access the data.

Creating an Application Variable in Windows Forms with Visual Basic

When programming in Visual Basic .NET, the easiest way to create a variable with application scope is to create a module and declare a public variable within that module. This variable then will be available to all forms in the application.

Creating an Application Variable in Windows Forms with Visual C#

When programming in Visual C#, the best way to create an application-scoped variable is to add a public, static variable to the .cs file that contains the Main sub. In Visual Studio–generated applications, this is usually Program.cs. Variables created in this fashion will be available to all forms in an application, though they will need to be prefaced with the name of the class.

Creating an Application Variable in WPF

When creating an application variable in WPF, the best way is to create an application resource that will be accessible by all objects in a particular application. You can create an application resource by opening the App.xaml file (for C# projects) or the Application.xaml file (for Visual Basic projects) and adding the resource to the Application.Resources collection, as shown in bold here:

<Application x:Class="WpfApplication2.App"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    StartupUri="Window1.xaml">
    <Application.Resources>
       <SolidColorBrush x:Key="appBrush" Color="PapayaWhip" />
    </Application.Resources>
</Application>

Managing Media

The .NET Framework provides tools that allow you to manage sound and video media presentations in your client applications. For simple sound support, the SoundPlayer component is provided; and for more complex sound or video media, the MediaPlayer and MediaElement components can be used.

SoundPlayer

The SoundPlayer class was introduced in .NET Framework 2.0 as a managed class to enable audio in Windows applications. It is lightweight and easy to use, but it has significant limitations.

The SoundPlayer class can play only uncompressed .wav files. It cannot read compressed .wav files, nor can it read files in other audio formats. Furthermore, the developer has no control over volume, balance, speed of playback, or any other aspects of sound playback.

In spite of its limitations, SoundPlayer can be a useful and lightweight way to incorporate sound into your applications. It provides a basic set of members that allow you to load and play uncompressed .wav files easily.

MediaPlayer and MediaElement

The MediaPlayer and MediaElement classes provide deep support for playing audio and video media files in a variety of formats. Both of these classes use the functionality of Windows Media Player 10, so while they are guaranteed to be usable in applications running on Windows Vista and later, which come with Media Player 11 as a standard feature, these classes will not function on Windows XP installations that do not have at least Windows Media Player 10 installed.

The MediaPlayer and MediaElement classes are very similar and expose many of the same members. The primary difference between the two classes is that although MediaPlayer loads and plays both audio and video, it has no visual interface and thus cannot display video in the UI. On the other hand, MediaElement is a full-fledged WPF element that can be used to display video in your applications. MediaElement wraps a MediaPlayer object and provides a visual interface to play video files. Furthermore, MediaPlayer cannot be used easily in XAML, whereas MediaElement is designed for XAML use.

While MediaPlayer and MediaElement are designed for use in WPF applications, you can use them in your Windows Forms applications through interoperability, as described earlier in this chapter.

Objective Summary

  • Data validation is a common task for the Presentation layer. Depending on the data, any of a number of different types of validation might be necessary. Both WPF and Windows Forms provide technologies to enable validation of user input.

  • WPF and Windows Forms both enable binding to datasets and other collections. In Windows Forms, the BindingSource object manages data currency and navigation. In WPF, these tasks are managed through the ICollectionView interface. WPF also incorporates technology that allows direct binding to XML and the use of XPath queries.

  • Data can be shared between multiple forms via application variables.

  • The SoundPlayer, MediaPlayer, and MediaElement classes incorporate functionality that enables multimedia presentations.

Objective Review

Answer the following questions to test your knowledge of the information in this objective. You can find the answers to these questions and explanations of why each answer choice is correct or incorrect in the “Answers” section at the end of the chapter.

  1. You are creating a Presentation layer that will validate user input. The fields that need to be validated include an employee age field, which must be an integer between 18 and 85. What is the correct data validation strategy for this scenario?

    1. Data type validation

    2. Range checking

    3. Lookup validation

    4. Complex validation

  2. Which of the following code snippets correctly demonstrates a data template that binds the ContactName field set in a ListBox? Assume that the DataContext is set correctly.

    1. <ListBox ItemsSource="{Binding}" name="ListBox1">
         <DataTemplate>
            <Label Content="{Binding Path=ContactName}" BorderBrush="Black"
               Background="Yellow" BorderThickness="3" Foreground="Blue" />
         </DataTemplate>
      </ListBox>
    2. <ListBox name="ListBox1">
         <ListBox.ItemsSource>
            <DataTemplate>
               <Label Content="{Binding Path=ContactName}" BorderBrush="Black"
                  Background="Yellow" BorderThickness="3" Foreground="Blue" />
            </DataTemplate>
         </ListBox.ItemsSource>
      </ListBox>
    3. <ListBox ItemsSource="{Binding}" name="ListBox1">
         <ListBox.ItemTemplate>
            <Label Content="{Binding Path=ContactName}" BorderBrush="Black"
               Background="Yellow" BorderThickness="3" Foreground="Blue" />
         </ListBox.ItemTemplate>
      </ListBox>
    4. <ListBox ItemsSource="{Binding}" name="ListBox1">
         <ListBox.ItemTemplate>
            <DataTemplate>
               <Label Content="{Binding Path=ContactName}" BorderBrush="Black"
                  Background="Yellow" BorderThickness="3" Foreground="Blue" />
            </DataTemplate>
         </ListBox.ItemTemplate>
      </ListBox>