Quick tips for Entity Framework Databinding
One of our customers asked this question yesterday on the Entity Framework forums. There were a few details missing and so I am not completely sure I got the question right. But I think it is about an issue I have heard quite a bit, and so I think it may be useful to share my answer here for others.
Given this simple line of code (i.e. in a WinForms application):
grid.DataSource = someQuery;
Several things will happening under the hood:
- Databinding finds that ObjectQuery<T> implements the IListSource interface, then it calls IListSource.GetList() on it, to obtain a binding list (an internal object that implements IBindingList and some other well-known interfaces that databound controls know how to communicate with)
- GetList() gets the query executed and it copies the contents of the resulting ObjectResult object to the binding list
- The new binding list is finally passed to the databound control
When a binding list is created this way from a tracked query (i.e. MergeOption in the query is set to anything but NoTracking), there are several other interesting details in the behavior:
- Changes made to entities in the binding list will get noticed by the state manger. Therefore changes will be saved to the database when SaveChanges is invoked on the tracking ObjectContext
- Additions of new entities to the context will not result in the new entities being added automatically to a binding list (this is a common expectation). Whether an entity belongs to a binding list is decided at the time the binding list is created. If we wanted to do something different (i.e. have the binding list get new objects added to the context automatically), we would hit some considerable obstacles:
- The binding list does not remember which filter condition was used in the query.
- Even if it did, our queries are server queries, expressed in either ESQL or LINQ, but always translated to server queries and then expressed in terms of the current values on the server.
- Even for LINQ queries, we cannot assume that the same query will have equivalent behavior while querying in-memory objects when compared to the same query translated to the native SQL of the database.
- As a consequence of #2: You can have multiple binding lists based on the same EntitySet with overlapping or disjoint sets of entities. You can use different queries (or even from the same query, but with different parameters) to get a different set of entities in each binding list.
- Deletion of entities from the context will result in the deleted entity being removed from all binding lists that contain it. The principle behind this is that the binding list is a window into the "current state of affairs" in the state manager. Even if we don't know if a new entity belongs into a binding list, we do know when it doesn't belong anymore, because it has been deleted.
- A binding list has its own Add() method you can use. If you could get a reference to one of our binding lists you could “manually” add new entities to the binding list, and the entity you add will also be added to the context automatically, the same as if it had invoked AddObject() on the context.
All these facts, especially #5, warrant me to give you a couple of tips:
- If you are interested in getting a reference to the binding list directly (i.e. to use its Add method), you can do something like this: var bindingList = ((IListSource)query).GetList() as IBindingList; There is a more convenient ToBindingList() extension method included in the EFExtensions you may want to take a look at.
- If you are not interested in getting the reference to the binding list for yourself, and you are using databinding directly against a query as in the sample at the top of this post, you should know that WinForms will call IListSource.GetList() twice, causing the query to be executed on the server twice! The recommendation then is to bind to the results, rather than the query, using code similar to this: grid.DataSource = someQuery.Execute(MergeOption.AppendOnly); In this case, since the resulting ObjectResult acts as a foward-only, enumerate-once cursor, its implementation of IListSource.GetList() is different from the one in the query: the call will consume the results (i.e. it will iterate through the whole results) and will cache the binding list in the ObjectResult instance. All subsequent calls to IListSource.GetList() will return the same binding list.
We have a few ideas around things we may improve in future versions. Some of them you may be able to guess from this explanation. But I will save that for another post...