Click here to Skip to main content
15,860,859 members
Articles / Desktop Programming / WPF

A LINQ Tutorial: WPF Data Binding with LINQ to SQL

Rate me:
Please Sign up or sign in to vote.
4.90/5 (47 votes)
11 Dec 2009CPOL9 min read 185.9K   9.3K   99   29
A tutorial and application on using WPF Data Binding with LINQ to SQL classes. This is part 3 of a three-part tutorial on using LINQ to SQL.

Note: Requires SQL Server Express 2008 and .NET 3.5 to run.

Book Catalog Application

Introduction

This is the final part of a three-part series on using LINQ to SQL:

These tutorials describe how to map your classes to your tables manually (rather than with an automated tool like SqlMetal) so that you can have support for M:M relationships and data binding against your entity classes. Even if you do choose to auto-generate your classes, understanding how these techniques work will allow you to expand the code to better fit your application's needs and to better troubleshoot issues when they arise.

The purpose of this final article is to complete the introduction to LINQ to SQL by showing how to make your LINQ to SQL classes work with WPF data bindings.

Getting Started

This article builds on top of: A LINQ Tutorial: Adding/Updating/Deleting Data, to add INotifyPropertyChanged events to your entity classes so they'll work with WPF's data binding. Please refer to the latest version of that article to see how the application has been setup.

Simple Data Binding

WPF data binding allows you to bind to any CLR object, including the classes you've mapped to your tables with LINQ to SQL. Let's start with a quick look at how it's used in the attached Book Catalog application.

The main window, BookCatalogBrowser.xaml, displays a list of book catalog items in a ListView named Listing (see the bottom of the file):

XML
<ListView Name="Listing" 
   ItemsSource="{Binding}" 
   HorizontalContentAlignment="Stretch"/> 

The DisplayList() method in its code-behind takes a list of (any type of) items and sets Listing's DataContext to that list:

C#
private void DisplayList( IEnumerable dataToList ) {
    Listing.DataContext = dataToList;
}

BookCatalogBrowser.xaml defines DataTemplates for each type (Book, Author, and Category) to define how they should be displayed. For example, here's part of the template for Category:

XML
<!-- How to display Category listings -->
<DataTemplate DataType="{x:Type LINQDemo:Category}">
  <Border Name="border" BorderBrush="ForestGreen" 
         BorderThickness="1" Padding="5" Margin="5">
    <StackPanel>
      <TextBlock Text="{Binding Path=Name}" 
           FontWeight="Bold" FontSize="14"/>
      <ListView ItemsSource="{Binding Path=Books}" 
           HorizontalContentAlignment="Stretch" BorderThickness="0" >
        <ListView.ItemTemplate>
          <DataTemplate>
            <TextBlock>
              <Hyperlink Click="LoadIndividualBook" 
                    CommandParameter="{Binding}" 
                    ToolTip="Display book details">
                <TextBlock Text="{Binding Path=Title}"/></Hyperlink>
...

This is how each Category instance within Listing's list will be displayed. The DataTemplate itself is selected based on the data type (DataType="{x:Type LINQDemo:Category}"), and everything within the template is bound to an individual Category instance.

It displays that Category's data via bindings:

  • The Category's name: {Binding Path=Name}
  • The Category's list of books (for a ListView): {Binding Path=Books}
  • Each book's title (for an individual book in the ListView): {Binding Path=Title}

This results in a list of categories, each one displaying their name and list of books:

Category Listing

Updating the Display When the Data Changes

This works great... until you try to update the data and it fails to get reflected in the UI. Even telling the data bindings to refresh fails to display any changes at all. Everything just seems broken.

The problem is that data binding requires the objects its bound to to provide notifications whenever they change. You can fix this by implementing the INotifyPropertyChanged interface on your classes.

Implementing INotifyPropertyChanged

In order for WPF data binding to automatically reflect data updates, the objects it binds to need to provide change notification to signal when their values have changed. The most common method for doing this is for your classes to implement the INotifyPropertyChanged interface to report changes to their data.

