I'm doing an LOB app with WPF + Linq to SQL, and the problem of the Linq-To-Sql collections not correctly implementing INotifyCollectionChanged is something that I 've had to work around on every facet of the system.
The best solution I've found so far is to do any one of the following:
- Create a model layer over your DataContext class, so that your GUI code only interacts with the model layer, not directly with the DataContext. In your business logic methods, always wrap returned collections in an ObservableCollection
and/or
- Implement secondary collection properties on your entity classes, such that where you originally had Customer.Products, you now have *Customer.Products_Observable", where this new readonly property simply returns an ObservableCollection that wraps whatever Customer.Products returns.
and/or
- Create a new class derived from ObservableCollection, which is DataContext aware. If you override the Add/Insert/Remove methods of such a class, then any alterations to the collection can automatically propogate to the DataContext and InsertOnSubmit / DeleteOnSubmit calls.
Here is an example of such a class:
Imports System.Collections.Generic
Imports System.Collections.ObjectModel
Imports System.ComponentModel
Imports System.Linq
Imports System.Data.Linq
Public Class ObservableEntityCollection(Of T As {Class})
Inherits ObservableCollection(Of T)
Private _Table As Table(Of T)
Public Sub New(ByVal Context As DataContext)
Me._Table = Context.GetTable(Of T)()
End Sub
Public Sub New(ByVal Context As DataContext, ByVal items As IEnumerable(Of T))
MyBase.New(items)
Me._Table = Context.GetTable(Of T)()
End Sub
Protected Overrides Sub InsertItem(ByVal index As Integer, ByVal item As T)
_Table.InsertOnSubmit(item)
MyBase.InsertItem(index, item)
End Sub
Public Shadows Sub Add(ByVal item As T)
_Table.InsertOnSubmit(item)
MyBase.Add(item)
End Sub
Public Shadows Sub Remove(ByVal item As T)
If MyBase.Remove(item) Then
_Table.DeleteOnSubmit(item)
End If
Dim deletable As IDeletableEntity = TryCast(item, IDeletableEntity)
If deletable IsNot Nothing Then deletable.OnDelete()
End Sub
Protected Overrides Sub RemoveItem(ByVal index As Integer)
Dim Item As T = Me(index)
_Table.DeleteOnSubmit(Item)
MyBase.RemoveItem(index)
End Sub
End Class
Public Interface IDeletableEntity
Sub OnDelete()
End Interface
The IDeletable interface allows you to implement specific logic on your entity classes (like cleaning up foreign-keys and deleting child objects).
Notice that the class requires a DataContext reference as a constructor, which makes it ideally suited for use with scenario 1) above (i.e. use from a Model layer/class). If you want to implement it method 2) [ie on the entity as a property], then you can give attached entities the ability to "find" their DataContext as follows:
[On the entity Class:]
Public Property Context() As DataContext
Get
If _context Is Nothing Then
_context = DataContextHelper.FindContextFor(Me)
Debug.Assert(_context IsNot Nothing, "This object has been disconnected from it's DataContext, and cannot perform the requeted operation.")
End If
Return _context
End Get
Set(ByVal value As DataContext)
_context = value
End Set
End Property
Private _context As DataContext
[As a utility class]:
Public NotInheritable Class DataContextHelper
Private Const StandardChangeTrackerName As String = "System.Data.Linq.ChangeTracker+StandardChangeTracker"
Public Shared Function FindContextFor(ByVal this as DataContext, ByVal caller As Object) As JFDataContext
Dim hasContext As Boolean = False
Dim myType As Type = caller.GetType()
Dim propertyChangingField As FieldInfo = myType.GetField("PropertyChangingEvent", BindingFlags.NonPublic Or BindingFlags.Instance)
Dim propertyChangingDelegate As PropertyChangingEventHandler = propertyChangingField.GetValue(caller)
Dim delegateType As Type = Nothing
For Each thisDelegate In propertyChangingDelegate.GetInvocationList()
delegateType = thisDelegate.Target.GetType()
If delegateType.FullName.Equals(StandardChangeTrackerName) Then
propertyChangingDelegate = thisDelegate
Dim targetField = propertyChangingDelegate.Target
Dim servicesField As FieldInfo = targetField.GetType().GetField("services", BindingFlags.NonPublic Or BindingFlags.Instance)
If servicesField IsNot Nothing Then
Dim servicesObject = servicesField.GetValue(targetField)
Dim contextField As FieldInfo = servicesObject.GetType.GetField("context", BindingFlags.NonPublic Or BindingFlags.Instance)
Return contextField.GetValue(servicesObject)
End If
End If
Next
Return Nothing
End Function
Note: An entity can only find it's DataContext if it is attached to a DataContext with ChangeTracking switched on. The above hack (yes - it is a hack!) relies on it.