|
|||||||||||||||||||||
|
|||||||||||||||||||||
|
Announcements
Chapters
Services
Feature Zones
|
Contents
IntroductionA DataGrid is a user interface component for displaying tabular data to the user, typically providing sorting and editing functionality, among others. DataGrids have been the work-horse of various frameworks such as ASP.NET ( Fortunately, the absence of this control has not hampered the popularity of WPF. The versatility of the Eventually, in August 2008, Microsoft released its DataGrid CTP (Community Technology Preview - a public beta) to CodePlex to coincide with the release of the .NET Framework 3.5 SP1 and Visual Studio 2008 SP1. The .NET Service Packs provided additional WPF functions including More recently, on October 22 2008, DataGrid v1 was released. This is the last update that we will see of the Article overviewCurrently, there is a lack of documentation and examples demonstrating common I cannot, of course, cover everything. If you are having problems with making the Installing the WPF ToolkitThe WPF
Binding to a DataSetProbably, one of the most frequent uses of a Displaying data from a DataSetFor this example, and the others in this article, I am using the ubiquitous Northwind database. Details of how to download it for SQL Express are given in the MSDN library. The
The simplest method for displaying the Customers table within the WPF <Window x:Class="WPFDataGridExamples.DataSetCRUDExample"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:dg="http://schemas.microsoft.com/wpf/2008/toolkit"
Title="Northwind Customer" Height="300" Width="600">
<Grid>
<dg:DataGrid ItemsSource="{Binding}"/>
</Grid>
</Window>
Then, construct an instance of our typed dataset, and populate it using the generated Table Adapter: public DataSetCRUDExample()
{
InitializeComponent();
// construct the dataset
NorthwindDataSet dataset = new NorthwindDataSet();
// use a table adapter to populate the Customers table
CustomersTableAdapter adapter = new CustomersTableAdapter();
adapter.Fill(dataset.Customers);
// use the Customer table as the DataContext for this Window
this.DataContext = dataset.Customers.DefaultView;
}
The resulting window will contain a grid which displays all the columns of the Customers table, thanks to the
This works well enough; however, one of the advertised features of the WPF An alternative method for providing data to your controls is through the use of an The following class effectively performs the same dataset population steps as above: public class CustomerDataProvider
{
private CustomersTableAdapter adapter;
private NorthwindDataSet dataset;
public CustomerDataProvider()
{
dataset = new NorthwindDataSet();
adapter = new CustomersTableAdapter();
adapter.Fill(dataset.Customers);
}
public DataView GetCustomers()
{
return dataset.Customers.DefaultView;
}
}
And, the modified XAML below uses the <Window ...>
<Window.Resources>
<!-- create an instance of our DataProvider class -->
<ObjectDataProvider x:Key="CustomerDataProvider"
ObjectType="{x:Type local:CustomerDataProvider}"/>
<!-- define the method which is invoked to obtain our data -->
<ObjectDataProvider x:Key="Customers"
ObjectInstance="{StaticResource CustomerDataProvider}"
MethodName="GetCustomers"/>
</Window.Resources>
<DockPanel DataContext="{Binding Source={StaticResource Customers}}">
<dg:DataGrid ItemsSource="{Binding}" Name="dataGrid"/>
</DockPanel>
</Window>
With the above code, the design-time support of the
This design-time support is certainly nice to have; however, it is very easily missed as it inserts a single menu option into an existing context menu. The WPF designer (Cider) does not follow the conventions of the Windows Forms and ASP.NET designers which indicate that a control has design-time support by the presence of a small button in the top right corner. Performing updatesWhen the user edits the Customers data within the The following example shows how the public CustomerDataProvider()
{
NorthwindDataSet dataset = new NorthwindDataSet();
adapter = new CustomersTableAdapter();
adapter.Fill(dataset.Customers);
dataset.Customers.CustomersRowChanged +=
new NorthwindDataSet.CustomersRowChangeEventHandler(CustomersRowModified);
dataset.Customers.CustomersRowDeleted +=
new NorthwindDataSet.CustomersRowChangeEventHandler(CustomersRowModified);
}
void CustomersRowModified(object sender, NorthwindDataSet.CustomersRowChangeEvent e)
{
adapter.Update(dataset.Customers);
}
The complete example above can be found in the Master / Detail viewA classic use of a In this example, synchronized views of the Customer (master) and Orders (detail) tables of the Northwind database will be displayed. The XAML below demonstrates how a master / detail view may be achieved. A second data source is added, again via the <Window ... >
<Window.Resources>
<!-- the customers datasource -->
<ObjectDataProvider x:Key="CustomerDataProvider"
ObjectType="{x:Type local:CustomerDataProvider}"/>
<ObjectDataProvider x:Key="Customers" MethodName="GetCustomers"
ObjectInstance="{StaticResource CustomerDataProvider}" />
<!-- the orders datasource -->
<ObjectDataProvider x:Key="OrdersDataProvider"
ObjectType="{x:Type local:OrdersDataProvider}"/>
<ObjectDataProvider x:Key="Orders" MethodName="GetOrdersByCustomer"
ObjectInstance="{StaticResource OrdersDataProvider}" >
<ObjectDataProvider.MethodParameters>
<x:Static Member="system:String.Empty"/>
</ObjectDataProvider.MethodParameters>
</ObjectDataProvider>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<dg:DataGrid Grid.Row="0"
ItemsSource="{Binding Source={StaticResource Customers}}"
SelectedValuePath="CustomerID"
SelectionChanged="CustomerGrid_SelectionChanged"/>
<dg:DataGrid Grid.Row="1"
ItemsSource="{Binding Source={StaticResource Orders}}"/>
</Grid>
</Window>
The /// <summary>
/// Obtains all the orders for the given customer.
/// </summary>
public DataView GetOrdersByCustomer(string customerId)
{
if (customerId == null || customerId == string.Empty)
{
return null;
}
DataView view = NorthWindDataProvider.NorthwindDataSet.Orders.DefaultView;
view.RowFilter = string.Format("CustomerID='{0}'", customerId);
return view;
}
When this method is invoked for the first time, an empty string is supplied as the private void CustomerGrid_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
DataGrid grid = sender as DataGrid;
// pass the customer ID to our Orders datasource via the ObjectDataProvider
ObjectDataProvider orderProvider = this.FindResource("Orders") as ObjectDataProvider;
orderProvider.MethodParameters[0] = grid.SelectedValue;
}
The above code simply sets the first
Any updates / deletions to the Customer or Order rows are written to the database. However, the interface is a little peculiar in that when inserting a new order row via the bottom The first step is to use the "Generate Columns" command on the Visual Studio Designer. We can then remove both the generated <dg:DataGrid Grid.Row="1" ItemsSource="{Binding Source={StaticResource Orders}}"
AutoGenerateColumns="True" RowEditEnding="DataGrid_RowEditEnding">
<dg:DataGrid.Columns>
<!-- <dg:DataGridTextColumn
Binding="{Binding Mode=OneWay, Path=OrderID}" Header="OrderID"/> -->
<!-- <dg:DataGridTextColumn
Binding="{Binding Path=CustomerID}" Header="CustomerID" /> -->
<dg:DataGridTextColumn
Binding="{Binding Path=EmployeeID}" Header="EmployeeID" />
<dg:DataGridTextColumn
Binding="{Binding Path=OrderDate}" Header="OrderDate" />
<dg:DataGridTextColumn
Binding="{Binding Path=RequiredDate}" Header="RequiredDate" />
<dg:DataGridTextColumn
Binding="{Binding Path=ShippedDate}" Header="ShippedDate" />
<dg:DataGridTextColumn
Binding="{Binding Path=ShipVia}" Header="ShipVia" />
<dg:DataGridTextColumn
Binding="{Binding Path=Freight}" Header="Freight" />
<dg:DataGridTextColumn
Binding="{Binding Path=ShipName}" Header="ShipName" />
<dg:DataGridTextColumn
Binding="{Binding Path=ShipAddress}" Header="ShipAddress" />
<dg:DataGridTextColumn
Binding="{Binding Path=ShipCity}" Header="ShipCity" />
<dg:DataGridTextColumn
Binding="{Binding Path=ShipRegion}" Header="ShipRegion" />
<dg:DataGridTextColumn
Binding="{Binding Path=ShipPostalCode}" Header="ShipPostalCode" />
<dg:DataGridTextColumn
Binding="{Binding Path=ShipCountry}" Header="ShipCountry" />
</dg:DataGrid.Columns>
</dg:DataGrid>
A handler for the private void DataGrid_RowEditEnding(object sender, DataGridRowEditEndingEventArgs e)
{
// drill down from DataGridRow, through row view to our order row
DataGridRow dgRow = e.Row;
DataRowView rowView = dgRow.Item as DataRowView;
NorthwindDataSet.OrdersRow orderRow =
rowView.Row as NorthwindDataSet.OrdersRow;
// set the foreign key to the customer ID
orderRow.CustomerID = CustomerGrid.SelectedValue as string;
}
The complete example above is found in the Binding in a layered applicationThe previous example demonstrated how to bind the This example demonstrates how to use a The architectureThis example is a simple CRUD application which allows the user to edit items in the Customers table of the Northwind database. The example has a Data Access Layer, which exposes Find/ Delete/Update methods that operate on simple data objects, and a Presentation Layer that adapts these objects in such a way that they can be bound effectively by the WPF framework. Because we are only performing CRUD functions, I have not added a Business Logic Layer (BLL); if you are a purist, you could add a pass-through BLL; however, I feel it would add little to this example. The key classes within this architecture are shown below: The Data Access Layer exposes an interface for managing the lifecycle of the Customer Data Objects. The class which implements this interface uses a typed public interface ICustomerDataAccessLayer
{
/// Return all the persistent customers
List<CustomerDataObject> GetCustomers();
/// Updates or adds the given customer
void UpdateCustomer(CustomerDataObject customer);
/// Delete the given customer
void DeleteCustomer(CustomerDataObject customer);
}
public class CustomerDataObject
{
public string ID { get; set; }
public string CompanyName { get; set; }
public string ContactName { get; set; }
}
As you can see, there are no UI framework specific interfaces or classes (such as We could bind the Handling delete operationsThe public CustomerObjectDataProvider()
{
dataAccessLayer = new CustomerDataAccessLayer();
}
public CustomerUIObjects GetCustomers()
{
// populate our list of customers from the data access layer
CustomerUIObjects customers = new CustomerUIObjects();
List<CustomerDataObject> customerDataObjects = dataAccessLayer.GetCustomers();
foreach (CustomerDataObject customerDataObject in customerDataObjects)
{
// create a business object from each data object
customers.Add(new CustomerUIObject(customerDataObject));
}
customers.CollectionChanged += new
NotifyCollectionChangedEventHandler(CustomersCollectionChanged);
return customers;
}
void CustomersCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Remove)
{
foreach (object item in e.OldItems)
{
CustomerUIObject customerObject = item as CustomerUIObject;
// use the data access layer to delete the wrapped data object
dataAccessLayer.DeleteCustomer(customerObject.GetDataObject());
}
}
}
When a user deletes a row with the Handling delete operations is relatively straightforward, but how about updates or insertions? You might think that the same approach can be used, the Handling updates / insertsTo determine when a user finishes editing a bound item, we need to delve a little deeper into the binding mechanism itself. The In order to notify the public delegate void ItemEndEditEventHandler(IEditableObject sender);
public event ItemEndEditEventHandler ItemEndEdit;
#region IEditableObject Members
public void BeginEdit() {}
public void CancelEdit() {}
public void EndEdit()
{
if (ItemEndEdit != null)
{
ItemEndEdit(this);
}
}
#endregion
When items are added to the public class CustomerUIObjects : ObservableCollection<CustomerDataObject>
{
protected override void InsertItem(int index, CustomerUIObject item)
{
base.InsertItem(index, item);
// handle any EndEdit events relating to this item
item.ItemEndEdit += new ItemEndEditEventHandler(ItemEndEditHandler);
}
void ItemEndEditHandler(IEditableObject sender)
{
// simply forward any EndEdit events
if (ItemEndEdit != null)
{
ItemEndEdit(sender);
}
}
public event ItemEndEditEventHandler ItemEndEdit;
}
The public CustomerUIObjects GetCustomers()
{
// populate our list of customers from the data access layer
CustomerUIObjects customers = new CustomerUIObjects();
List<CustomerDataObject> customerDataObjects = dataAccessLayer.GetCustomers();
foreach (CustomerDataObject customerDataObject in customerDataObjects)
{
// create a business object from each data object
customers.Add(new CustomerUIObject(customerDataObject));
}
customers.ItemEndEdit += new ItemEndEditEventHandler(CustomersItemEndEdit);
customers.CollectionChanged += new
NotifyCollectionChangedEventHandler(CustomersCollectionChanged);
return customers;
}
void CustomersItemEndEdit(IEditableObject sender)
{
CustomerUIObject customerObject = sender as CustomerUIObject;
// use the data access layer to update the wrapped data object
dataAccessLayer.UpdateCustomer(customerObject.GetDataObject());
}
The above code will handle both insert and update operations. In conclusion, this method adapts the data items and collection provided by the DAL into UI items and collections which are more appropriate for data binding within the WPF framework. All database synchronisation logic is performed by handling event from this bound collection; therefore, there is no WPF One final note: the above example does not include error handling. For example, foreign key constraint violations will result in the bound ValidationValidation within the WPF However, it should be noted that the validation support for the WPF This article will present a few common validation scenarios, demonstrating how the Validation on exceptionsA common approach to validation is to have your object's property setters throw an exception if the passed value is not valid for whatever reason. The WPF framework includes a validation rule, <!-- explicit addition of ExceptionValidationRule -->
<TextBox>
<TextBox.Text>
<Binding Path="Name">
<Binding.ValidationRules>
<ExceptionValidationRule />
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
<!-- implicit addition of ExceptionValidationRule -->
<TextBox Text="{Binding Path=Name, ValidatesOnExceptions=True}"/>
To demonstrate how a public class Person
{
private readonly Regex nameEx = new Regex(@"^[A-Za-z ]+$");
private string name;
public string Name
{
get { return name; }
set
{
if (value == null)
throw new ArgumentException("Name cannot be null");
if (!nameEx.Match(value).Success)
throw new ArgumentException("Name may only " +
"contain characters or spaces");
name = value;
}
}
private int age;
public int Age
{
get { return age; }
set
{
if (value < 0 || value > 110)
throw new ArgumentException("Age must be positive " +
"and less than 110");
age = value;
}
}
}
If we simply enable validation for a
However, there is no feedback to the user regarding the nature of the error, and there is no indicator on the row to alert the user. Often, validation failures are displayed as a tooltip relating to the data input control, as follows (see the aforementioned CodeProject article for further details and examples): <Style TargetType="{x:Type TextBox}">
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="true">
<Setter Property="ToolTip"
Value="{Binding RelativeSource={RelativeSource Self},
Path=(Validation.Errors)[0].ErrorContent}"/>
</Trigger>
</Style.Triggers>
</Style>
However, implicit styles do not work for elements generated by The XAML to display this data object within a <Window ... >
<Window.Resources>
<!-- the data source for this Window -->
<ObjectDataProvider x:Key="PersonDataSource"
ObjectType="{x:Type local:PersonDataSource}"/>
<ObjectDataProvider x:Key="People"
ObjectInstance="{StaticResource PersonDataSource}"
MethodName="GetPeople"/>
<!-- style to apply to DataGridTextColumn in edit mode -->
<Style x:Key="CellEditStyle" TargetType="{x:Type TextBox}">
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Padding" Value="0"/>
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="true">
<Setter Property="ToolTip"
Value="{Binding RelativeSource={RelativeSource Self},
Path=(Validation.Errors)[0].ErrorContent}"/>
</Trigger>
</Style.Triggers>
</Style>
</Window.Resources>
<DockPanel DataContext="{Binding Source={StaticResource People}}">
<dg:DataGrid Name="dataGrid" AutoGenerateColumns="False"
ItemsSource="{Binding}">
<dg:DataGrid.Columns>
<dg:DataGridTextColumn Header="Name"
EditingElementStyle="{StaticResource CellEditStyle}"
Binding="{Binding Path=Name, ValidatesOnExceptions=True}"/>
<dg:DataGridTextColumn Header="Age"
EditingElementStyle="{StaticResource CellEditStyle}"
Binding="{Binding Path=Age, ValidatesOnExceptions=True}"/>
</dg:DataGrid.Columns>
</dg:DataGrid>
</DockPanel>
</Window>
Which gives the following result:
A common interface feature of the <dg:DataGrid Name="dataGrid" AutoGenerateColumns="False" ItemsSource="{Binding}">
<dg:DataGrid.RowValidationRules>
<local:RowDummyValidation/>
</dg:DataGrid.RowValidationRules>
<dg:DataGrid.Columns>
<dg:DataGridTextColumn Header="Name"
EditingElementStyle="{StaticResource CellEditStyle}"
Binding="{Binding Path=Name, ValidatesOnExceptions=True}"/>
<dg:DataGridTextColumn Header="Age"
EditingElementStyle="{StaticResource CellEditStyle}"
Binding="{Binding Path=Age, ValidatesOnExceptions=True}"/>
</dg:DataGrid.Columns>
</dg:DataGrid>
This validation rule simply returns
Validation with IDataErrorInfoA popular alternative to the previous example, where exceptions are thrown on the property setters of the data objects, is the use of the public class Appointment : IDataErrorInfo
{
private readonly Regex nameEx = new Regex(@"^[A-Za-z ]+$");
public string Name { get; set; }
public int Age { get; set; }
public DateTime StartTime { get; set; }
public DateTime EndTime { get; set; }
#region IDataErrorInfo Members
public string Error
{
get
{
StringBuilder error = new StringBuilder();
// iterate over all of the properties
// of this object - aggregating any validation errors
PropertyDescriptorCollection props = TypeDescriptor.GetProperties(this);
foreach (PropertyDescriptor prop in props)
{
string propertyError = this[prop.Name];
if (propertyError != string.Empty)
{
error.Append((error.Length!=0 ? ", " : "") + propertyError);
}
}
// apply object level validation rules
if (StartTime.CompareTo(EndTime) > 0)
{
error.Append((error.Length != 0 ? ", " : "") +
"EndTime must be after StartTime");
}
return error.ToString();
}
}
public string this[string columnName]
{
get
{
// apply property level validation rules
if (columnName == "Name")
{
if (Name == null || Name == string.Empty)
return "Name cannot be null or empty";
if (!nameEx.Match(Name).Success)
return "Name may only contain characters or spaces";
}
if (columnName == "Age")
{
if (Age < 0 || Age > 110)
return "Age must be positive and less than 110";
}
return "";
}
}
#endregion
}
These objects are bound to a grid with the following XAML: <dg:DataGrid Name="dataGrid" AutoGenerateColumns="False"
RowStyle="{StaticResource RowStyle}" ItemsSource="{Binding}">
<dg:DataGrid.RowValidationRules>
<local:RowDataInfoValidationRule ValidationStep="UpdatedValue" />
</dg:DataGrid.RowValidationRules>
<dg:DataGrid.Columns>
<dg:DataGridTextColumn Header="Name" Binding="{Binding Path=Name}"/>
<dg:DataGridTextColumn Header="Age" Binding="{Binding Path=Age}"/>
<dg:DataGridTextColumn Header="Start" Binding="{Binding Path=StartTime}"/>
<dg:DataGridTextColumn Header="End" Binding="{Binding Path=EndTime}"/>
</dg:DataGrid.Columns>
</dg:DataGrid>
The row validation rule in the above example is given below: public class RowDataInfoValidationRule : ValidationRule
{
public override ValidationResult Validate(object value,
CultureInfo cultureInfo)
{
BindingGroup group = (BindingGroup)value;
StringBuilder error = null;
foreach (var item in group.Items)
{
// aggregate errors
IDataErrorInfo info = item as IDataErrorInfo;
if (info != null)
{
if (!string.IsNullOrEmpty(info.Error))
{
if (error == null)
error = new StringBuilder();
error.Append((error.Length != 0 ? ", " : "") + info.Error);
}
}
}
if (error != null)
return new ValidationResult(false, error.ToString());
return ValidationResult.ValidResult;
}
}
This rule iterates over all of the items within the binding group (i.e., the The image below shows the use of
Note also that because the validation error does not relate to an individual property of our business object, none of the <!-- Row Style-->
<Style x:Key="RowStyle" TargetType="{x:Type dg:DataGridRow}">
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="true">
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="BorderBrush" Value="Red"/>
<Setter Property="ToolTip"
Value="{Binding RelativeSource={RelativeSource Self},
Path=(Validation.Errors)[0].ErrorContent}"/>
</Trigger>
</Style.Triggers>
</Style>
The WPF framework also has a stock validation rule for use with objects that implement public class CellDataInfoValidationRule : ValidationRule
{
public override ValidationResult Validate(object value,
CultureInfo cultureInfo)
{
// obtain the bound business object
BindingExpression expression = value as BindingExpression;
IDataErrorInfo info = expression.DataItem as IDataErrorInfo;
// determine the binding path
string boundProperty = expression.ParentBinding.Path.Path;
// obtain any errors relating to this bound property
string error = info[boundProperty];
if (!string.IsNullOrEmpty(error))
{
return new ValidationResult(false, error);
}
return ValidationResult.ValidResult;
}
}
With the above rule associated with our column bindings, you can now give feedback regarding which cell has a validation error (in the case where the validation error relates to an individual object property):
Validation with bound DataSetsThe WPF When a new row or changes to an existing row is committed to a The workaround given here uses validation to ensure that all the public class DataRowValidation : ValidationRule
{
public override ValidationResult Validate(object value,
CultureInfo cultureInfo)
{
// if this rule is being applied to a cell we
// will be inspecting a binding expression
if (value is BindingExpression)
{
// obtain the row which is being validated
BindingExpression expression = value as BindingExpression;
DataRow row = ((DataRowView)expression.DataItem).Row;
// determine the column to validate
string propertyName = expression.ParentBinding.Path.Path;
return ValidateColumn(propertyName, row);
}
// if this rule is being applied to a cell
// we will be inspecting a binding group
else if (value is BindingGroup)
{
BindingGroup group = (BindingGroup)value;
// iterate over all the bound items (this should always be one!)
| ||||||||||||||||||||