Implement Interface

In A LINQ Tutorial (Part 1), we created classes for Book, Author, and Category, and mapped them to their database tables.

Let's walk through adding the INotifyPropertyChanged interface to your classes using Book as an example.

1. Add the INotifyPropertyChanged interface to the class declaration

C#
[Table( Name = "Books" )]
public class Book : INotifyPropertyChanged

2. Add a public PropertyChangedEventHandler that callers can use to register for change notifications

C#
public event PropertyChangedEventHandler PropertyChanged;

3. Add the OnPropertyChanged method to notify callers of changes

This method will check to see if there is a PropertyChanged delegate and, if so, invokes that delegate, passing it the name of the field that has changed.

C#
private void OnPropertyChanged( string name ) {
    if( PropertyChanged != null ) {
        PropertyChanged( this, new PropertyChangedEventArgs( name ) );
    }
}

Do this for each of your public entity classes (in our example: Book, Author, and Category).

You can skip the M:M Join classes as they're not part of your public interface, so you're probably not binding to them.

Call OnPropetyChanged() for Each Public [Column] Attribute

These are the public properties with a [Column] attribute, mapping them directly to a database column.

The [Column] public properties in Book are:

  • Title
  • Price

Call OnPropertyChanged from set() just after you set the value, passing it the name of the property that changed.

Always be sure to call OnPropetyChanged() after you've set the field. Otherwise, the caller will get the alert and check the field for its value before you've had a chance to update it.

If you've been using automatic properties as we have, you'll sadly need to split them into backing field + property:

C#
private string _title;
[Column] public string Title {
    get { return _title; }
    set {
        _title = value;
        OnPropertyChanged( "Title" );
    }
}

private decimal _price;
[Column] public decimal Price {
    get { return _price; }
    set {
        _price = value;
        OnPropertyChanged( "Price" );
    }
}

Do this for all your public [Column] attributes. In BookCatalog, this would be:

  • Author: Name
  • Category: Name

If you have a public Id that's an identity column, you should be able to skip that since, by definition, it won't change for a given instance.

Call OnPropetyChanged() for Each Public Single Reference (1:M) [Association] Attribute

This is the singleton side of any 1:M relationship. For example, Book holds a single Category.

Category:Books M:1 Association

Call OnPropertyChanged() just after setting your EntityRef backing field to its new value.

For example, in our Book class' Category property, call it right after setting _category.Entity:

