locked
Remove entity from collection triggers delete of record! RRS feed

  • Question

  • Hi,

    I the following code to associate two entities:

     

    _dataContext.AddLink(CurrentVolunteer, "Groups", value);
    CurrentVolunteer.Groups.Add(group);
    group.Volunteers.Add(CurrentVolunteer);
    

     

    This creates the link and also updates the entities in the context and is working as expected.

    If I try and do the reverse and remove the association I end up with a request to delete the entity all together!

     

    _dataContext.DeleteLink(CurrentVolunteer, "Groups", group);
     CurrentVolunteer.Groups.Remove(group);
     group.Volunteers.Remove(CurrentVolunteer);

    Results in:

    DELETE http://localhost.:51750/DataService.svc/Volunteers(5)/$links/Groups(4) HTTP/1.1

    DELETE http://localhost.:51750/DataService.svc/Groups(4) HTTP/1.1

    Is this a bug or am I going about it the wrong way??

    Regards,

    Neil

    Thursday, July 22, 2010 9:12 PM

All replies

  • If the CurrentVolunteer.Groups is a DataServiceCollection<T> then it's "by design". DataServiceCollection<T> behaves such that if you remove entity from it, it will mark the entity as deleted. In some cases that's what people want, in some cases it's not. You can override this behavior by providing custom handling methods in the constructor of DataServiceCollection<T> - (for example this constructor http://msdn.microsoft.com/en-us/library/ee652860.aspx). If you pass in your own Func for collectionChangedCallback you get to do what you want there. So you can change the behavior and instead of marking the entity for deletion you don't do that. Note that if you specify the Func, the DataServiceCollection will not perform what it does and it's up to you to propagate the reported change to the context (via calls to AddObject, DeleteObject and so on).

    Thanks,


    Vitek Karas [MSFT]
    Friday, July 23, 2010 12:34 PM
    Moderator
  • If the CurrentVolunteer.Groups is a DataServiceCollection<T> then it's "by design". DataServiceCollection<T> behaves such that if you remove entity from it, it will mark the entity as deleted. In some cases that's what people want, in some cases it's not. You can override this behavior by providing custom handling methods in the constructor of DataServiceCollection<T> - (for example this constructor http://msdn.microsoft.com/en-us/library/ee652860.aspx). If you pass in your own Func for collectionChangedCallback you get to do what you want there. So you can change the behavior and instead of marking the entity for deletion you don't do that. Note that if you specify the Func, the DataServiceCollection will not perform what it does and it's up to you to propagate the reported change to the context (via calls to AddObject, DeleteObject and so on).

    Thanks,


    Vitek Karas [MSFT]

    Hi,

    Thanks for the suggestion. Do you have an example of doing this as I'm not sure how to proceed? My DataServiceCollection<Group> is already instantiated when I call _dataContext.Groups so not sure how to hook into the constructor?

    I understand what you're saying about the "by design" functionality of this but I'm sure there must be many others like me that would not want to remove the actual entity in the relationship so surely it should be an optional parameter rather than needing to implement custom handling.

    Cheers,

    Neil

    Friday, July 23, 2010 2:51 PM
  • I have been dealing with this issue for weeks, and frankly, it's the worst design decision in the whole framework. It is 100% *NOT* the expected behavior when you remove an object from the collection, and if you put 20 developers with no WCF Data Services experience in a room and let them code against this, they would all be really irritated. Spend 5 minutes with it in a WPF Model-View-ViewModel application with a complex data model, and it's not hard to see the problem.

    Part of the problem stems from the fact that you cannot start tracking a collection after it is loaded, which is the 2nd worst design decision and also quite ridiculous. I'm not sure why you didn't implement the code from the EF4 Self-Tracking Entities code, but whatever.

    This is my solution to the problem. You'll need to tack the following code onto the partial class created by the Client proxy:

        public void ProcessCollectionChanged(object source, string sourceProperty, NotifyCollectionChangedEventArgs e)
        {
          if (source == null) throw new ArgumentNullException("source", "The source cannot be null");
          if (string.IsNullOrWhiteSpace(sourceProperty)) throw new ArgumentNullException("sourceProperty", "The sourceProperty cannot be null");
    
          LinkDescriptor ld;
          object changed;
          switch (e.Action)
          {
            case NotifyCollectionChangedAction.Add:
              changed = e.NewItems[0];
              if (changed != null)
              {
                ld = this.GetLinkDescriptor(source, sourceProperty, changed);
                if (ld == null)
                  this.AddLink(source, sourceProperty, changed);
                else
                  if (ld.State == EntityStates.Deleted)
                    this.DetachLink(source, sourceProperty, changed);
              }
              break;
            case NotifyCollectionChangedAction.Remove:
              changed = e.OldItems[0];
              if (changed != null)
              {
                ld = this.GetLinkDescriptor(source, sourceProperty, changed);
                if (ld != null)
                {
                  if (ld.State == EntityStates.Added)
                    this.DetachLink(source, sourceProperty, changed);
                  else
                    this.DeleteLink(source, sourceProperty, changed);
                }
              }
              break;
          }
        }
    
    

    You'll also need this:

        public bool IsLinkTracked(object source, string sourceProperty, object target)
        {
          return (this.GetLinkDescriptor(source, sourceProperty, target) != null);
        }
    

    So now, when loading up NavigationProperties that are collections, you can use code like this (this particular code lives in a WPF ViewModel:

    this.SelectedPhoneNumbers = new ObservableCollection<PhoneNumber>(this.Person.PhoneNumbers).ToList());
    this.SelectedPhoneNumbers.CollectionChanged += (sender1, e1) =>
    {
      App.YourEntitiesContext.ProcessCollectionChanged(this.Person, "PhoneNumbers", e1);
    };
    
    

    and when you Add or Remove from the collection, it will work as expected.

    I sure hope you guys fix this behavior as an out-of-band release ASAP, because I almost had to throw away all my code and move to WCF Rest Services, and it's a real shame that you guys let something as awesome as WCF Data Services v4 be ruined by such an ill-conceived implementation.

    Friday, July 23, 2010 7:17 PM
  • @interscape: I would have to agree this is almost a show stopping problem with Data Services. The workaround provided seems like way too much custom code to be writing into applications for a pretty basic requirement of supporting many-to-many relationships. And to be honest I'm struggling to understand how to implement said workaround in my code.

    I suggest you vote for this to be fixed here:

    https://connect.microsoft.com/data/feedback/details/534068/delete-from-many-to-many-relationship-using-tracking-dataservicecollection#tabs

    Regards,

    Neil

    Saturday, July 24, 2010 11:51 AM
  • 1. Generate your DataServiceClient reference.
    2. Create a new Partial class for the client generated in #1.
    3. Add the following code to that class:

        public void ProcessCollectionChanged(object source, string sourceProperty, NotifyCollectionChangedEventArgs e)
        {
          if (source == null) throw new ArgumentNullException("source", "The source cannot be null");
          if (string.IsNullOrWhiteSpace(sourceProperty)) throw new ArgumentNullException("sourceProperty", "The sourceProperty cannot be null");

          LinkDescriptor ld;
          object changed;
          switch (e.Action)
          {
            case NotifyCollectionChangedAction.Add:
              changed = e.NewItems[0];
              if (changed != null)
              {
                ld = this.GetLinkDescriptor(source, sourceProperty, changed);
                if (ld == null)
                  this.AddLink(source, sourceProperty, changed);
                else
                  if (ld.State == EntityStates.Deleted)
                    this.DetachLink(source, sourceProperty, changed);
              }
              break;
            case NotifyCollectionChangedAction.Remove:
              changed = e.OldItems[0];
              if (changed != null)
              {
                ld = this.GetLinkDescriptor(source, sourceProperty, changed);
                if (ld != null)
                {
                  if (ld.State == EntityStates.Added)
                    this.DetachLink(source, sourceProperty, changed);
                  else
                    this.DeleteLink(source, sourceProperty, changed);
                }
              }
              break;
          }
        }

        public bool IsLinkTracked(object source, string sourceProperty, object target)
        {
          return (this.GetLinkDescriptor(source, sourceProperty, target) != null);
        }

    The method is called much like the "SetLink" method on the data context is, where the first parameter is the object containing the many-to-many relationship, the second parameter is a string with the Property name to change, and the 3rd parameter is the CollectionChangedEventArgs generated by the event.

    4. When you want to DataBind a Many-to-Many collection to UI (say, in a WPF application), create a new ObservableCollection and load it up with the Many-to-Many property as the reference. Example:

    ObservableCollection<PhoneNumber> SelectedPhoneNumbers = new ObservableCollection<PhoneNumber>(somePerson.PhoneNumbers).ToList());

    5. Add the following code beneath it:

    SelectedPhoneNumbers.CollectionChanged += (sender1, e1) =>
    {
      yourEntitiesContext.ProcessCollectionChanged(this.Person, "PhoneNumbers", e1);
    };

    This created an Anonymous Method event handler for your collection. When it changes, the ProcessCollectionChanged event will be called. The method basically checks to see what the operation on the collection is, checks to see if the link in question is being tracked, and responds accordingly. For example, if you try to remove an object that has been Added to the context, but not persisted yet, then it will simply detach it from the Context. Otherwise, it Deletes it, which translates to a command that is sent back to the server.

    It's completely ridiculous that this is even necessary, especially when the EF4 Self-Tracking Entities includes the exact same functionality, but what can you do?

    I'm looking into the possibility of using the Self-Tracking Entites T4 code generation templates to generate code that would be compatible with the DataServiceContext, but leverage an alternative to the DataServiceCollection that would work as expected. I won't have time to investigate it further till the end of August, but if I find anything, I'll post it here.

    Anyways, I hope this helps.

    • Proposed as answer by interscape Sunday, July 25, 2010 3:17 AM
    Sunday, July 25, 2010 3:17 AM
  • BTW, I apologize if my original post was mean or snarky... it stems from the dozens of wasted hours trying to come up with this solution, and the fact that I can't come up with any scenario where deleting the object involved in the link would *ever* be the expected behavior.
    Sunday, July 25, 2010 3:31 AM
  • @interscape: I appreciate your posting your solution. I've also managed to workaround the problem using the OnCollectionChanged override that was suggested, but only to encounter the next problem! I've posted a new thread for it here:

    http://social.msdn.microsoft.com/Forums/en-US/adodotnetdataservices/thread/22d88a70-d8c1-4354-a763-9a0b8aed434b

    Cheers,

    Neil

    Monday, July 26, 2010 12:25 PM
  • Hi,

    Your solution fixes the deletion of the parent entity but the child collection still contain the deleted record.Since the deletion is done on a copy of the child collection, the context does not see these changes (although they are sent to the database). If I re query the child collection, the deleted record is still there.

    Thanks

    Madani

    Wednesday, February 13, 2013 6:55 AM