问题
OVERVIEW/DESCRIPTION
Simple: Polymorphic removal of objects of runtime types derived from TEntity
added to an ObjectSet<TEntity>
will NOT raise the IBindingList.ListChanged
event on the IBindingList
object returned by the of the ObjectSet<TEntity>.IListSource.GetList()
method.
However, removal of instances whose runtime type match TEntity
are effectively notified on the ListChanged
event.
To clarify, at all times, objects are effectively removed from the underlying collections or data views/stores, but when these objects are instances of types strictly derived from the actual TEntity
used, the ListChanged
event is not raised to notify of their removal.
This is simply a phenomenal BUG for the purposes of appropriate data-binding support of runtime polymorphism for collections.
REPLICATION
Model Setup
- Table per Type Strategy.
- Entity model mapped and validated against gemerated SQL database on Server 2012 Express.
This is the entity hierarchy (pseudo-UML):
FiascoEntityContext : ObjectContext
+ Foos : ObjectSet<Foo>
Foo : EntityObject
+ Id: Int32
+ Name: String
SpecialFoo : Foo
+ SpecialProperty: String
Demonstration Code
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel;
using System.Data.Objects;
namespace FiascoEF {
class Program {
static void Main(string[] args) {
using (FiascoEntityContext context = new FiascoEntityContext()) {
//
// add some foos
//
context.Foos.AddObject(new Foo { Name = "Foo1" });
context.Foos.AddObject(new BetterFoo { Name = "BetterFoo1", SpecialProperty = "Something Special" });
context.SaveChanges();
//
// show the contents
//
Console.WriteLine("Listing all foos:");
foreach (var foo in context.Foos) {
Console.WriteLine(" Got {0}: Id={1} Name={2} (State={3})", foo, foo.Id, foo.Name, foo.EntityState);
}
//
// attach handler for the ListChanged event of the IBindingList returned by context.Foos as IListSource
// NOTE: have to do this here bacause SaveChanges() above will reset the internal IBindingList
//
var bindingList = (context.Foos as IListSource).GetList() as IBindingList;
bindingList.ListChanged += new ListChangedEventHandler(bindingList_ListChanged);
//
// delete all foos and show state. expect the event handler above to be invoked.
//
Console.WriteLine("Deleting all foos:");
foreach (var foo in context.Foos) {
context.Foos.DeleteObject(foo);
Console.WriteLine(" Deleted {0}: Id={1} Name={2} (State={3})", foo, foo.Id, foo.Name, foo.EntityState);
}
context.SaveChanges();
}
}
static void bindingList_ListChanged(object sender, ListChangedEventArgs e) {
Console.WriteLine(" Event on {0}: {1}", sender, e.ListChangedType);
}
}
}
Expected Results
Listing all foos:
Got FiascoEF.Foo: Id=257 Name=Foo1 (State=Unchanged)
Got FiascoEF.BetterFoo: Id=258 Name=BetterFoo1 (State=Unchanged)
Deleting all foos:
Event on System.Data.Objects.ObjectView`1[FiascoEF.Foo]: ItemDeleted
Deleted FiascoEF.Foo: Id=257 Name=Foo1 (State=Deleted)
Event on System.Data.Objects.ObjectView`1[FiascoEF.Foo]: ItemDeleted
Deleted FiascoEF.BetterFoo: Id=258 Name=BetterFoo1 (State=Deleted)
Actual Results
Listing all foos:
Got FiascoEF.Foo: Id=257 Name=Foo1 (State=Unchanged)
Got FiascoEF.BetterFoo: Id=258 Name=BetterFoo1 (State=Unchanged)
Deleting all foos:
Event on System.Data.Objects.ObjectView`1[FiascoEF.Foo]: ItemDeleted
Deleted FiascoEF.Foo: Id=257 Name=Foo1 (State=Deleted)
Deleted FiascoEF.BetterFoo: Id=258 Name=BetterFoo1 (State=Deleted)
RESEARCH
Through reflector found that the actual IBindingList
returned is of type ObjectView<TElement>
, and this type delegates removal operations to an internal IObjectViewData<TElement>
. The implementation found for that interface is ObjectViewQueryResultData<TElement>
which defines:
public ListChangedEventArgs OnCollectionChanged(object sender, CollectionChangeEventArgs e, ObjectViewListener listener) {
ListChangedEventArgs changeArgs = null;
if (e.Element.GetType().IsAssignableFrom(typeof(TElement)) && _bindingList.Contains((TElement) (e.Element))) {
...
changeArgs = new ListChangedEventArgs(ListChangedType.ItemDeleted, ...);
...
}
return changeArgs;
}
The check:
if (e.Element.GetType().IsAssignableFrom(typeof(TElement)) && ...) { ... }
seems bogus... probably the following was intended?
if (typeof(TElement).IsAssignableFrom(e.Element.GetType()) && ...) { ... }
回答1:
Fair enough, bug reported to Microsoft — http://connect.microsoft.com/VisualStudio/feedback/details/749368... They seem to have acknowledged the issue, but it's not clear what they will do.
Remember, we're talking about the IBindingList
implementation retrieved by the ObjectSet<T>
when viewed as a IListSource
for the purposes of data-binding, so the event is expected to be fired, just as it does for the case of a homogeneous list.
I broke loose by defining a class that inherits from ObservableCollection<T>
and wraps the ObjectSet<T>
, then implement IListSource
with the help of the DbExtensions.ToBindingList<T>(this ObservableCollection<T>)
extension method.
Alternatively, I could just move on and start using the DbContext
API entirely, but defining my own ObservableCollection<T>
allows me to keep using ObjectContext
for now, which is what I want.
来源:https://stackoverflow.com/questions/11061548/polymorphic-removal-of-objects-added-to-objectsettentity-will-not-raise-the-ib