C#
public Category Category {
    ...
    set {
            ...

            // set category to the new value
            _category.Entity = newCategory;
            OnPropertyChanged( "Category" );

...

Call OnPropertyChanged() for Each Public Collection (M:1 and M:M) [Association] Attribute

Collection associations (e.g., Book.Categories and Book.Authors) need to do two things:

  1. Return a collection that implements INotifyCollectionChanged.
  2. Call OnPropertyChanged() whenever the collection changes.

Step #1 is used by WPF when it binds to the collection to determine what has changed. For example, the list of Category.Books we bind to when displaying a Category's data.

Step #2 is used when WPF binds to your object to determine when it changes. For example, if Category.Name changes.

M:M Collection of Reference Associations

Books:Authors M:M Association

In A LINQ Tutorial (Part 2), we set up our M:M public classes (e.g., Book and Author) to return an ObservableCollection, which already implements INotifyCollectionChanged, so Step #1 is already done for Book (as shown below) and for Author:

C#
public class Book : INotifyPropertyChanged
{
    ...

    public ICollection Authors {
        get {
          var authors = new ObservableCollection( from ba in BookAuthors select ba.Author );
          authors.CollectionChanged += AuthorCollectionChanged;
          return authors;
        }
    }

When you create the collection, you register to receive notifications for it (authors.CollectionChanged += AuthorCollectionChanged). This will call your AuthorCollectionChanged method whenever the collection is changed.

Update AuthorCollectionChanged to call OnPropertyChanged() at the end:

C#
private void AuthorCollectionChanged( object sender, NotifyCollectionChangedEventArgs e ) {
    if( NotifyCollectionChangedAction.Add == e.Action ) {
        foreach( Author addedAuthor in e.NewItems ) {
            OnAuthorAdded( addedAuthor );
        }
    }

    if( NotifyCollectionChangedAction.Remove == e.Action ) {
        foreach( Author removedAuthor in e.OldItems ) {
            OnAuthorRemoved( removedAuthor );
        }
    }
    
   // Call OnPropertyChanged() after updating Authors
    OnPropertyChanged( "Authors" );
}

And then, mirror these changes on the other side of the M:M relationship (Author.Books).

M:1 Collection of Reference Associations

That leaves our M:1 collections - such as Category.Books - which we have not yet wrapped in an ObservableCollection.

So do that now: in Category, update the get() for Books to return an ObservableCollection just as you did for Book.Authors:

C#
public class Category : INotifyPropertyChanged
{
    ...
    public ICollection Books {
        get {
            var books = new ObservableCollection<Book>( _books );
            books.CollectionChanged += BookCollectionChanged;
            return books;
        }

And, update its set() to call OnPropertyChanged() after assigning the value:

C#
   set {
        _books.Assign( value );
        OnPropertyChanged( "Books" );
    }
}

In A LINQ Tutorial (Part 2), we created two delegate methods: OnBookAdded and OnBookRemoved, that handle synchronization whenever a Category's books change.

Create your BookCollectionChanged method. Have it invoke OnBookAdded and OnBookRemoved, and call OnPropertyChanged("Books") at the end:

C#
private void BookCollectionChanged( object sender, NotifyCollectionChangedEventArgs e ) {
    if( NotifyCollectionChangedAction.Add == e.Action ) {
        foreach( Book addedBook in e.NewItems ) {
            OnBookAdded( addedBook );
        }
    }

    if( NotifyCollectionChangedAction.Remove == e.Action ) {
        foreach( Book removedBook in e.OldItems ) {
            OnBookRemoved( removedBook );
        }
    }
    OnPropertyChanged( "Books" );
}

Since you wrapped your backing field (_books), you need to handle updating it whenever you receive a change notification.

Update your OnBookAdded and OnBookRemoved methods to update the underlying _books collection accordingly:

C#
private void OnBookAdded( Book addedBook ) {
    _books.Add( addedBook );
    addedBook.Category = this;
}

private void OnBookRemoved( Book removedBook ) {
    _books.Remove( removedBook );
    removedBook.Category = null;
}

Finally, since BookCollectionChanged is now invoking OnBookAdded and OnBookRemoved, it would be redundant (and recursive!) to also invoke it from _books.

Remove the Action arguments from your new EntitySet constructor call:

C#
public Category( ){
    _books = new EntitySet<Book>( <del>OnBookAdded, OnBookRemoved )</del>;
}

Updating the Display When the Data Changes: Once More With Feeling

The attached BookCatalog application includes an EditDetails.xaml UserControl to allow editing any of the LINQ data types.

I'm sure I got entirely too clever for my own good here in trying to handle Books, and Authors and Categories using the same set of methods and UserControls - so I'm not suggesting you model your designs after what is here. :-) But I hope it serves its purpose by providing an example of how you can data bind to LINQ to SQL classes and make sure all of the data is synchronized throughout your application.

In EditDetails.xaml.cs, there is a BindDataToEditForm() that sets up the binding from the UserControl to the dataItem to edit (this could be a Book, Author, or Category):

C#
private void BindDataToEditForm( ) {
    Binding binding = new Binding( );
    binding.Source = dataItem;
    binding.Mode = BindingMode.OneWay;
    binding.UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged;
    EditForm.SetBinding( DataContextProperty, binding );
}

Like the main window, EditDetails.xaml uses DataTemplates to determine how to display the edit form. For example, here's part of the template for Category:

C#
<!-- How to display Category details -->
<DataTemplate DataType="{x:Type LINQDemo:Category}">
  ...
  <Border Name="border" BorderBrush="ForestGreen" 
         BorderThickness="1" Padding="10" 
         DockPanel.Dock="Right">
    <StackPanel>
      <DockPanel>
        <TextBlock FontWeight="Bold" 
            DockPanel.Dock="Left" 
            VerticalAlignment="Center">Name:</TextBlock>
        <TextBox Text="{Binding Path=Name}" Margin="5 0" 
            VerticalAlignment="Center" DockPanel.Dock="Right"/>
      </DockPanel>
...

This is similar to the main window except it uses TextBoxes instead of TextBlocks to display the data, such as {Binding Path=Name}.

The difference is that since Category now implements INotifyPropertyChanged, when you edit Name in the UI, it automatically updates the underlying Category instance.

Edit Category

The Save button calls SaveDetails(), which calls SubmitChanges() on our DataContext:

C#
private void SaveDetails( object sender, RoutedEventArgs e ) {
    BookCatalog.SubmitChanges();
    CloseDialog();
}

The Cancel button calls CancelUpdate() which:

  1. Does not submit changes on the DataContext, so those changes will be discarded as a new DataContext (BookCatalog) instance is created each time you open EditDetails.
  2. Does call the CancelChanges() method we added to BookCatalog in A LINQ Tutorial (Part 2) to cancel any pending changes to our M:M Join tables.
C#
private void CancelUpdate( object sender, RoutedEventArgs e ){
    BookCatalog.CancelChanges( );
    CloseDialog( false );
}

When the dialog is closed, the main window, BookCatalogBrowser.xaml, gets a fresh DataContext instance to refresh its listing to pick up any changes that you might have saved.

A Known Issue

I discovered the following issue occurs if you choose to use a separate DataContext to delete your M:M Join records:

If you delete and then re-add the same M:M relationship within the same "transaction", you'll get a DuplicateKeyException.

For example, if you edit a Book and first remove an author and then re-add that same author before calling SubmitChanges():

C#
BookCatalog bookCatalog = new BookCatalog( );
Book xpExplained = bookCatalog.Books.Single( 
  book => book.Title.Contains("Extreme Programming Explained") );
Author kentBeck = bookCatalog.Authors.Single( author => author.Name == "Kent Beck" );


xpExplained.Authors.Remove( kentBeck );
xpExplained.Authors.Add( kentBeck );

// This will throw a DuplicateKeyException
bookCatalog.SubmitChanges();

One way to handle this is to prevent that scenario from occurring -- don't allow a removed relationship to be re-added until after you call SubmitChanges() to persist the deletion. Then, you're free to add it back without error.

The Book Catalog application does this. If you open a Book to edit and delete one of its authors - you simply aren't given the choice to re-add that author until you click the Save button. The same is true on the other side when editing an Author to change which Books they have.

Note that this is only the case for M:M Join records that you remove with a separate DataContext, as described in A LINQ Tutorial (Part 2). There are no limitations to, for example, removing and then re-adding the same book to a Category since this is a M:1 (rather than M:M) relationship.

A Note on the Design

I purposefully chose to provide the View with direct access to the entity classes so that it would be as clear as possible of an example for how the bindings and DataContexts work in an application.

Obviously, in a real application, you'll want to put a layer between your view and the model. You can put your business logic there. You can also hide the details of working with the DataContext (e.g., when to refresh, when to call SubmitChanges()) there, so the view doesn't have to understand anything about LINQ to SQL.

The Model-View-ViewModel pattern is a great way to handle this. See Sacha Barber's MVVM Tutorials here on CodeProject.

Thank You!

If you read this far, wow, you should totally get yourself some of that new CodeProject reputation for that. Just sayin'... :-) Thanks for reading.

History

  • 12/09/2009: Initial version.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer Microsoft
United States United States
Abby Fichtner is a Microsoft Developer Evangelist and author of The Hacker Chick Blog.

She's been developing custom software applications, wearing every hat imaginable, since 1994. Although, technically, she got her start at the age of 8 when her father brought home an Atari 800. In the evenings, they would sit together and type in the machine code from the Atari magazines – because that was the way serious geeks got their computer games!

Today, she works for Microsoft as a Developer Evangelist to the startup community - helping them to create the next generation of software.

Comments and Discussions

 
QuestionExcellent Pin
icristut1-Sep-18 5:12
icristut1-Sep-18 5:12 
General5 / 5 Pin
Member 1162054919-Apr-15 14:33
Member 1162054919-Apr-15 14:33 
GeneralMy vote of 5 Pin
Erol Esen30-Aug-13 14:10
Erol Esen30-Aug-13 14:10 
Questionthrowing DuplicateKeyException Pin
Don Dausi12-May-13 20:51
Don Dausi12-May-13 20:51 
GeneralMy vote of 5 Pin
CelPlusPlus3-May-13 4:10
CelPlusPlus3-May-13 4:10 
QuestionGood tutorial on Linq to SQL Pin
Member 985116021-Feb-13 9:21
Member 985116021-Feb-13 9:21 
GeneralMy vote of 5 Pin
Member 985116021-Feb-13 9:16
Member 985116021-Feb-13 9:16 
GeneralMy vote of 5 Pin
Josh Hawley2-Jan-13 10:14
Josh Hawley2-Jan-13 10:14 
GeneralMy vote of 5 Pin
Vipin_Arora28-Apr-12 6:45
Vipin_Arora28-Apr-12 6:45 
QuestionGreat Pin
Member 867889326-Feb-12 5:57
Member 867889326-Feb-12 5:57 
QuestionMy vote of 5 Pin
Abinash Bishoyi14-Nov-11 5:52
Abinash Bishoyi14-Nov-11 5:52 
QuestionMulti-user environment? Pin
WPFNub30-Jun-11 19:08
WPFNub30-Jun-11 19:08 
GeneralFantastic Article! Question on Association Tables though Pin
workindeveloper13-Mar-11 19:16
workindeveloper13-Mar-11 19:16 
GeneralMy vote of 5 Pin
AminMaredia8-Feb-11 19:58
AminMaredia8-Feb-11 19:58 
GeneralDear Pin
boobalan6-Jan-11 17:16
boobalan6-Jan-11 17:16 
GeneralRe: Dear Pin
Abby Fichtner (Hacker Chick)7-Jan-11 2:16
Abby Fichtner (Hacker Chick)7-Jan-11 2:16 
GeneralMy vote of 5 Pin
boobalan6-Jan-11 17:14
boobalan6-Jan-11 17:14 
QuestionGreat article - where next? Pin
John Stines5-Dec-10 3:34
John Stines5-Dec-10 3:34 
GeneralDear Abby :) Pin
Member 32645628-Apr-10 10:10
Member 32645628-Apr-10 10:10 
GeneralRe: Dear Abby :) Pin
Abby Fichtner (Hacker Chick)30-Apr-10 3:37
Abby Fichtner (Hacker Chick)30-Apr-10 3:37 
QuestionI used O/R designer - how do i get notified when data changes? Pin
Anna Tran26-Mar-10 12:41
Anna Tran26-Mar-10 12:41 
GeneralCertainly helped me Pin
RugbyLeague15-Dec-09 0:22
RugbyLeague15-Dec-09 0:22 
GeneralRe: Certainly helped me Pin
Abby Fichtner (Hacker Chick)15-Dec-09 13:24
Abby Fichtner (Hacker Chick)15-Dec-09 13:24 
GeneralGreat example Pin
Stephen Trinder14-Dec-09 16:36
Stephen Trinder14-Dec-09 16:36 
GeneralRe: Great example Pin
Abby Fichtner (Hacker Chick)14-Dec-09 17:50
Abby Fichtner (Hacker Chick)14-Dec-09 17:50 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.