Up WPF tour
DevForce Resource Center » Getting started » WPF tour » Tour of DevForce WPF - Part 2

Tour of DevForce WPF - Part 2

Last modified on September 20, 2012 08:12

Part 2: CUD - In Part 2 of this series we'll focus on "CUD" - Create, Update, and Delete.  We'll take the application we started in Part 1, swap out the DataGrid for a simple editor, show how to add and delete entities and how to save to the database, work with a parent-child relationship and see both asynchronous and eager loading of dependent data, and finally watch it all work with some simple logging.


Design the form

Let's do something a little more interesting now. It would be nice if we could add, delete, and edit these employee records.

Let's go back to MainWindow.xaml. We are going to replace the DataGrid with a form. Unfortunately, WPF doesn’t provide us with a control to rapidly develop a data form, so we are going to have to build our form the old fashioned way by dragging fields, labels and buttons onto the designer and wiring everything up ourselves.

First, let’s remove the DataGrid and prepare the layout. We would like to have a row of buttons at the top, followed by labels and fields for the Employee properties.

Inside the XAML pane for MainWindow.xaml, remove the <DataGrid /> element and add two row definitions to the grid. We will use the top row for the buttons and the second row for the actual form.

The completed markup for MainWindow.xaml now looks like this:

XAML
<Window x:Class="WpfSimpleSteps.MainWindow"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  Title="MainWindow" Height="350" style="width:525px">
  <Grid>
    <Grid.RowDefinitions>
      <RowDefinition Height="Auto" />
      <RowDefinition Height="Auto" />
    </Grid.RowDefinitions>
  </Grid>
</Window>

Next, we are going to add all of our buttons. Let’s add another grid in the first row with 8 buttons. The eight buttons will be labeled First, Previous, Next, Last, Add, Delete, Ok, Cancel. We will be able to perform all of our operations with these buttons. So, let’s add the necessary markup for the button row:

XAML
<Grid>
  <Button Content="First" Height="23" HorizontalAlignment="Left" Margin="12,12,0,0"
          Name="firstButton" VerticalAlignment="Top" style="width:50px" />
  <Button Content="Previous" Height="23" HorizontalAlignment="Left" Margin="68,12,0,0"
          Name="prevButton" VerticalAlignment="Top" style="width:50px" />
  <Button Content="Next" Height="23" HorizontalAlignment="Left" Margin="124,12,0,0"
          Name="nextButton" VerticalAlignment="Top" style="width:50px" />
  <Button Content="Last" Height="23" HorizontalAlignment="Left" Margin="180,12,0,0"
          Name="lastButton" VerticalAlignment="Top" style="width:50px" />
  <Button Content="Add" Height="23" HorizontalAlignment="Left" Margin="236,12,0,0"
          Name="addButton" VerticalAlignment="Top" style="width:50px" />
  <Button Content="Delete" Height="23" HorizontalAlignment="Left" Margin="292,12,0,0"
          Name="deleteButton" VerticalAlignment="Top" style="width:50px" />
  <Button Content="Ok" Height="23" HorizontalAlignment="Left" Margin="348,12,0,0"
          Name="okButton" VerticalAlignment="Top" style="width:50px" />
  <Button Content="Cancel" Height="23" HorizontalAlignment="Left" Margin="404,12,0,0"
          Name="cancelButton" VerticalAlignment="Top" style="width:50px" />
</Grid>

The completed markup for MainWindow.xaml now looks like this:

XAML
<Window x:Class="WpfSimpleSteps.MainWindow"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  Title="MainWindow" Height="350" style="width:525px">
  <Grid>
    <Grid.RowDefinitions>
      <RowDefinition Height="Auto" />
      <RowDefinition Height="Auto" />
    </Grid.RowDefinitions>
    <Grid>
      <Button Content="First" Height="23" HorizontalAlignment="Left" Margin="12,12,0,0"
          Name="firstButton" VerticalAlignment="Top" style="width:50px" />
      <Button Content="Previous" Height="23" HorizontalAlignment="Left" Margin="68,12,0,0"
          Name="prevButton" VerticalAlignment="Top" style="width:50px" />
      <Button Content="Next" Height="23" HorizontalAlignment="Left" Margin="124,12,0,0"
          Name="nextButton" VerticalAlignment="Top" style="width:50px" />
      <Button Content="Last" Height="23" HorizontalAlignment="Left" Margin="180,12,0,0"
          Name="lastButton" VerticalAlignment="Top" style="width:50px" />
      <Button Content="Add" Height="23" HorizontalAlignment="Left" Margin="236,12,0,0"
          Name="addButton" VerticalAlignment="Top" style="width:50px" />
      <Button Content="Delete" Height="23" HorizontalAlignment="Left" Margin="292,12,0,0"
          Name="deleteButton" VerticalAlignment="Top" style="width:50px" />
      <Button Content="Ok" Height="23" HorizontalAlignment="Left" Margin="348,12,0,0"
          Name="okButton" VerticalAlignment="Top" style="width:50px" />
      <Button Content="Cancel" Height="23" HorizontalAlignment="Left" Margin="404,12,0,0"
          Name="cancelButton" VerticalAlignment="Top" style="width:50px" />
    </Grid>
  </Grid>
</Window>

