tags:

views:

185

answers:

2

Hi all,

I have created an application which has the following:

  • A database created in VS2008, LINQ to SQL
  • A view of the database:

XAML part

<CollectionViewSource x:Key="CustomerView" Source="{Binding Source={x:Static Application.Current}, Path=Customers}" />

C# part

    public IEnumerable<Customer> Customers
    {
        get
        {
            var database = new ApplicationDataContext();
            return from customer in database.Customers
                   select customer ;
        }
    }

The view show not only the customers, but also subtables like Customers.Products (a linked table).

Now I change the properties of a Product somewhere, and I would expect the View to be automatically updated (because I see that the tables implement INotifyPropertyChanged, INotifyPropertyChanging).

But this does not happen.

I could all trigger it manualy, but before I start doing that I wonder if it should happen automatically. Anyone?

+2  A: 

It wont update if you just expose customers as an IEnumerable<Customer>. You need to expose it as a collection that triggers events when it's contents change. Either expose it fully as whatever type your Customers table is (if that type raises INotify events), or you need to wrap it up in something like an ObservableCollection<>.

Simon P Stevens
How looked into the (generated) LINQ code a bit: the Table (the collection) does not implement INotify, but each item in the Table, the "Customer" does implement INotity.
Robbert Dam
@Robbert: Rather than binding your view directly to the table, you should probably be following something like the MVVM pattern (see here: http://www.orbifold.net/default/?p=550). In this case, you would have a model class that would wrap up any adding/removing to the data table. The model class would raise events when you did things. You would also have a viewmodel class that would expose an ObservableCollection. You would be able to add/remove things from this ObsColl in response to events from the model. You would then bind your view to the viewmodel.
Simon P Stevens
You don't have to expose the list as an ObservableCollection, although that is the easiest thing to do. Any type that implements INotifyCollectionChanged will do.ObservableCollection implements INotifyCollectionChanged so using that probably fits the majority of cases.
Mark Seemann
A: 

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:

  1. 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

  1. 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

  1. 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.

Mark