Table of Contents
- Introduction
- Pre-requisites
- Running the Sample Application
- ESN Business Layer Setup
- Developing the ESN User Interface
- Viewing Data from Business Entities
- The Editor Controls
- Handling Many-to-Many Relationships
- The EntitySpaces Collection Confusion
- Saving and Stuff
- Conclusion
Developing an effective business layer is possibly the most difficult task of any database-driven application. Sure, there are challenges in everything, but the business layer is like the mutual friend of two people who hate each other: it's the thing that everyone leans on to tie things together and keep peace and harmony. The business layer interface needs to represent the database in a concise, effective, and flexible manner. The business layer also has to behave consistently when dealing with a combination of entities from the database and freshly-created entities in memory (without DB-assigned IDs/defaults, or calculated columns). As databinding methods between UI technologies and business layer technologies get more complex, it can be very painful if you choose two technologies that do not work well together.
Early on in my team's project, we were leaning pretty heavily towards Windows Presentation Foundation (WPF) and XAML to create our user interface. In order to continue with WPF/XAML, we needed to find a business layer framework that worked well with its databinding methodology. My first pick was, dOOdads. It quickly became apparent that d00dads did not work well with XAML, for reasons that aren't worth going into. After examining a few other frameworks, we did a lot of work with the EntitySpaces (ES) trial version, and found that it worked pretty well with XAML. Here are some of the reasons we liked ES:
- Rich & Intuitive - EntitySpaces does what you want, how you want it; almost everything I want is there, and it's easy to use
- Dynamic Queries - rather than using custom stored procedures or writing ad-hoc queries, ES provides a way to create dynamic, type-safe queries in code
- Simple code generation - regenerating all business objects is simple, and uses partial classes so generated code does not overlap with user code
- Hierarchical Properties - in addition to foreign key ID properties, typed reference properties are generated for objects referenced by foreign keys
- Nullable Properties - generated properties use nullable types so we can tell the difference between
null
and empty/0
values
- Custom Collection - these are handy because they provide a way to implement custom collection-level code while still providing common collection functionality
- Good Support - free software is nice, but the support alone may be worth the cost--the ES team responded to some show-stopping issues very quickly
It hasn't all been perfect, but we've done pretty well with ES and XAML. The EntitySpaces team resolved a few issues, and we found work-arounds to some others. My intent with this article is to walk you through our journey of discovery and show you how to avoid the pitfalls. I hope that it helps you make an informed decision when choosing your presentation and business layer technologies.
The two technologies you should be familiar with before reading this are Windows Presentation Foundation and EntitySpaces. This is not intended to be a tutorial in either technology, but rather a demonstration of how to use the two together. If you're not familiar with the EntitySpaces Framework, I suggest you first familiarize yourself with the framework by reading the documentation available at the EntitySpaces Web site. I recommend starting with the Getting Started PDF or the Walk-through video presentation. In this article, I will only cover the specific steps that are important to get your business entities to work with WPF Databinding. As for WPF, you will need at least a basic knowledge of XAML and Databinding to understand the topics covered in this article.
To develop your own applications using EntitySpaces for your business layer, you will need to Download MyGeneration and the EntitySpaces trial version. Both are free downloads, but the EntitySpaces trial is limited to 45 days of use.
There are four things you need to do before you can run the sample application for this article:
- Download the EntitySpaces trial edition
- Install the Northwind database on your SQL Server (The Northwind creation script is included in the zip file)
- Edit the connection string in ESN.WindowsClient.exe.config to connect to your database
- Add the following references to both projects:
EntitySpaces.Core
EntitySpaces.Interfaces
EntitySpaces.SqlClientProvider
EntitySpaces.Loader
Once all that's set, build the solution and you're good to go.
The first important step for using EntitySpaces in XAML comes in the code generation. WPF supports dynamic DataBinding via the INotifyPropertyChanged
interface, so make sure you select the option 'Support INotifyPropertyChanged' when generating your business entities. Without this option, you will still be able to databind to EntitySpaces entities, but they will not reflect changes that occur in the underlying business layer.
You can see the options I used to generate the business objects for this project below:
And here's what the generated Customers
class looks like:
The generated classes are ready to go as soon as they're created. You can use the ES Custom Master Template with similar settings to create separate files (partial classes) to contain user-created code, but it's not necessary. For this project, I only added custom code to one class (covered later).
The user interface for the EntitySpaces Northwind Customer Editor is split into three areas:
- A grid to view customer data on all customers
- A grid to edit the data for the selected customer
- An area for matching customers with customer types (demographics).
The customer view grid is a piece of cake. Here's the XAML I used to define it:
<ListView Grid.Row="0" Grid.ColumnSpan="3" Name="CustomersListBox"
SelectionMode="Single" IsSynchronizedWithCurrentItem="True"
ItemsSource="{Binding ElementName=ThisWindow, Path=Customers}"
ToolTip="Press Ctrl + S to save changes.">
<ListView.View>
<GridView>
<GridViewColumn Header="Company Name"
DisplayMemberBinding="{Binding CompanyName}"/>
<GridViewColumn Header="Contact Name"
DisplayMemberBinding="{Binding ContactName}"/>
<GridViewColumn Header="Contact Title"
DisplayMemberBinding="{Binding ContactTitle}"/>
<GridViewColumn Header="Address" DisplayMemberBinding="{Binding Address}"/>
<GridViewColumn Header="City" DisplayMemberBinding="{Binding City}"/>
<GridViewColumn Header="Region" DisplayMemberBinding="{Binding Region}"/>
<GridViewColumn Header="Postal Code" DisplayMemberBinding="{Binding PostalCode}"/>
<GridViewColumn Header="Country" DisplayMemberBinding="{Binding Country}"/>
<GridViewColumn Header="Phone" DisplayMemberBinding="{Binding Phone}"/>
<GridViewColumn Header="Fax" DisplayMemberBinding="{Binding Fax}"/>
<GridViewColumn CellTemplate="{StaticResource DeleteCustomerTemplate}"/>
</GridView>
</ListView.View>
</ListView>
The first thing you need to realize is that the data source for the items in this ListView
is an EntitySpaces collection (CustomersCollection
). The ElementName
in the binding refers to the Window itself (declared with Name="ThisWindow"
), so the Customers
property can be found in the code-behind for MainWindow.xaml (MainWindow.xaml.cs). Here's the property definition:
private Biz.CustomersCollection _customers = null;
public Biz.CustomersCollection Customers
{
get
{
if (_customers == null)
ResetCustomers();
return _customers;
}
}
void ResetCustomers()
{
_customers = new Biz.CustomersCollection();
_customers.Query.OrderBy(_customers.Query.ContactName.Ascending);
_customers.Query.Load();
}
Dynamically loading data from your database with EntitySpaces couldn't be easier. Since I didn't specify a where
clause here, the _customers
collection will be filled with all records in the Customers
table (sorted by ContactName
). It's important that collections like this should be instance variables defined on the Window/Control itself. In a tabbed application, I originally used static
properties that were shared across multiple tabs for efficiency, but that caused some weird behavior where different controls referencing the collection would share the same selected item (changing it in one would affect the others).
Once the data source is set up, defining the columns is a piece of cake. Each column binds to a public
property on the business entity class (Customers
). Any public
property can be a binding source for XAML controls, but the INotifyPropertyChanged
interface and a proper implementation on each property is necessary for the binding to update automatically when the underlying data changes. The options I used when generating my business objects created property changed notification for all properties based on database columns, and it's easy to add PropertyChanged
support to your own properties in the user-created code files.
The final point of interest in the customers grid
is the column that uses a CellTemplate
instead of DisplayMemberBinding
. The static
resource it references can be found in the Window.Resources
section. It's a fairly trivial template, but is intended to demonstrate how you can use templates to display data or provide interaction in a more flexible way using more complex controls. You could just as easily template all columns with TextBoxes
and/or ComboBoxes
to allow inline editing in the grid
, but I wanted to keep it simple in this example.
Ignoring the unimportant layout and label controls, the TextBox
es that allow you to edit customer data are just as simple as the viewer grid
.
<TextBox Grid.Row="0" Grid.Column="2"
Text="{Binding CompanyName, UpdateSourceTrigger=PropertyChanged}"/>
<TextBox Grid.Row="2" Grid.Column="2"
Text="{Binding ContactName, UpdateSourceTrigger=PropertyChanged}"/>
<TextBox Grid.Row="4" Grid.Column="2"
Text="{Binding ContactTitle, UpdateSourceTrigger=PropertyChanged}"/>
<TextBox Grid.Row="6" Grid.Column="2"
Text="{Binding Address, UpdateSourceTrigger=PropertyChanged}"/>
<TextBox Grid.Row="8" Grid.Column="2"
Text="{Binding City, UpdateSourceTrigger=PropertyChanged}"/>
<TextBox Grid.Row="10" Grid.Column="2"
Text="{Binding Region, UpdateSourceTrigger=PropertyChanged}"/>
<TextBox Grid.Row="12" Grid.Column="2"
Text="{Binding PostalCode, UpdateSourceTrigger=PropertyChanged}"/>
<TextBox Grid.Row="14" Grid.Column="2"
Text="{Binding Country, UpdateSourceTrigger=PropertyChanged}"/>
<TextBox Grid.Row="16" Grid.Column="2"
Text="{Binding Phone, UpdateSourceTrigger=PropertyChanged}"/>
<TextBox Grid.Row="18" Grid.Column="2"
Text="{Binding Fax, UpdateSourceTrigger=PropertyChanged}"/>
By default, Binding
s to TextBox.Text
are two-way, so whenever the text changes, the binding
will write the text back to the Binding
source (which in turn will update the GridView
). The only thing I needed to change from the default was the UpdateSourceTrigger
property. Without this setting, changes would not be passed to the source until the TextBox
loses focus, so the GridView
would be out of synch with the text as the user is typing. Although this example only uses TextBoxes
and string
properties, Binding
to generated classes will work similarly for other controls and data types. However, in some cases, you may need to use a converter to convert the source data to the type of the property you're setting. Hmmm, that sounds like a segue...
The EntitySpaces templates generate different property definitions based on the type of foreign key reference (one-to-one, one-to-many, many-to-many). For the Northwind Customers
table, ES generates two properties and two methods for managing the many-to-many relationship with CustomerDemographics
.
CustomerCustomerDemoCollection CustomerCustomerDemoCollectionByCustomerID
CustomerDemographicsCollection UpToCustomerDemographicsCollection
void AssociateCustomerDemographicsCollection(CustomerDemographics)
void DissociateCustomerDemographicsCollection(CustomerDemographics)
The properties give you access to the collection of association records in the linking table and the collection of associated Demographics
, respectively. You can either manually create and delete associations via the CustomerCustomerDemoCollectionByCustomerID
or use the provided Associate
and Dissociate
methods. Any changes to associations will be saved to the database when you save the Customer
. So how do you put all this together to make an effective user interface? Good question. Let's dig into it.
First off, we need to display all the available CustomerDemographics
so the user can choose which to associate with the selected Customer
. Then we need to display whether each demographic is associated. I decided to represent this through a CheckBox
list. WPF doesn't actually have a pre-defined CheckBoxList
control, but that's ok. We can use a generic ItemsControl
with a template to display CheckBoxes
.
<ScrollViewer Grid.Row="1" HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Disabled">
<ItemsControl ItemsSource="{Binding ElementName=ThisWindow, Path=Demographics}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Orientation="Vertical"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<CheckBox Margin="4,2,10,2" Content="{Binding CustomerDesc}"
Click="CustomerType_Click">
<CheckBox.IsChecked>
<MultiBinding Mode="OneWay"
Converter="{StaticResource CustomerDemographicsCheckConverter}">
<MultiBinding.Bindings>
<Binding ElementName="CustomersListBox" Path="SelectedItem"/>
<Binding Path="."/>-->
</MultiBinding.Bindings>
</MultiBinding>
</CheckBox.IsChecked>
</CheckBox>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
First things first: ItemsSource
. This binds to a property that's similar to the Customers
collection I created. Next, the ItemsPanel
defines the layout of the items within the control. The most important part is the template that defines the content of each item. Since the ItemsSource
is a CustomerDemographicsCollection
, the DataContext
of each item will be a CustomerDemographics
object, so all bindings within will be based off that source. For example, Content="{Binding CustomerDesc}"
is setting the text of each CheckBox
to the description of each demographic.
The IsChecked
status was the trickiest part. First off, it needs to be a MultiBinding
, since I need to know both the current customer
and demographic
in order to determine if they're associated. Second, I need a custom MultiValueConverter
to convert the values from the binding
into a boolean checked status. Finally, I have to handle changes to the checked status and represent those changes in the business object. Value converters support bi-directional conversions, but it would be difficult to convert a boolean (the checked status) into a Customer
and CustomerDemographic
object. I decided to keep it simple, using a one-way binding
and handling the Click
event on the CheckBox
. CheckBox
es also have Checked
and Unchecked
events, but those fire before the framework sets the IsChecked
status, so that won't work. Why not? Because the binding is one-way, WPF will set the CheckBox
's IsChecked
status to a literal value after the box is clicked. Whenever a databound
property is set to a literal value, it will overwrite the binding. So after the user clicks a CheckBox
, its checked status will remain the same, even after I select a different customer
. My solution was to reset the binding
after each CheckBox
click (_checkBoxBinding
is a binding
object equivalent to the one declared in the XAML file).
void CustomerType_Click(object sender, RoutedEventArgs e)
{
Biz.Customers currentCustomer = this.CustomersListBox.SelectedItem as Biz.Customers;
if (currentCustomer == null)
return;
CheckBox check = sender as CheckBox;
if (check == null)
return;
Biz.CustomerDemographics currentDemographic =
check.DataContext as Biz.CustomerDemographics;
if (currentDemographic == null)
return;
if (check.IsChecked ?? false)
currentCustomer.AssociateDemographic(currentDemographic);
else
currentCustomer.DissociateDemographic(currentDemographic);
check.SetBinding(CheckBox.IsCheckedProperty, this._checkBoxBinding);
}
You might notice another strange thing about this Click
handler: I'm not using the generated Associate
and Dissociate
methods. That's because the changes these methods cause are not available through any public
property of the business object. They work fine if you just want to associate something and save it, but if you want to bind to the most recent in-memory associations, you're out of luck. So instead of using the generated association methods, I created my own versions that simply add records to the linking table, then I check that table for associations in the binding
's converter
. Here's what those methods look like:
public void AssociateDemographic(CustomerDemographics demographic)
{
CustomerCustomerDemo link = this.CustomerCustomerDemoCollectionByCustomerID.AddNew();
link.CustomerID = this.CustomerID;
link.UpToCustomerDemographicsByCustomerTypeID = demographic;
}
public void DissociateDemographic(CustomerDemographics demographic)
{
foreach (CustomerCustomerDemo link in
this.CustomerCustomerDemoCollectionByCustomerID)
if (link.CustomerID == this.CustomerID &&
(object.ReferenceEquals(link.UpToCustomerDemographicsByCustomerTypeID,
demographic) ||
link.CustomerTypeID == demographic.CustomerTypeID))
link.MarkAsDeleted();
}
The only thing I don't like about this method is that you're creating and deleting records every time the user clicks a CheckBox
. If there was already a record in the database, it might get deleted and replaced by a new one, so the linking collections aren't very smart that way. At least it all just works when you click save, though; no manual manipulation is required.
There's one more snag you'll want to be aware of when working with EntitySpaces. While the individual entities can support INotifyPropertyChanged
, there is no support for INotifyCollectionChanged
yet (it is new to .NET 3.0). This is what WPF controls primarily use for tracking changes in collections. You would think that because they don't implement this interface, ES collections will not supply the proper change notification to databound
controls, and that you wouldn't be able to see changes to the collection reflected in the UI. That is not the case, though. I'm not sure why, but they do work in most cases. The only time a control will treat an ES collection differently from a List
or ObservableCollection
is when it starts out empty. If you load 1 or more records into the collection, and bind to it, everything will update as expected when you add and remove records, but if the collection was empty when it was first bound, the UI control will behave very strangely as records are added and removed. I'm not sure if this is a problem with EntitySpaces or WPF, but thankfully there is a work-around. You may have noticed this strangeness in the Demographics
property definition:
void ResetDemographics()
{
_demographics = new Biz.CustomerDemographicsCollection();
_demographics.LoadAll();
_demographics.DetachEntity(_demographics.AddNew());
}
Since the problem occurs when binding
to a collection that is initially empty, one of my co-workers discovered that if you simply add a record, then immediately remove it after you initialize the collection, everything works fine.
The last thing I want to bring up is amusing in that I discovered two drawbacks as I tried to implement a single feature. What I wanted to do was have a column in the Customer
grid with a Save button that would allow the user to save individual users. Here's what the template would have looked like for that column:
<DataTemplate x:Key="SaveCustomerTemplate">
<Button.Style>
<Style TargetType="Button">
<Setter Property="IsEnabled" Value="False"/>
<Style.Triggers>
<DataTrigger Binding="{Binding es.IsAdded}" Value="True">
<Setter Property="IsEnabled" Value="True"/>
</DataTrigger>
<DataTrigger Binding="{Binding es.IsModified}" Value="True">
<Setter Property="IsEnabled" Value="True"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Button.Style>
</DataTemplate>
The first problem I discovered with this setup is that the Save buttons would never become enabled, even after I changed some customer data. That's because the properties generated for database columns implement PropertyChanged
notification, but the core EntitySpaces classes like the one referenced in every entity's es
property do not (the es
object stores information about the entity's RowState
). Since the es.IsAdded
and es.IsModified
properties do not fire property changed events, the IsEnabled
status of the button will not ever change. I reported this issue to the EntitySpaces team. They've been pretty responsive in the past to bug reports and feature requests, so I expect this to be addressed in their next release.
The second problem with the individual Save buttons is one I should have realized when I first had the idea. EntitySpaces doesn't support saving individual entities that reside in a collection. It actually throws an exception. My understanding is that this is for efficiency reasons. Obviously, it's more efficient to save the changes to an entire collection at once, but some users may have valid business reasons for saving individual objects within a collection. In addition, some built-in EntitySpaces Framework behavior may cause this exception. This actually occurred when working on this sample application:
- User creates a new
demographic
- User associates that
demographic
with the first customer
- User saves the
customers
collection
- When the first
customer
saves, it saves the new association, which saves the new demographic
- Since the
demographic
is part of a collection, an exception is thrown and the save is aborted
In this case, I was able to work-around the problem by saving the entire Demographics
collection first, but it won't always be this easy. I think it would be much better if ES just allowed users to save entities independently from the collection in which they reside.
So that wraps up my analysis of using the EntitySpaces business object Framework with Windows Presentation Foundation. Overall, I'd say EntitySpaces is a great Framework that has room for a little improvement. If I had to quantify it, I'd say ES is about 90% XAML-ready. You'll probably be satisfied with the functionality of ES objects and XAML in most situations, but as with any technology (XAML itself included), there are a few snags you'll need to work around. If you'd like more information or assistance with EntitySpaces (with or without WPF), I recommend visiting their Web site and support forums.
Licence Notes
The code related to EntitySpaces is not covered by any specific licence. All other code in this article is covered by the licence below.