Next, we need to place all of the controls for the actual data form. Instead of doing this all by hand, we are going to take advantage of the Visual Studio Designer. To use the designer, we first need to create a DataSource. This DataSource is going to be solely used for simplifying our design process and can later be deleted.

First, let’s build the solution, to make sure all of the required assemblies can be found by Visual Studio. Then, in the Visual Studio menu, select “Data|Add New DataSource…”.

output_html_2f74daa2.png

We are going to create a DataSource from an Object, so select Object and click “Next”. On the next screen you should see the WpfSimpleSteps project among the IdeaBlade assemblies. Expand the WpfSimpleSteps project, find the Employee object and check it. If you don’t see the WpfSimpleSteps project, go back and make sure the solution built without errors, then come back to this step.

output_html_9cec463.png

Now click “Finish” and let Visual Studio create the DataSource. Next, select “Data|Show Data Sources” from the menu, so we can see our DataSource. Click on the dropdown next to Employee and select “Details”.

output_html_m616691f7.png

Next, let’s hide a few properties that we don’t want Visual Studio to add to our data form. Select DirectReports and click on the dropdown to the right, then select [None]. Do the same for Orders and RowVersion.

output_html_m4ce12af9.png

Next, make sure you switch to the Design view, grab Employee with your mouse and drag and drop it somewhere below the button row. This will create a new grid with all the labels and fields for the Employee properties along with the necessary bindings.

We need to make a minor adjustment before we can snap the form in its final resting place. Switch over to the XAML view and change the grid1 definition to:

XAML
<Grid DataContext="{StaticResource employeeViewSource}" Grid.Row="1" Name="grid1">

This adjustment will make sure that the form stays contained to the row below the buttons instead of spanning both rows and covering up the buttons. Before we switch back to the design view, let’s make one more change. Let’s set the height of the main window big enough to make enough room, so we can add a few more controls as this tour progresses. In the <Window /> element at the top, change the height attribute from Height=”350” to Height=”925”.

Now, switch back to the Design view and snap the form in place by right-clicking on it and selecting “Reset Layout|All”.

output_html_40612314.png

The final result now looks like this:

output_html_244e9df2.png

Notice that we could improve the form by reordering the fields for better usability. You can do so by right-clicking on a label or field and selecting “Grid Row|Move Up” and/or “Grid Row|Move Down” until everything is the way you like it. For the purpose of this tour, we just leave it the way it is.

At this point we are done with designing our form and are ready to write some code to make this form come to life.

Create, Update and Delete

Let’s open MainWindowViewModel.cs. So far the only thing in our view model is the Employees collection, which was all we needed to make the Employees grid run in the first part of this tour. To make our new data form work, we are going to have to add quite a few things to our view model.

First, we need to introduce the concept of current employee. As opposed to a DataGrid, in our data form, we are going to display one employee at a time, so we need to provide an Employee object, which the form can bind to.

So, let’s add a private field to our view model that will hold the current employee:

C#
private Employee _currentEmployee = null;
VB
Private _currentEmployee As Employee = Nothing

Next, let’s add a property to expose the current employee object, so we can bind to it from XAML:

C#
public Employee CurrentEmployee {
 get { return _currentEmployee; }
 private set {
  _currentEmployee = value;
  }
}
VB
Public Property CurrentEmployee() As Employee
 Get
   Return _currentEmployee
 End Get
 Private Set(ByVal value As Employee)
    _currentEmployee = value
 End Set
End Property

At this point, you might be wondering why we are doing this in two steps. Well, let’s think about the scenario we are implementing. Our data form is going to bind to the current employee and displays its properties. So far the code we wrote seems to be sufficient for that purpose. But, what about refreshing the view when the current employee changes? We want to be able to go next, previous and so forth and have the view display the new employee as we navigate through the employees collection, right? So, we need a way to tell the view that the current employee has changed.

This behavior is accomplished by raising events that WPF is going to listen to. In particular for this scenario, WPF is going to look for the PropertyChanged event in the DataContext and if present, it will install an event handler, so it can be notified whenever a certain property changes. Remember, the DataContext is still MainWindowViewModel. We are setting it in the Window.Loaded event handler in the code-behind for MainWindow.xaml.

So, let’s add support for PropertyChanged and raise the event in the CurrentEmployee setter. The PropertyChanged event is provided by the INotifyPropertyChanged interface. Let’s modify the MainWindowViewModel class to implement INotifyPropertyChanged:

C#
class MainWindowViewModel : INotifyPropertyChanged
VB
Class MainWindowViewModel
 Implements INotifyPropertyChanged

Add this using or Imports statement if not present:

C#
using System.ComponentModel;
VB
Imports System.ComponentModel

Next, add the following code to the class:

C#
public event PropertyChangedEventHandler PropertyChanged = delegate { };

private void RaisePropertyChanged(string property) {
  PropertyChanged(this, new PropertyChangedEventArgs(property));
}
VB
Public Event PropertyChanged As PropertyChangedEventHandler _
 Implements INotifyPropertyChanged.PropertyChanged

Private Sub RaisePropertyChanged(ByVal [property] As String)
 RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs([property]))
End Sub

As the last step, let’s go back and modify the CurrentEmployee setter to raise the event:

C#
public Employee CurrentEmployee {
 get { return _currentEmployee; }
 private set {
    _currentEmployee = value;
    RaisePropertyChanged("CurrentEmployee");
  }
}
VB
Public Property CurrentEmployee() As Employee
 Get
   Return _currentEmployee
 End Get
 Private Set(ByVal value As Employee)
    _currentEmployee = value
    RaisePropertyChanged("CurrentEmployee")
 End Set
