locked
EF CTP5 .Local - ObservableCollection does not get updated RRS feed

  • Question

  • I am using the .Local property, which returns a ObservableCollection. What I figured is that this returned ObservableCollection does not get updates with new values from the database. Try the following code, set a breakpoint, change the database value and execute the the next statements:

    context.Employees
      .Load();
    var v1 = context.Employees.Local.First().Name;

    // Set a breakpoint here, change the database values and see if the values are update.

    context.Employees
      .Load();
    var v2 = context.Employees.Local.First().Name;

    Values in v1 and v2 are the same, but should be different...

    Is this a bug or a feature? Am I doing something wrong?

    Cheers Harry

    Thursday, January 13, 2011 10:52 PM

Answers

  • Harry,

     

    There isn’t currently a good way to refresh/reload all the entities in a set using the DbContext API.  However, you can drop-down to ObjectContext to do this with code something like this:

     

        var objectSet = ((IObjectContextAdapter)context).ObjectContext.CreateObjectSet<Employee>();

        objectSet.MergeOption = MergeOption.OverwriteChanges;

        objectSet.Load();

     

    There are some caveats about doing this:

    ·         Usually when you need to handle concurrent updates it is not as simple as just updating the entities from the database.  Usually you also need to consider what to do in the case of conflicts.  That is, what to do when the same entity is modified in the database and on the client.

    ·         Queries with merge options other than AppendOnly and NoTracking have some very complicated behaviors, especially if you also use independent associations.  This is why we haven’t exposed this in the DbContext API.  We intend to make a reload/refresh of multiple entities available in DbContext, but it likely won’t ready for the first release.

     

    Thanks,

    Arthur

    Friday, January 14, 2011 5:11 PM
    Moderator

