Now that I’m back developing production Line of Business (LOB) applications again, I wanted to share some techniques and challenges that I’ve overcome in our WPF UI.
Our company uses the Infragistics NetAdvantage for WPF product for our WPF UI. We have been very happy with the product, support response and updates. Currently, we use the XamRibbon
, and XamOutlookBar
in our Prism shell; the XamDataChart
in our dashboard; and XamDataGrid
in our data entry forms. Our application is in its infancy and as new requirements, features and capabilities are added, we will be using more of the controls in the NetAdvantage for WPF full featured suite.
Introduction
For my first post, I thought I would cover an aspect of using the XamDataGrid
(all data grids really) that is foundational to LOB applications.
What I’m referring to is tracking the users edits (inserts, updates and deletes) while working in a data grid and then providing the capability to utilize the same database connection to perform the updates, and optionally perform the updates as an atomic transaction.
There are use cases when a user changes data in a data grid, that those changes are immediately reflected back to the database.
Our scenario is one where the design requires that the user be able to make their edits and then commit all the changes all at once. An example of this would be a master-detail use case, when child rows are associated with a master record, e.g., customers
, orders
, order detail
.
In this example, all other development concerns such as data validation, concurrency, actually reading from or writing to a database are not part of the example code; this enables the reader to focus on one technique for tracking inserts, updates, and deletes.
XamDataGrid Requirement
The Infragistics XamDataGrid
requires that the data source implement IBindingList
in order for the data grid to support adding rows.
Yes, there are techniques for dynamically adding data to your data source without having to do this, but I wanted to allow the data grid to perform its work in a natural way so our data source is derives from BindingList<T>
.
Tracking Inserts, Updates and Deletes
When the user deletes a row in the XamDataGrid
, the row is removed from the collection and is no longer visible in the UI.
This is where the below ChangeTrackingBindingList<T>
comes into play. This class keeps track of the deleted rows by adding a deleted row to an internal collection and then exposing a method (GetDeletedItems
) to return those deleted rows when required. If you look below, you’ll see I’ve overridden the RemoveItem
method; the implementation adds the deleted row to the internal collection of deleted rows.
It also exposes a method (GetChangedItems
) to return only those non-deleted rows that have been inserted or updated.
namespace GridEditing.Infrastructure {
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
public class ChangeTrackingBindingList<T> : BindingList<T> where T : ITrackDirtyEntity {
IList<T> _deletedItems = new List<T>();
public ChangeTrackingBindingList() {
}
public ChangeTrackingBindingList(IList<T> list)
: base(list) {
}
public IEnumerable<T> GetAllItems() {
return this.Union(_deletedItems).ToList();
}
public IEnumerable<T> GetChangedItems() {
return this.Where(i => i.IsDirty).ToList();
}
public IEnumerable<T> GetDeletedItems() {
return _deletedItems;
}
protected override void ClearItems() {
base.ClearItems();
_deletedItems = new List<T>();
}
protected override void RemoveItem(Int32 index) {
var item = this[index];
_deletedItems.Add(item);
base.RemoveItem(index);
}
}
}
Consuming the ChangeTrackingBindingList<T>
The below MainWindowViewModel
exposes the Customers
collection. The collection is initialized and customers inserted in the constructor. (Please do not populate your collections in your constructors, this is demo-only code.)
You’ll notice that after loading the customers, I loop through the collection and set the IsDirty
property to false
. Your base class for your entity objects should handle this for you, so that when an object is populated from a database, the object is returned from the service layer in a non-dirty state to provide accurate tracking in the UI layer. The below example is over simplified on purpose to show how the view model will process the data once the user saves their changes.
The most important method below is the SaveExecute
method that is invoked when the user clicks the Save button. The CanSaveExecute
method determines if the collection has been changed or not. Any change to the collection will cause the Save button to be enabled.
Please Note: The code in the SaveExecute
method would 99.9999% of the time actually be executing in a service layer. The service layer method would receive the ChangeTrackBindingList<T>
as an argument and would use a data layer to process the changes.
Within the SaveExecute
method, we can see the workflow:
- Deleted items are removed from the database.
- Then the inserted or updated items are committed to the database.
- The view model would then reload the inserted and changed data into the data grid. This reloading refreshing the timestamps that play the role in concurrency and identity columns are populated after an item is inserted and reloaded from the database.
namespace GridEditing {
using System;
using System.Diagnostics;
using System.Linq;
using System.Windows.Input;
using GridEditing.Infrastructure;
using GridEditing.Model;
public class MainWindowViewModel : ObservableObject {
ChangeTrackingBindingList<Customer> _customers;
ICommand _saveCommand;
Boolean _customersDirty;
public ChangeTrackingBindingList<Customer> Customers {
get { return _customers; }
set {
_customers = value;
RaisePropertyChanged("Customers");
}
}
public ICommand SaveCommand {
get { return _saveCommand ?? (_saveCommand = new RelayCommand(SaveExecute, CanSaveExecute)); }
}
public MainWindowViewModel() {
var list = new ChangeTrackingBindingList<Customer>();
list.Add(new Customer { FirstName = "Josh", LastName = "Smith", Id = 1 });
list.Add(new Customer { FirstName = "Sacha", LastName = "Barber", Id = 2 });
list.Add(new Customer { FirstName = "Brian", LastName = "Lagunas", Id = 3 });
list.Add(new Customer { FirstName = "Karl", LastName = "Shifflett", Id = 4 });
foreach (var customer in list) {
customer.IsDirty = false;
}
this.Customers = list;
this.Customers.ListChanged += (s, e) => _customersDirty = true;
}
Boolean CanSaveExecute() {
return _customersDirty;
}
void SaveExecute() {
foreach (var customer in this.Customers.GetDeletedItems()) {
Debug.WriteLine(
String.Format("Customer deleted: {0} {1}", customer.FirstName, customer.LastName));
}
foreach (var customer in this.Customers.GetChangedItems()) {
if (customer.Id == 0) {
customer.Id = this.Customers.Max(c => c.Id) + 1;
Debug.WriteLine(
String.Format("Customer inserted: {0} {1}", customer.FirstName, customer.LastName));
} else {
Debug.WriteLine(
String.Format("Customer updated: {0} {1}", customer.FirstName, customer.LastName));
}
}
_customersDirty = false;
}
}
}
Running the Application
When the application is initially spun up, the Save button will be disabled since no inserts, updates or deletes have taken place.
After a row has been inserted, the Save button is enabled.
To the initial data, the following changes have been made.
When the Save button is clicked, notice the Debug output window. Each of the requested operations are performed; these operations are easily driven by the ChangeTrackingBindingList<T>
.
After the Save is completed, the UI will be updated as below, the Id
field has been populated and the Save button is disabled again.
UI XAML – XamDataGrid Tips
The Infragistics XamDataGrid
really makes it easy for developers to deliver a full-featured data grid editing experience. This example does not even scratch the surface of the many capabilities of the data grid. In future posts, I’ll demonstrate some real-world scenarios with respect to the XamDataGrid
’s UI capabilities, for now, let’s stick with tracking the users edits and updating the database.
Tips
- By default, the
XamDataGrid
will set string
properties to null
if the user enters a blank or empty string
in the data grid. This is not the behavior I want, instead I need an empty string
. This is accomplished using a no-op converter.
Infragistics has made applying the no-op converter a snap by setting ValueToTextConverter
property on the XamTextEditor
using a style as I’ve done below. In the XamDataGridNullStringPreventionConverter
’s Convert
and ConvertBack
methods, I simply return value. The causes the XamDataGrid
to set string
properties to an empty string
instead of the null
value. - Take a look down at the
XamDataGrid.FieldSettings
, you’ll notice CellClickAction
has been set to EnterEditModeIfAllowed
. The user no longer has to double click a cell to get into edit mode, simply clicking the cell, will put the cell in edit mode. - Making an
Id
field read-only is straightforward. Have a look at the Id
field in the XamDataGrid
’s FieldLayout
. By setting Id
columns FieldSettings.AllowEdit
to false
, the cells are read-only.
<Window
xmlns:local="clr-namespace:GridEditing"
xmlns:infrastructure="clr-namespace:GridEditing.Infrastructure"
x:Class="GridEditing.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Data Grid Editing and Deleting"
Height="341"
Width="608"
ResizeMode="NoResize"
xmlns:igDP="http://infragistics.com/DataPresenter"
xmlns:igEditors="http://infragistics.com/Editors">
<Window.DataContext>
<local:MainWindowViewModel />
</Window.DataContext>
<Window.Resources>
<infrastructure:XamDataGridNullStringPreventionConverter
x:Key="XamDataGridNullStringPreventionConverter" />
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock Text="Infragistics Data Grid Editing and
Deleting" Margin="11" FontSize="18" />
<igDP:XamDataGrid
Grid.Row="1" Margin="11"
DataSource="{Binding Path=Customers}"
IsNestedDataDisplayEnabled="False">
<igDP:XamDataGrid.Resources>
<Style TargetType="{x:Type TextBox}"
BasedOn="{StaticResource {x:Type TextBox}}" >
<Setter Property="Margin" Value="0" />
</Style>
<Style TargetType="{x:Type igEditors:XamTextEditor}" >
<Setter
Property="ValueToTextConverter"
Value="{StaticResource XamDataGridNullStringPreventionConverter}" />
</Style>
</igDP:XamDataGrid.Resources>
<igDP:XamDataGrid.FieldSettings>
<igDP:FieldSettings CellClickAction="EnterEditModeIfAllowed" />
</igDP:XamDataGrid.FieldSettings>
<igDP:XamDataGrid.FieldLayoutSettings>
<igDP:FieldLayoutSettings
SupportDataErrorInfo="None"
DataErrorDisplayMode="None"
AutoGenerateFields="False"
AddNewRecordLocation="OnBottom"
AllowAddNew="True"
AllowClipboardOperations="All"
AllowDelete="True"
ExpansionIndicatorDisplayMode="Never" />
</igDP:XamDataGrid.FieldLayoutSettings>
<igDP:XamDataGrid.FieldLayouts>
<igDP:FieldLayout>
<igDP:FieldLayout.SortedFields>
<igDP:FieldSortDescription FieldName="Id" Direction="Ascending" />
</igDP:FieldLayout.SortedFields>
<igDP:Field Name="Id">
<igDP:Field.Settings>
<igDP:FieldSettings AllowEdit="False" />
</igDP:Field.Settings>
</igDP:Field>
<igDP:Field Name="FirstName" Width="200" />
<igDP:Field Name="LastName" Width="200" />
</igDP:FieldLayout>
</igDP:XamDataGrid.FieldLayouts>
</igDP:XamDataGrid>
<Button
Width="65" Margin="7" Grid.Row="2" Content="Save"
Command="{Binding Path=SaveCommand}" HorizontalAlignment="Right"/>
</Grid>
</Window>
Download
After downloading, change the extension from .zip.DOC to .zip. This is a requirement of WordPress.
Download demo application (25K)
Requirements
To run the demo application, you’ll need to get the Infragistics NetAdvantage for WPF. A demo version is available here.
Close
There are business frameworks like CSLA that have trackable objects and handle the above scenarios I’ve shown. Currently, I prefer to the simplest solution possible and use the above for tracking user changes within the XamDataGrid
.
You can see the amount of very simple code to enable this scenario is small, easy to understand and implement.
Have a great day!
Just a grain of sand on the worlds beaches.