End Property

At this point, we have created the necessary pieces, so that we can bind the current employee and whenever the current employee changes, the view gets notified and refreshes.

Next, we need a way to cursor back and forward over the employees collection and keep track of the current position. Similar to the CurrentEmployee property, let’s add a CurrentPosition property:

C#
private int _currentPosition = -1;
public int CurrentPosition {
 get { return _currentPosition; }
 set {
   if (Employees != null && Employees.Count > 0) {
      _currentPosition = Math.Min(Math.Max(value, 0), Employees.Count - 1);
      CurrentEmployee = Employees[_currentPosition];
    } else {
      _currentPosition = -1;
      CurrentEmployee = null;
    }
    RaisePropertyChanged("CurrentPosition");
  }
}
VB
Private _currentPosition As Integer = -1
Public Property CurrentPosition() As Integer
 Get
   Return _currentPosition
 End Get
 Set(ByVal value As Integer)
   If Employees IsNot Nothing AndAlso Employees.Count > 0 Then
      _currentPosition = Math.Min(Math.Max(value, 0), Employees.Count - 1)
      CurrentEmployee = Employees(_currentPosition)
   Else
      _currentPosition = -1
      CurrentEmployee = Nothing
   End If
    RaisePropertyChanged("CurrentPosition")
 End Set
End Property

Let’s take a look at this code. First, we are using -1 to indicate that the cursor is currently not positioned. This could be the case if the collection is null or empty or we haven’t done the initial positioning.

The setter for CurrentPosition does the necessary math to ensure the position never gets out of bounds and then it sets the CurrentEmployee to the employee at the current position, provided that the employees collections is not NULL and not empty.

Now we are ready to wire up the data form to what have so far.

Let’s open MainWindow.xaml and switch to the XAML view. What we have to do is bind the data form to the CurrentEmployee property. You’ll notice that Visual Studio created a CollectionViewSource named employeeViewSource. This is an artifact of using the DataSource to create our form. Let’s get rid of it. Delete the entire <Window.Resources /> element with everything in it. Next, we need to set the proper DataContext for our form.

Let's again change the definition of grid1, this time to:

XAML
<Grid DataContext="{Binding Path=CurrentEmployee}" Grid.Row="1" Name="grid1">

With this change, we are telling WPF to set the data context to the CurrentEmployee property of MainWindowViewModel. All the form fields already have the proper bindings provided as part of dragging and dropping the DataSource when we created the form.

Let’s go ahead and run the application and see what happens. To our disappointment, nothing seems to happen. The form is largely empty and the buttons don’t do anything. We shouldn’t be surprised by this behavior. We haven’t provided any logic for the buttons and we haven’t positioned the cursor. The only thing that happened is that we initialized the Employees collection and filled it with all the employees, but nothing told our view model to position the cursor in order to display an employee.

We have more work to do. So, let’s open MainWindowViewModel.cs and add a few methods that allow us to move the cursor, add an employee and delete an employee:

C#
public void MoveCurrentToFirst() {
  CurrentPosition = 0;
}

public void MoveCurrentToPrevious() {
  CurrentPosition = CurrentPosition - 1;
}

public void MoveCurrentToNext() {
  CurrentPosition = CurrentPosition + 1;
}

public void MoveCurrentToLast() {
  CurrentPosition = Employees != null ? Employees.Count - 1 : -1;
}

public void AddNew() {
  Employees.Add(new Employee());
  MoveCurrentToLast();
}

public void RemoveAt(int position) {
  Employees.RemoveAt(position);
  CurrentPosition = position;
}
VB
Public Sub MoveCurrentToFirst()
  CurrentPosition = 0
End Sub

Public Sub MoveCurrentToPrevious()
  CurrentPosition = CurrentPosition - 1
End Sub

Public Sub MoveCurrentToNext()
  CurrentPosition = CurrentPosition + 1
End Sub

Public Sub MoveCurrentToLast()
  CurrentPosition = If(Employees IsNot Nothing, Employees.Count - 1, -1)
End Sub

Public Sub AddNew()
  Employees.Add(New Employee())
  MoveCurrentToLast()
End Sub

Public Sub RemoveAt(ByVal position As Integer)
  Employees.RemoveAt(position)
  CurrentPosition = position
End Sub

These methods are pretty straightforward. We simply manipulate the CurrentPosition property to move within the collection. To add a new employee, we new up an Employee object, add it to the collection and move the cursor to the end of the collection, where the new employee object was added. Similarly for the delete operation, we delete the employee at the current position and then reposition the cursor. This last step is necessary to reset the CurrentEmployee to the employee that followed the one we just deleted. Notice that we don’t actually change the position, because all the employee objects following the one we just deleted, moved down one position, but we need to invoke the CurrentPosition setter in order to correctly set the CurrentEmployee property.

Now, all we have to do is add event handlers to the buttons and call the respective method we just added, but first we need to promote the MainWindowViewModel from a local variable to a field on the MainWindow class. Visual Studio did us a favor by creating an event handler for the Window.Loaded event when we dropped the DataSource onto the designer.

In the Solution Explorer, right-click on MainWindow.xaml and select “View Code”, this will open the code-behind for MainWindow.xaml. Modify the MainWindow class to look like this:

C#
public partial class MainWindow : Window {
 private MainWindowViewModel viewModel;
 public MainWindow() {
    InitializeComponent();
  }
 private void Window_Loaded(object sender, RoutedEventArgs e) {
    viewModel = new MainWindowViewModel();
    DataContext = viewModel;
  }
}
VB
Partial Public Class MainWindow
 Inherits Window
 Private viewModel As MainWindowViewModel
 Public Sub New()
    InitializeComponent()
 End Sub
 Private Sub Window_Loaded(ByVal sender As Object, ByVal e As RoutedEventArgs)
    viewModel = New MainWindowViewModel()
    DataContext = viewModel
 End Sub
End Class

Next, open MainWindow.xaml and switch to the design view. Double-click on the first button. This creates an event handler for the button and takes you to the code-behind for MainWindow.xaml. Repeat this step for all the buttons, except Ok and Cancel. We get to those later. Once you have all the event handlers, fill in their bodies like so:

C#
private void firstButton_Click(object sender, RoutedEventArgs e) {
  viewModel.MoveCurrentToFirst();
}

private void prevButton_Click(object sender, RoutedEventArgs e) {
  viewModel.MoveCurrentToPrevious();
}

private void nextButton_Click(object sender, RoutedEventArgs e) {
  viewModel.MoveCurrentToNext();
}

private void lastButton_Click(object sender, RoutedEventArgs e)
  viewModel.MoveCurrentToLast();
}

private void addButton_Click(object sender, RoutedEventArgs e) {
  viewModel.AddNew();
}

private void deleteButton_Click(object sender, RoutedEventArgs e) {
 if (viewModel.CurrentPosition > -1) {
    viewModel.RemoveAt(viewModel.CurrentPosition);
  }
}
VB
Private Sub firstButton_Click(ByVal sender As Object, ByVal e As RoutedEventArgs)
  viewModel.MoveCurrentToFirst()
End Sub

Private Sub prevButton_Click(ByVal sender As Object, ByVal e As RoutedEventArgs)
  viewModel.MoveCurrentToPrevious()
End Sub

Private Sub nextButton_Click(ByVal sender As Object, ByVal e As RoutedEventArgs)
  viewModel.MoveCurrentToNext()
End Sub

Private Sub lastButton_Click(ByVal sender As Object, ByVal e As RoutedEventArgs)
  viewModel.MoveCurrentToLast()
End Sub

Private Sub addButton_Click(ByVal sender As Object, ByVal e As RoutedEventArgs)
  viewModel.AddNew()
End Sub

Private Sub deleteButton_Click(ByVal sender As Object, ByVal e As RoutedEventArgs)
 If viewModel.CurrentPosition > -1 Then
    viewModel.RemoveAt(viewModel.CurrentPosition)
 End If
End Sub

As you can see, all we have to do is simply call the proper method on the view model to make the cursor move accordingly, add or delete employees.

Let’s run the application and test that everything is properly wired up. After a couple of seconds, you should be able to navigate through the list of employees, add a new employee or delete the current employee. The delay is due to the fact, that we are loading the employees collection asynchronously. Also notice that we are not yet adding or deleting employees to the database. We are simply manipulating the employees collection. We will have to add more logic to persist these operations to the database, but first, let’s add some visual feedback to the buttons.

Currently, all the buttons are enabled all the time even if one or more of the operations are not valid based on the current position of the cursor or while we are waiting for the employees collection to be initialized.

First we want all the buttons to be disabled out of the gate. We will enable them in the code when appropriate. Open MainWindow.xaml and switch to the XAML view.

Modify each button by adding the following attribute:

XAML
<Button IsEnabled="False"/>

Next right-click anywhere in the XAML view and select “View Code”. We want to add a method that enables/disables the buttons based on the current state of the view model. Let’s add the following code to the MainWindow class:

C#
private void EnableDisableButtons() {
  firstButton.IsEnabled = viewModel.CurrentPosition > 0;
  prevButton.IsEnabled = viewModel.CurrentPosition > 0;
  nextButton.IsEnabled = viewModel.CurrentPosition < viewModel.Employees.Count - 1;
  lastButton.IsEnabled = viewModel.CurrentPosition < viewModel.Employees.Count - 1;
  addButton.IsEnabled = viewModel.Employees != null;
  deleteButton.IsEnabled = viewModel.CurrentPosition > -1;
}
VB
Private Sub EnableDisableButtons()
  firstButton.IsEnabled = viewModel.CurrentPosition > 0
  prevButton.IsEnabled = viewModel.CurrentPosition > 0
  nextButton.IsEnabled = viewModel.CurrentPosition < viewModel.Employees.Count - 1
  lastButton.IsEnabled = viewModel.CurrentPosition < viewModel.Employees.Count - 1
  addButton.IsEnabled = viewModel.Employees IsNot Nothing
  deleteButton.IsEnabled = viewModel.CurrentPosition > -1
End Sub

Now, we need to figure out a way to call this method every time the state of the view model changes. Let’s think about this. How do we know the state of the view model changed? Well, this isn’t any different from how WPF determines if it needs to refresh the view. It’s the PropertyChanged event that gets raised every time CurrentEmployee or CurrentPosition changes. There’s one twist to it, though. The PropertyChanged event isn’t raised when the Employees Collection is modified, but not to worry. The collection being an ObservableCollecton raises its own event whenever the collection changes.

