Background
Having just read Sacha Barber's article WPF: A Beginners Guide - Part 5 of N I wondered how to get Binding to work where the source of data was an actual database rather than an XML file. Sacha mentioned that his current favourite method involves LINQ-SQL and ObservableCollections of records. So I decided to write a tiny demo to show what was involved. In doing so I found one or two little things which may be of interest to others.
Introduction
Sacha's article uses a very simple collection of People, where each Person has a PersonName and Id. My question was how to use a Database Table People where each Row represents a Person and stores a PersonId and a PersonName. In producing the App using Visual Studio 2008 C# to try this out I had to first use DataBase Explorer to define the database and initial table data for testing, and then add the LINQ-SQL class using Add Item.. from the Solution Explorer. Finally I created a simple ObservableCollection Class and added that to the solution.
I kept the UI down to a very simple listBox to display the People, a Field to allow new people to be added, a field to edit a selected Person and a couple of buttons to add, and delete people from the database, as well as a button to Commit changes back the persistance store of the database.
The DataBase Table
Created by adding a Service Based Database to the Solution using Add Item.. in Solution Explorer and then using DataBase Explorer. I added the new table People with two Fields, PersonID as an int and PersonName as nvchar(50) and set the PersonId Fields IsIdentity property to yes. I made a mistake the first time around here in not setting the field as the Primary Key and left the table without a Primary Key defined. This effected the LINQ-SQL class generated when I later added that to the solution.
The Linq-SQL Class
Using Solution Explorer add Item.. and adding a LINQ-SQL class is simple. A designer pane opens onto what I drag to the People Table from the Database Explorer. I then close that window and am prompted for a name to call the class I had just created, which creates the DataClasses1DataContext class. Wondering what I had just created I took a look through the DataClasses1DataContext.designer.cs file and spotted that the People class didn't have :INotifyPropertyChanged defined on the class, which as you will spot in Sacha's article is quite important. Later when I tried to add a couple of rows of test data to the database using Database Explorer I got an error saying I couldn't insert into the table because it didn't have a Primary Key. I changed the DB and deleted the DataContext class from the solution and re-created the LINQ-SQL class, and this time the :INotifyPropertyChanged was added to the class.
The ObservableCollection
The LINQ-SQL DataContext object allows me to access the table and extract from that a number of People objects. To make this visible I want to put them into a collection which can be used by a listbox as its itemsSource, and an ObservableCollection<People> will allow Binding to work on the listbox items. So I added a C# class to the solution. I gave it a constructor which will load it up with the People from DataClasses1DataContext when an instance is created.
public class ObservablePeople: ObservableCollection<People>
{
public ObservablePeople(DataClasses1DataContext dataDc)
{
foreach ( People thisPerson in dataDc.Peoples)
{
this.Add(thisPerson);
}
}
}
The WPF C# Code Behind
I gave the WPF window some properties to access and use the database and the collection of People, namely _dataDC a DataClasses1DataContext, and _knownPeople an ObservablePeople, and I added a Window_Loaded method as a convenient place to load the _knownPeople from the _dataDC and Bind it to the listbox.
Adding New Records to the Database
The click handler for the Add button takes the name entered in textBox1 and creates a new People instance with the PersonName set. Adding this to the LINQ-SQL DataContext via the InsertOnSubmit method of the Peoples table works OK, EXCEPT does not result in any change to the UI. This is because the ObservablePeople collection is not automatically updated when the DataContext gets additional content. I had to manually code to insert it to _knownPeople in order for it to be visible. Which led to the question of how was the new record working correctly in the DataBinding in the XAML? The answer is that the code places the same object reference into both the DataContext and the ObservableCollection. If I had, in a more complex coding, taken a COPY of an objects data from the datacontext and created a new Object and added that to the ObservableCollection then the new object could be changed, and the object in the DataContext would not be updated and Binding would not update the database. For example, the classic mistake would be to pass the object into a Helper Method as a value parameter instead of passing by reference.
Editing Selected People
This is accomplished completely in XAML, textBox2 has a Binding
Text="{Binding ElementName=listBox1, Path=SelectedItem.PersonName,
UpdateSourceTrigger=PropertyChanged}"
which Binds to the PersonName of the Selected Item in the listbox's ItemsSource _knownPeople, and no C# code is required.
Committing the Changes to Database
All the changes made have had an effect on the objects in the _dataDC datacontext object, and have not changed the database table at all. So the commit button click handler is used to call the _dataDc.SubmitChanges() method to update the database. In the debugging process this led to an observation. Whenever I ran the App and committed some changes and exited the App, and then changed the code, as with the Delete click, the changes to the database disappeared when I next started to debug. However, if I exited and made no code changes and started debugging again the database changes were in fact visible on start up. I've since seen this as a question in a forum and so it is something to remember when you do your first DB app and are wondering if your DB actually changed when you start debugging it.
The Window Code
Collapse
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace WpfDbLinqBind1
{
public partial class Window1 : Window
{
private static DataClasses1DataContext _dataDC = new DataClasses1DataContext();
private ObservablePeople _knownPeople;
public Window1()
{
InitializeComponent();
}
private void Window_Loaded(object sender, RoutedEventArgs e)
{
_knownPeople = new ObservablePeople(_dataDC);
this.listBox1.ItemsSource = _knownPeople;
}
private void add_Click(object sender, RoutedEventArgs e)
{
if (textBox1.Text.Length > 0 )
{
People newPerson = new People();
newPerson.PersonName = textBox1.Text;
_dataDC.Peoples.InsertOnSubmit(newPerson);
_knownPeople.Add(newPerson);
textBox1.Text = "";
}
}
private void commit_Click(object sender, RoutedEventArgs e)
{
_dataDC.SubmitChanges();
}
private void Delete_Click(object sender, RoutedEventArgs e)
{
if (listBox1.SelectedItem != null)
{
_dataDC.Peoples.DeleteOnSubmit( (People)listBox1.SelectedItem);
_knownPeople.Remove((People)listBox1.SelectedItem);
}
}
}
}
XAML Notes and Queries
The listbox contains items with 2 datafields and so the XAML uses a DataTemplate to format it into something the listbox can dislay
<DataTemplate x:Key="pName">
-->
<TextBlock Text="{Binding Path=PersonName}"/>
</DataTemplate>
and the listbox sets the
ItemTemplate="{StaticResource pName}"
to use it.
and it is convenient that the edit textbox can use the dotted syntax "SelectedItem.PersonName" to bind to, because a textblock does not have any kind of a handy data Template attribute to convert a People to text.
Points of Interest
- When adding a LINQ-SQL Classes to your project via Add Item.. in Solution Explorer make sure your database has the Primary Key field defined in each table you need to bind to. Otherwise the class created by the wizard won't have
INotifyPropertyChanged declared, which can make binding a bit of a problem.
- When running in debug mode in VS2008 any rebuild resets the DB back to the starting test data entered in database explorer. Exit the App and re-start debugging without changing any code and you see the changes are persistant in the DB, and are lost as soon as you re-compile. Obviously not a problem in the release build. Remember to use database explorer to remove the test data before the final build unless you want it released.
- Adding new records to the LINQ-SQL DataContext object doesnt automatically update your ObservableCollection, nor vice-versa.
- When you add an object to the LINQ-SQL DataContext object, add the same object to your ObservableCollection, do not add a COPY of the object, it must be the same object reference.
- After the wizard has created your LINQ-SQL DataContext class you can use ClassView to find out what it has called the table property to access the table.