All replies

  • Harry,

     

    The Load() method is shorthand for querying all the entities in the set from the database.  However, when EF does a query against the database it does so in AppendOnly mode.  This means that only entities that are not already in the context are created from the database.  Any entity that is already in the context is not changed.  This is often desirable behavior, both for performance reasons and so that you have more control over the changes made to entities for handling things like concurrency issues.

     

    If you  want to override the property values of an entity in the context with values from the database you can use the Reload method.  For example:

     

        context.Entry(employee).Reload();

     

    Thanks,

    Arthur

    Friday, January 14, 2011 6:08 AM
    Moderator
  • Hi Arthur

    thankx for your reply. What I saw when I did my evaluation is, that even on the second .Load() statement, the database gets hit with the same SQL statement as with the first one. So the same amount of data is transfered to the client the second time aswell. But the client context is not updated. That makes no sense in my opinion. If I get the data from the database, I would assume that the data is available via the context.

    If I would use your solution, and the context does have a 1000 records, I would need to loop through the context and execute the statement a 1000 times, would the database be hit a 1000 times? In my testing the database gets hit.

    Cheers Harry

    Friday, January 14, 2011 6:43 AM
  • Hi Arthur

    I have done more investigation but with your suggestion (using .Reload()) the solution is very slow.

    What do you suggest for the following situation:

    • I want to have an ObservalbleCollection since I am using WPF (I would like to use .Local and keep the DbContext around)
    • The ObservableCollection should reflect updates, additions and deletions which are done on the WPF-Client, aswell as coming from the SQL Server (concurrent users) - so far the problem not solved is, updates form the database are not reflected to the ObservableCollection, all other requirements are meet. Your suggestion does update, but is too slow (the list does have 400 reocrds).
    • I have implemented a class with my own ObservableCollections which handles the requirements just fine, but I am not sure if I should implement my own DbContext, especially I am not sure how it will develop if more foreign references are coming into the play.

    Thankx, Harry

    Friday, January 14, 2011 12:21 PM
  • Harry,

     

    There isn’t currently a good way to refresh/reload all the entities in a set using the DbContext API.  However, you can drop-down to ObjectContext to do this with code something like this:

     

        var objectSet = ((IObjectContextAdapter)context).ObjectContext.CreateObjectSet<Employee>();

        objectSet.MergeOption = MergeOption.OverwriteChanges;

        objectSet.Load();

     

    There are some caveats about doing this:

    ·         Usually when you need to handle concurrent updates it is not as simple as just updating the entities from the database.  Usually you also need to consider what to do in the case of conflicts.  That is, what to do when the same entity is modified in the database and on the client.

    ·         Queries with merge options other than AppendOnly and NoTracking have some very complicated behaviors, especially if you also use independent associations.  This is why we haven’t exposed this in the DbContext API.  We intend to make a reload/refresh of multiple entities available in DbContext, but it likely won’t ready for the first release.

     

    Thanks,

    Arthur

    Friday, January 14, 2011 5:11 PM
    Moderator
  • Hi Arthur

    thankx a again for all your help and information.

    I am now using the CreateObjectSet statement from above and the data in the context gets updated. But if a record is deleted from the database, this record is not removed from the context. Any suggestion on this part?

    Thankx, Harry

    Wednesday, January 19, 2011 2:31 PM
  • Harry,

     

    The problem is that when you execute a query against the database it will only operate on entities that are returned by that query.  The alternative to this is to request that the entities actually in the context are refreshed/reloaded based on their current state in the database.  To do this you would write something like:

     

        ((IObjectContextAdapter)context).ObjectContext.Refresh(RefreshMode.StoreWins, context.Employees.Local);

     

    However since this is working only on entities you already have in the context it will not bring in any new entities that are in the database but not in the context.  In order to get both new entities brought in and deleted entities removed you will have to do both the query and the refresh.

     

    Thanks,

    Arthur

    Wednesday, January 19, 2011 5:59 PM
    Moderator
  • Arthur,

    great work on the new DbContext and DbSet API! Code-first is currently not an option for me but the simple productivity improvements bring LOB apps with WPF & DataGrids a lot closer to RAD. Please, please, keep improving the T4 templates for DB-first so that they include basic DataAnnotations (at least Required, StringLength, Editable) for Validation.

    On the current thread: I have a scenario with multiple local WPF clients on a single DB. If I get you correctly your solution for doing a *complete* refresh from the database while keeping the context is

     // refresh existing and add new
     var objectSet = ((IObjectContextAdapter)context).ObjectContext.CreateObjectSet<Employee>();
     objectSet.MergeOption = MergeOption.OverwriteChanges;
     objectSet.Load();
    
     // remove from context, what's been removed from database 
     ((IObjectContextAdapter)context).ObjectContext.Refresh(RefreshMode.StoreWins, context.Employees.Local);
    
    

    Allthough not very elegant this does work for me. This will fire two very similar database queries where one would actually do, right?

    Now, in CTP5 you have moved the ObjectContext to a hidden property only accessible via a special interface. This suggests, that you plan to remove ObjectContext completely from the new DbXyz API. Will there be an alternative for the refresh scenario above in DbContext BEFORE ObjectContext is removed completely? If not, I'rather stay with the old ObjectContext.

    Thanks for your answer, Andreas

    • Proposed as answer by Anuj Agg Tuesday, February 15, 2011 10:44 AM
    Monday, January 24, 2011 8:30 AM
  • Andreas,

     

    We don’t plan on removing access to ObjectContext. It is somewhat hidden because we intend for it to be used only infrequently, especially as some of the features missing from DbContext are added.  It’s also hidden because it can be confusing for people when they are trying to find something using Intellisense.

     

    We certainly intend to add support for refresh/reload of multiple entities.  However, this won’t make it into the first RTM of DbContext due to time constraints and also because we’re still figuring out what the best API should be.  Rather than rush out a bad API the advice is for now to continue to drop down to ObjectContext.

     

    Thanks,

    Arthur

    Wednesday, January 26, 2011 11:22 PM
    Moderator
  • I found in the SQL profiler that the ObjectContext.Refresh(...) method actually produces (has to produce) pretty lengthy SQL and is probably not terrebly efficient for a larger number of records. To get exactly the records of the Enumeration in the second parameter it builds WHERE clauses over the keys of all those items. If the Enumeration holds more than 250 items it builds a second WHERE clause. I suppose some RDBMS can not handle conditions with more than 250 ORed expressions. So if you refresh 1000 records 4 queries will be fired each listing 250 "myKey=xyz" conditions. The result of all this is correct.

    I took a little different (may be too simplistic approach), but compared to my lines above it does a complete refresh of .Local with a single query.

        public static void RefreshFromStore<T>(DbContext context,IEnumerable<T> cacheObjects) 
          where T : class
        {
          // refresh existing and add new
          ObjectContext oc = ((IObjectContextAdapter)context).ObjectContext;
          ObjectSet<T> objectSet = oc.CreateObjectSet<T>();
          objectSet.MergeOption = MergeOption.OverwriteChanges;
    
          var storeDict = objectSet.ToDictionary(e => e); // triggers DB query
    
          // remove from context, what's been removed from database 
          IList dbDeleted = new ArrayList();
          foreach (T cacheObject in cacheObjects) {
            if (!storeDict.ContainsKey(cacheObject)) {
              // cacheObject is an orphan in our context
              dbDeleted.Add(cacheObject);
            }
          }
          foreach (object entity in dbDeleted) {
            oc.Detach(entity); // remove from context
          }
        }
    
    

    Note that the use of this is very limited as there is no filtering at all so as-is it does not really make sense for large table. One could easily add Where(..) condition to the objectSet, but I haven't tried this. 

    Thursday, April 21, 2011 5:04 PM