So, let’s install a couple of delegates that will take care of calling the above methods. Add the following two lines to the Window_Loaded event handler:

C#
viewModel.PropertyChanged += delegate { EnableDisableButtons(); };
viewModel.Employees.CollectionChanged += delegate { EnableDisableButtons(); };
VB
AddHandler viewModel.PropertyChanged, Sub() EnableDisableButtons()
AddHandler viewModel.Employees.CollectionChanged, Sub() EnableDisableButtons()

The completed Window_Loaded event handler now looks like this:

C#
private void Window_Loaded(object sender, RoutedEventArgs e) {
  viewModel = new MainWindowViewModel();
  DataContext = viewModel;
  viewModel.PropertyChanged += delegate { EnableDisableButtons(); };
  viewModel.Employees.CollectionChanged += delegate { EnableDisableButtons(); };
}
VB
Private Sub Window_Loaded(ByVal sender As Object, ByVal e As RoutedEventArgs)
  viewModel = New MainWindowViewModel()
  DataContext = viewModel
 AddHandler viewModel.PropertyChanged, Sub() EnableDisableButtons()
 AddHandler viewModel.Employees.CollectionChanged, Sub() EnableDisableButtons()
End Sub

Let’s run the application and make sure everything is working properly. The buttons should now be enabled and disabled accordingly and give the user visual feedback of the current state.

Adding and deleting records

The add and delete buttons allow us to add and delete records. Well, almost…

In our current implementation, when we add a new record or to delete a record, it is merely affecting our view model. The changes are not propagated to the database.

In fact, no changes that we make through the data form are being persisted to the database yet! We need to let DevForce's EntityManager know when the data form has changed something. Let's add a method to our view model that will tell the EntityManager to save any changes back to the database.

Open MainWindowViewModel.cs.

First, we need to promote our EntityManager from a local variable in the constructor to being a field on the class:

C#
private NorthwindIBEntities _mgr;

async void AsyncMainPageViewModel() {
  Employees = new ObservableCollection<Employee>();
  _mgr = new NorthwindIBEntities();
...
}
VB
Private _mgr As NorthwindIBEntities

Async Sub AsyncMainPageViewModel()
  Employees = New ObservableCollection(Of Employee)()
  _mgr = New NorthwindIBEntities()
...
End Sub

and now we can add the following:

C#
public void Save() {
  _mgr.SaveChanges();
}
VB
Public Sub Save()
  _mgr.SaveChanges()
End Sub

This tells the EntityManager to save any changes that we've made. We are using the synchronous version of the save operation for this tour. We could also save the changes asynchronously.  

We'd like to call Save() on our view model whenever the user clicks ok. Let's go back to the code-behind for MainWindow.xaml and add the following:

C#
private void okButton_Click(object sender, RoutedEventArgs e) {
  viewModel.Save();
}
VB
Private Sub okButton_Click(ByVal sender As Object, ByVal e As RoutedEventArgs)
  viewModel.Save()
End Sub

We can wire this handler to the ok button by adding this attribute to appropriate <Button /> element in the XAML:

XAML
Click="okButton_Click"

At this point we seem to have wired everything up, so that the user can click ok and the EntityManager will save all the changes to the database. We have one small problem, though. The ok button as of now is always disabled. If you remember, we added IsEnabled=”False” to every button. We need a way to enable the ok button and the cancel button for that matter, whenever the EntityManager has changes.

Fortunately, the EntityManager raises events for pretty much everything. In this case we are interested in the EntityChanged event. The EntityManager raises EntityChanged whenever an entity changed in a significant way. So far so good, but it is bad practice to expose the EntityManager to the view. The EntityManager is an implementation detail that should remain encapsulated inside the view model.

Instead, let’s add a property to our view model that keeps track of whether the EntityManager has changes and that raises the PropertyChanged event, so that the view can be notified whenever an Entity changed.

Open MainWindowViewModel.cs and add the following:

C#
private bool _hasChanges = false;

public bool HasChanges {
 get { return _hasChanges; }
 private set {
    _hasChanges = value;
    RaisePropertyChanged("HasChanges");
}
VB
Private _hasChanges As Boolean = False

Public Property HasChanges() As Boolean
 Get
   Return _hasChanges
 End Get
 Private Set(ByVal value As Boolean)
    _hasChanges = value
    RaisePropertyChanged("HasChanges")
 End Set

Next, we want to listen to the EnityChanged event and update the HasChanges property accordingly.

On the line after we instantiate the EntityManager in the constructor of MainWindowViewModel, add:

C#
_mgr.EntityChanged += (s, args) => HasChanges = ((NorthwindIBEntities)s).HasChanges();
VB
AddHandler _mgr.EntityChanged, Sub(s, args) HasChanges = (CType(s, NorthwindIBEntities)).HasChanges()

In this event handler, we ask the EntityManager if it has any changes and keep track of the result in our HasChanges property. The setter of the HasChanges property in turn raises the PropertyChanged event to let the view know that the buttons need to be updated. So, let’s add code to enable/disable the ok button. We will do the same for the cancel button, although we still have to provide the logic for when the cancel button is clicked.

Let’s go back to the code-behind of MainWindow.xaml and add the following to the EnableDisableButtons() method:

C#
okButton.IsEnabled = viewModel.HasChanges;
cancelButton.IsEnabled = viewModel.HasChanges;
VB
okButton.IsEnabled = viewModel.HasChanges
cancelButton.IsEnabled = viewModel.HasChanges

Now, any edits we make to employees will be persisted back to the database. However, we still need to handle adding and deleting employees.

Since the add and delete buttons are calling methods on our view model that are in turn adding and removing employees from the Employees property of our view model, we can take advantage of the fact that the collection raises events when it is changed.

On the line after we initialize Employees in the constructor for MainWindowViewModel, add:

C#
Employees.CollectionChanged += Employees_CollectionChanged;
VB
AddHandler Employees.CollectionChanged, AddressOf Employees_CollectionChanged;

Add these using statements if they are not present:

C#
using System.Collections.Specialized;
using System.Linq;
using System.Collections.Generic;
VB
Imports System.Collections.Specialized
Imports System.Linq
Imports System.Collections.Generic

Then add the following code for handling the event:

C#
private void Employees_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e){
 switch (e.Action) {
   case NotifyCollectionChangedAction.Add:
      Add(e.NewItems.Cast<Employee>());
     break;
   case NotifyCollectionChangedAction.Remove:
      Delete(e.OldItems.Cast<Employee>());
     break;
   }
}

