Introduction
With this article, I want to show some basic techniques to achieve 2-way databinding in a WPF/XBAP application.
I do not intend to clarify "huge" architect principles like NTier separation into DataLogic, BusinessLogic and UILogic (as you should be aware of as a professional developer ...), but I wanted to make things in this article as simple as possible and focus on the basic "DataBinding" principles in what may be needed by a simple XBAP/WPF application.
In this article, I will demonstrate the creation of a simple administration program for the "Category
" table of the NorthWind database. The purpose of this small XBAP/WPF program is to visualize the categories, and let the user add/delete or modify category objects. Finally, I've added some sorting and filtering possibilities.
Background
The reader is supposed to have some background knowledge about databinding in WinForms, connecting to a SQL-Server dataBase and a brief knowledge of XAML.
Using the Code
1. The Model
First of all, we have our "Model
" class, which in this case is an ADO.NET EntityFramework Entity Data Model, just containing our Categories
class, as needed by the example.
To add some custom behaviour, I've created an override of the "Categories
" partial class:
public partial class Categories
{
private bool _isNew;
public bool IsNew
{
get { return _isNew; }
set { _isNew = value; }
}
protected override void OnPropertyChanged(string property)
{
base.OnPropertyChanged(property);
}
private Categories()
{
}
public static Categories Create()
{
Categories c = new Categories();
Bitmap emptyBitMap = WpfBrowserApplication1.Properties.Resources.Empty;
MemoryStream ms = new MemoryStream();
emptyBitMap.Save(ms, ImageFormat.Bmp);
byte[] buffer = ms.ToArray();
c.CategoryName = "[Enter Categoryname]";
c.IsNew = true;
c.Picture = buffer;
return c;
}
}
It's a good habit to foresee a custom method for object creating, other than letting the programmer use the standard new object()
method. For this reason, I've put the base class constructor as private
and provided a Create()
method to add a new category. As you can see from the code, I've added some custom behaviour, like creating an empty image
for each newly created category. I've also added the _isNew
property, this is needed by the persistence code, when we're going to persist the newly added object to the database.
2. Userinterface Storage Properties
private static NorthwindEntities nwEntities = new NorthwindEntities();
private List<categories> _categoryList;
private List<categories> _deletedCategoryList;
private CollectionView _view;
private SortDescription _sortByCategoryName;
private SortDescription _sortByDescription;
Within our startup page (Page1.xaml), we have to initialize our data access object, namely a static reference to our NorthWindEntities
, next we need some lists of type Categories
to hold track of our loaded and during the session deleted Categories
objects, and finally we need a CollectionView
which makes a sorted and/or filtered view of our binded data.
3. Data Loading and Binding
private void Page_Loaded(object sender, RoutedEventArgs e)
{
this._categoryList = nwEntities.Categories.ToList();
this.DataContext = _categoryList;
this._view = (CollectionView)CollectionViewSource.GetDefaultView(_categoryList);
this._sortByCategoryName = new SortDescription
("CategoryName", ListSortDirection.Ascending);
this._sortByDescription = new SortDescription
("Description", ListSortDirection.Ascending);
this._view.CurrentChanged += new EventHandler(_view_CurrentChanged);
this.SetCurrentItemPointer();
}
First, we're going to load our categories into our _categoryList
collection. Next we set the DataContext
of our Page to our categories collection and create a view to our data. This view points to the default view of our collection, and in this way, navigation, sorting and filtering our collection is made possible. We also create some sorting properties that can be used to create a sorted view of our data. Finally, if something changes in our collection, like a user is navigation or filtering, or sorting, we want the UI to be updated.
4. Navigating
private void btnFirst_Click(object sender, RoutedEventArgs e)
{
this._view.MoveCurrentToFirst();
}
private void btnPrev_Click(object sender, RoutedEventArgs e)
{
if (this._view.CurrentPosition > 0)
this._view.MoveCurrentToPrevious();
}
private void btnNext_Click(object sender, RoutedEventArgs e)
{
if (this._view.CurrentPosition < this._view.Count - 1)
this._view.MoveCurrentToNext();
}
private void btnLast_Click(object sender, RoutedEventArgs e)
{
this._view.MoveCurrentToLast();
}
void _view_CurrentChanged(object sender, EventArgs e)
{
this.SetCurrentItemPointer();
}
private void SetCurrentItemPointer()
{
string currentPosition = Convert.ToString(_view.CurrentPosition + 1);
this.textBlockCurrentItem.Text = currentPosition;
this.textBlockTotalItems.Text = _view.Count.ToString();
}
Navigating our data, thus moving from one category to another ... , is a common behaviour of our application. I've coded the First, Prev, Next and Last buttons to point our data to the first, previous, next or last category of our list. Also notice that the CurrentChanged
handler we created on our view will be triggered each time the user "hits" one of the previous buttons. This notification will update our recordpointer display in the UI.
5. Sorting and Filtering
private void btnSortByCategoryName_Click(object sender, RoutedEventArgs e)
{
this._view.SortDescriptions.Clear();
this._view.SortDescriptions.Add(this._sortByCategoryName);
this._view.MoveCurrentToFirst();
}
private void btnSortByDescription_Click(object sender, RoutedEventArgs e)
{
this._view.SortDescriptions.Clear();
this._view.SortDescriptions.Add(this._sortByDescription);
this._view.MoveCurrentToFirst();
}
private void btnResetSort_Click(object sender, RoutedEventArgs e)
{
this._view.SortDescriptions.Clear();
this._view.MoveCurrentToFirst();
}
private void _searchBar_TextChanged(object sender, TextChangedEventArgs e)
{
TextBox source = e.OriginalSource as TextBox;
if (source == null)
return;
switch (source.Name)
{
case "_searchText":
FilterCategory predicate = new FilterCategory(source.Text);
this._view.Filter = predicate.Match;
if (this._view.Count > 0)
this._view.MoveCurrentToFirst();
break;
}
}
public class FilterCategory
{
private string _filter;
public FilterCategory(string p_filter)
{
_filter = p_filter;
}
public Predicate[object] Match
{
get { return Contains; }
}
public bool Contains(object p_category)
{
Categories category = p_category as Categories;
if (_filter == string.Empty)
{
return (category.CategoryName != string.Empty);
}
return (category.CategoryName.Contains(_filter));
}
}
By means of the sort properties added in the beginning, sorting a CollectionView
is quite easy to achieve. I've also added a simple filter, namely CategoryName
. The filter code is a bit more of a challenge. In this case, I've created a filter class filtering the objectlist on the searchtext as supplied by the user and next sets the record pointer to the first object in the list and refreshes the UI.
6. Adding a New Category
private void btnAdd_Click(object sender, RoutedEventArgs e)
{
this._categoryList.Add(Categories.Create());
this._view.MoveCurrentToLast();
}
Creating a new Category
and adding it to the collectionlist is, as you can see, quite a simple process!
7. Deleting a Category
private void btnDelete_Click(object sender, RoutedEventArgs e)
{
if (_view.CurrentPosition > -1)
{
Categories c = (Categories)_view.CurrentItem;
if (c != null)
{
if (this._deletedCategoryList == null)
{
this._deletedCategoryList = new List[Categories]();
}
this._deletedCategoryList.Add(c);
int currentPosition = (this._view.CurrentPosition - 1);
this._categoryList.Remove(c);
this._view.Refresh();
if (currentPosition > -1)
{
this._view.MoveCurrentToPosition(currentPosition);
}
}
}
}
First we get the current item for our view and add it to the deleted items list (needed for persistence -> see later). Next we remove the object from our current list and position to the next record if any.
8. Persisting our New, Modified and Deleted Objects
private void btnSave_Click(object sender, RoutedEventArgs e)
{
IEnumerable[Categories] newCategories =
this._categoryList.Where(cat => cat.IsNew == true);
foreach (Categories newCategory in newCategories)
{
nwEntities.AddToCategories(newCategory);
newCategory.IsNew = false;
}
if (this._deletedCategoryList != null)
{
foreach (Categories deleteCategory in this._deletedCategoryList)
{
nwEntities.DeleteObject(deleteCategory);
}
}
nwEntities.SaveChanges();
if(this._deletedCategoryList != null)
this._deletedCategoryList.Clear();
}
Data persistency for saving the data ... is quite an easy process in EF! All SQL-Related Commands, Adapters, Connections parameters and so on, are hidden for the user, so quite readable code can be written to persist our data. The comments in the code above are self-explanatory, I think!
9. Final Words
So, thanks for reading! Hope you enjoyed it. And of course ... once more, in a professional environment, certainly in case of large projects, you should separate your code in layers, abstracting data-access in a DataLayer, UI binding and validation though a BusinessLayer, and so on ... but for educational reasons, I just kept this application as simple as possible!
History
- 13th March, 2009: Initial version