private void Add(IEnumerable<Employee> employees) {
  employees.ForEach(_mgr.AddEntity);
}

private void Delete(IEnumerable<Employee> employees) {
  employees.ForEach(employee => employee.EntityAspect.Delete());
  Save();
}
VB
Private Sub Employees_CollectionChanged(ByVal sender As Object, ByVal e As NotifyCollectionChangedEventArgs)
Select Case e.Action
  Case AddressOf NotifyCollectionChangedAction.Add
     Add(e.NewItems.Cast(Of Employee)())
  Case NotifyCollectionChangedAction.Remove
     Delete(e.OldItems.Cast(Of Employee)())
End Select
End Sub

Private Sub Add(ByVal employees As IEnumerable(Of Employee))
  employees.ForEach(_mgr.AddEntity)
End Sub

Private Sub Delete(ByVal employees As IEnumerable(Of Employee))
  employees.ForEach(Function(employee) employee.EntityAspect.Delete())
  Save()
End Sub

Let's examine what we just added.

We can tell from the Action property on the event args if items are being added or removed. The event args also provide us with two collections: NewItems is a list of what was just added to the collection and OldItems is a list of the ones that were just removed. Neither list is strongly typed, so we have to explicitly cast them.
In order to make the logic easier to follow, we created the two supporting methods, Add() and Delete().

Add() iterates over the employees, passing them one at a time to AddEntity() on the Entity Manager. The next time SaveChanges() is called, these new employees will be inserted into the database.

Likewise, Delete() also iterates over the employees. However, we don't call a method on the EntityManager and pass in each employee. Instead, we call Delete() on the EntityAspect of each employee.

The EntityAspect property encapsulates most of the non-business properties and methods needed on an entity. Its entire reason for being is to remove clutter from the logical and intellisense listings of the Entity itself. Thus, on an Employee, you see business properties like Address, BirthDate, etc., and a handful of other properties, methods, and events for very standard operations (like GetType(), ToString(), and so forth). Members encapsulated under EntityAspect include things less regularly used, or relating to data access (rather than business) concerns.

There are few things worth noting about this code. First, even though our method deals with collections of items, the data form control will only ever add or remove one employee at a time. Second, an employee is put under the EntityManager’s control (via the _mgr.AddEntity operation) as soon as we click the “add” button. This means the EntityManager is aware of the new employee before we have edited any of its data. The new employee is also immediately shown in the data form control, and consequently the ok button logic we added earlier will handle the saving of the new employee. For removing an employee, on the other hand, we don’t want to have to click the ok button also and that is why we explicitly call Save() inside our Delete() method.

At this point we are almost done with our basic save functionality. We haven’t implemented the cancel operation, though. So, let’s add a method to our view model that will allow us to roll back the changes instead of saving them.

Open MainWindowViewModel.cs.

First, we need to promote our EntityQuery object from a local variable in the constructor to being a field on the class:

C#
private EntityQuery<Employee> _query;
public MainWindowViewModel() {
  Employees = new ObservableCollection<Employee>();
  Employees.CollectionChanged += Employees_CollectionChanged;
  _mgr = new NorthwindIBEntities();
  _mgr.EntityChanged += (s, args) => HasChanges = ((NorthwindIBEntities)s).HasChanges();
  _query = _mgr.Employees;
   var results = await _query.ExecuteAsync();
   results.ForEach(Employees.Add);
}
VB
Private _query As EntityQuery(Of Employee)
Public MainWindowViewModel()
  Employees = New ObservableCollection(Of Employee)()
  Employees.CollectionChanged += Employees_CollectionChanged
  _mgr = New NorthwindIBEntities()
 AddHandler _mgr.EntityChanged, Sub(s, args) HasChanges = (CType(s, NorthwindIBEntities)).HasChanges()
  _query = _mgr.Employees
 Dim results = Await _query.ExecuteAsync()
  results.ForEach(Sub(emp) Employees.Add(emp))End Sub

And now we can add our roll back method:

C#
public void RollbackChanges() {
 int privateCurrentPosition = CurrentPosition;
  _mgr.RejectChanges();
  Employees.Clear();
  _query.With(QueryStrategy.CacheOnly).Execute().ForEach(Employees.Add);
  CurrentPosition = privateCurrentPosition;
}
VB
Public Sub RollbackChanges()
 Dim privateCurrentPosition As Integer = CurrentPosition
  _mgr.RejectChanges()
  Employees.Clear()
  _query.With(QueryStrategy.CacheOnly).Execute().ForEach(Employees.Add)
  CurrentPosition = privateCurrentPosition
End Sub

Let’s examine the above code. To roll back our changes we are going to take advantage of the Entity Manager’s local cache. First, we are going to remember the current position of the cursor, so we can take the user back to where they were or as close as possible, depending on if the user added or deleted employees before deciding to cancel. Next, we are telling the Entity Manager to reject all changes. This will roll back all the changes the user made, including restoring deleted employees and getting rid of new employees. Lastly, we have to deal with the Employees Collection. The collection no longer contains employees that were deleted, but instead it contains new employees that were added, but we rolled back in the entity manager. So, let’s clear the collection and use the CacheOnly query strategy to rerun the query against the local cache and repopulate the collection. Once done, we reset the CurrentPosition to the position we remembered.

Now, all that’s left is wiring an event handler to the cancel button’s click event and calling the RollbackChanges() method.

Go back to the code-behind for MainWindow.xaml and add the following:

C#
private void cancelButton_Click(object sender, RoutedEventArgs e) {
  viewModel.RollbackChanges();
}
VB
Private Sub cancelButton_Click(ByVal sender As Object, ByVal e As RoutedEventArgs)
  viewModel.RollbackChanges()
End Sub

We can wire this handler to the cancel button by adding this attribute to appropriate <Button /> element in the XAML:

XAML
Click="cancelButton_Click" 

Master-detail

Now that we've worked out the basic functionality let's explore how to load and display "master-detail" or "parent-child" data.  Here we want to look at the Orders for each Employee.

Load child data on demand

Every employee has a set of orders associated with them. We can access these orders using the navigation property called Orders. Let's say that we want to display these orders in a data grid. First, let’s add a data grid back to MainWindow.xaml. We'll need to modify the layout to accommodate for this. We'll place the orders grid below the data form

Open MainWindow.xaml and in the XAML pane modify the <Grid.RowDefinition />. We’ll add a third row to hold the orders grid:

XAML
<Grid.RowDefinitions>
  <RowDefinition Height="Auto" />
  <RowDefinition Height="Auto" />
  <RowDefinition Height="Auto" />
</Grid.RowDefinitions>

Next, let’s add the grid that will display the orders:

XAML
<DataGrid AutoGenerateColumns="True" ItemsSource="{Binding Path=CurrentEmployee.Orders}" Height="100" style="width:500px" Grid.Row="2" />

The data grid is populated with orders and we didn't have to write any additional code!

Notice that DevForce will initiate the fetch of an Employee’s orders automatically when the Employee’s Orders navigation property is invoked.

CurrentEmployee.Orders refers to a different set of Orders for each Employee. Therefore, each time we navigate to a different Employee on our data form, DevForce must retrieve the appropriate set of Orders. The first time this retrieval takes place, the Orders must be obtained from the database. Because of that trip out to the database, you will notice a slight delay when navigating to a different employee before you see that Employee’s Orders in the data grid. However, if you move backward to an Employee previously viewed, you will see quick response, because once DevForce has retrieved the Orders into the local cache, it remembers that fact and subsequently retrieves them from there, with great speed.

Eager loading of child data

In many scenarios a developer can predict with fair accuracy when the user is going to need certain collections of data. (For example, in our application, it’s easy to figure out that a user viewing our Employee form is going to need the Orders associated with the various Employees.) In such a circumstance, it often makes sense to load the data for which the need is anticipated up front, in a single batch, rather than a little bit at a time. This is particularly true when displaying a datagrid whose rows incorporate data from related entities. In that circumstance, for each row displayed, additional trips to the server to fetch the related entities will be incurred unless the related data is pre-loaded.

Naturally such a pre-loading operation introduces some up-front delay while the data is being retrieved, but that delay is usually very small in comparison to the total delays that would otherwise occur while retrieving the data in small batches. Even more importantly, preloading the data ensures extremely crisp performance for all subsequent operations which use that data, since they can obtain it from the local cache, almost instantaneously.

Pre-loading related data is very simple to do. To load the related Orders at the same time we load the Employees, we simply add to our query a call to the extension method Include.

C#
_query = _mgr.Employees.Include("Orders");
VB
_query = _mgr.Employees.Include("Orders")

Now our form will take a small amount of extra time to load initially, but thereafter performs very snappily indeed.

Add the call to Include() to your copy of the view model, and notice the differences in both load time for the form and its subsequent performance. Do you like the trade-off?

Watch it work

Now let's add some simple logging to see what's happening as the query executes.  We'll add another ObservableCollection named Log to hold the messages our code will generate.

Open MainWindowViewModel.cs and add the following members:

C#
public ObservableCollection<string> Log { get; private set; }

// In the constructor, add:
Log = new ObservableCollection<string>();
WriteToLog("Initializing View Model");

private void WriteToLog(string message) {
  Log.Insert(0, message);
}
VB
Private privateLog As ObservableCollection(Of String)
Public Property Log() As ObservableCollection(Of String)
 Get
   Return privateLog
 End Get
 Private Set(ByVal value As ObservableCollection(Of String))
    privateLog = value
 End Set
End Property

' In the constructor, add:
Log = New ObservableCollection(Of String)()
WriteToLog("Initializing View Model")

private void WriteToLog(String message)
  Log.Insert(0, message)

Now we have a simple logging mechanism in place, let's modify the XAML so that we can see it.

Let's place the log at the bottom of the screen, under the data grid. Open MainWindow.xaml and add a new row in the RowDefinitions for main grid:

XAML
<RowDefinition Height="Auto"/>

We set the height to 'auto' so that the row will collapse to the smallest possible size. Next, below the data grid add:

XAML
<ItemsControl ItemsSource="{Binding Log}" Height="200" Grid.Row="3" />

We can run the application now, and see our first log message.

What we are really interested in, however, is seeing what sort of data access is occurring. We can listen on some of the events raised as the EntityManager retrieves data, in this case we'll listen on the Fetching event, which occurs before the query is executed.

Go back to MainWindowViewModel and inside the constructor, immediately after we instantiate the EntityManager, add the following statement:

C#
_mgr.Fetching += new EventHandler<EntityFetchingEventArgs>(_mgr_Fetching);
VB
AddHandler _mgr.Fetching, AddressOf _mgr_Fetching

Tip: you can get Visual Studio to complete the right-hand-side of the above statement by pressing the TAB key after typing the “+=”. A second press of TAB directs Visual Studio to write a stub version of the event handler itself:

C#
void _mgr_Fetching(object sender, EntityFetchingEventArgs e) {
 throw new NotImplementedException();
}
VB
Private Sub _mgr_Fetching(ByVal sender As Object, ByVal e As EntityFetchingEventArgs)
 Throw New NotImplementedException()
End Sub

Complete the handler by making it look as follows:

C#
void _mgr_Fetching(object sender, EntityFetchingEventArgs e) {
  WriteToLog("Fetching " + e.Query.ElementType);
}
VB
Private Sub _mgr_Fetching(ByVal sender As Object, ByVal e As EntityFetchingEventArgs)
  WriteToLog("Fetching " & e.Query.ElementType)
End Sub

The event handler can also be wired in a single statement using lambda-expression syntax. This syntax is more compact, but also a bit more abstruse. It is quite handy when you are familiar with the method signature for the event handler:

C#
_mgr.Fetching += (s,e) => WriteToLog("Fetching " + e.Query.ElementType);
VB
AddHandler _mgr.Fetching, Sub(s,e) WriteToLog("Fetching " & e.Query.ElementType)

Whenever Fetching is raised, we will now write out the CLR type of the results to our log.

output_html_692854bf.png

Performance

Now that we have some basic logging, we are making an interesting observation. Notice that the Entity Manager is fetching a lot of objects when we navigate from employee to employee. In particular, it is fetching the customer and the order details for every order in the grid. These are a lot of round trips to the server to fetch data that we actually don’t want to be displayed in the grid. It is an unfortunate side effect of letting the data grid auto-generate the columns for us. The auto-generation logic includes all the navigation properties, creating an unnecessary performance bottleneck.

In a real production application, one would explicitly define all the columns in XAML instead of using the auto-generation option. This is the preferred way to avoid this issue. For the purpose of this tour, we are going to leverage an event that the data grid raise, which gives us the opportunity to intervene during the column generation and tell the grid not to generate columns for the Customer and OrderDetails navigation properties.

Let’s go back to the code-behind of MainWindow.xaml and add the following event handler:

C#
private void OrdersGrid_AutoGeneratingColumn(object sender, DataGridAutoGeneratingColumnEventArgs e) {
  e.Cancel = e.PropertyName == "Customer" || e.PropertyName == "OrderDetails";
}
VB
Private Sub OrdersGrid_AutoGeneratingColumn(ByVal sender As Object, ByVal e As DataGridAutoGeneratingColumnEventArgs)
  e.Cancel = e.PropertyName = "Customer" OrElse e.PropertyName = "OrderDetails"
End Sub

We can wire this handler to the orders grid by adding this attribute in the XAML:

XAML
AutoGeneratingColumn="OrdersGrid_AutoGeneratingColumn"

Now run the application and notice that we are no longer fetching Customer and OrderDetail objects and the application should feel significantly faster especially if you are still using “.Include(“Orders”)” to pre-load the orders.

This concludes Part 2 of the tour of DevForce WPF.

Learn More

We've briefly covered a lot of topics here.  Here are some links to what we've covered, and additional information we hope you find useful.

Prerequisites

The user interface for the application built during this tour uses a BusyIndicator component supplied by the Extended WPF Toolkit. You can download the assembly from:

http://wpftoolkit.codeplex.com/

After downloading the assembly, place it anywhere in your solution directory and add a reference to your project. Because the assembly is coming from the Web, you may have to unlock it in order for Visual Studio to be able to open it. Right-click on the assembly in Windows Explorer and choose Properties. If the file is locked you will be presented with an Unlock button at the bottom of the General tab. Click the Unlock button to unlock the file.

Created by DevForce on September 29, 2010 16:21

This wiki is licensed under a Creative Commons 2.0 license. XWiki Enterprise 3.2 - Documentation. Copyright © 2020 IdeaBlade