Brian S
Brian S

Reputation: 5785

Deleting element from an observableArray

I have an ASP.NET MVC app that uses durandal, knockout.js, and breeze on the client. I have an issue that I'm encountering repeatedly, and yet I haven't found any mention of it anywhere. Not sure if I've gotten myself into a unique situation, or if I'm just not searching the right way.

I need to know how to delete a Breeze entity from an observableArray so that the commit succeeds (see Option A below) and the UI reflects the change (see Option B below).

I have the following models (abbreviated):

public class Donor
{
    [Required]
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public int Id { get; set; }

    public virtual IList<Contact> Contacts { get; set; }
}

public class Contact
{
    [Required]
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public int Id { get; set; }

    [ForeignKey("Donor")]
    public int? DonorId { get; set; }
    public virutal Donor Donor { get; set; }
}

I'm trying to delete a Contact from my Donor. I am having difficulty getting the flow right between Breeze and Knockout so that the item is both removed from the observableArray (with a notification) and also able to be deleted through Breeze.

Here is what I have tried (javascript):

Option A:

function deleteContact(contact){
    viewModel.donor().contacts.remove(contact);
    contact.entityAspect.setDeleted();
    viewModel.uow.commit();
}

When I use this approach, I get the following error from Breeze.WebApi:

Int32Converter cannot convert from System.Int64

I have looked through the stack, and examined the Breeze source code (though I haven't yet configured a solution to step through it), and the error is coming from Breeze.WebApi.EFContextProvider::RestoreOriginal, where it is restoring original property values to the object. I don't know why it thinks my value is an Int64, but I was not able to find a good work-around, so I tried...

Option B:

function deleteContact(contact){
    contact.entityAspect.setDeleted();
    viewModel.uow.commit();
}

This approach allows me to successfully save the delete (because the item has not been removed from the collection manually, and therefore, doesn't have any "original values"). However, the issue here is that setDeleted effectively removes the item from the observableArray without notifying my knockout bindings that the array has changed. So the item has been removed and deleted, but my UI still shows the item. Future attempts to call donor().contacts.remove(contact) are futile, because the observableArray no longer has the item.

Upvotes: 4

Views: 1076

Answers (3)

Etienne Maheu
Etienne Maheu

Reputation: 3255

TL:DR

Never set the FKs in the initializer of createEntity while also pushing the entity to the parent's navigation property. Keeps to either one of them, but not both.

Underlying principles

After having encountered a similar issue and done much investigations, I would like to propose an alternate answer. The issue you are experimenting is not about how you delete the item but how you create the item.

Breeze is very able when it comes to managing its data context. It knows about navigation properties and foreign keys and how to handle them. As a side effect of the local data context, all observables also posses this intelligence. This is where you probably overlooked something and ended up with this issue.

Tracing breeze's code

What does this all means concretely? Well, there is two ways that you can use to create an object with a parent entity using breeze. The first one is by setting its parent id in the initializer, like so:

var c = manager.createEntity('contact', { 'donorId': 12, 'name': 'bob' });

The other is by adding the entity to the navigation property on the parent's entity in the breeze data context.

var parents = ko.observableArray();
manager.runQuery(..., parents);

var c = manager.createEntity('contact', { 'name': 'bob'});
parents.contacts.push(c);

Both cases have their pros and cons. The problem arises when you try to do both:

var parents = ko.observableArray();
manager.runQuery(..., parents);

var c = manager.createEntity('contact', { 'donorId': 12, 'name': 'bob' });
parents.contacts.push(c);

Breeze tries to optimize its queries when dealing with insertions to prevent UI flickering. When calling push, it disables notifications on the target observable until the entire operation is done. Then, it will call valueHasMutated internally which will trigger a UI refresh. The thing is, calling createEntity interfere with this mechanism and causes the notification to be reinitialized too soon. The push will then save this invalid sate, swap it and reset it, leaving the observable in an invalid state.

When you ultimately call setDeleted the notifications will still be disabled on the observable preventing a UI refresh even though the data is properly pushed in breeze's data context. This will only happen once after inserting a new element. Deleting an element will force the state to be changed to its proper value and all subsequent deletion on the navigation property will trigger a UI refresh.

Looking at your two options

In the end, you only have to use setDeleted to properly remove an entity from a breeze navigation property. There is no need to remove it from the observable manually and, in fact, doing this will reset the foreign key to null which might cause deserialization issues on the server if the type is not nullable in your model or errors when trying to delete the row from the database depending on how your primary key is defined. Option B is the one to go with.

Upvotes: 0

Jay Traband
Jay Traband

Reputation: 17052

If you 'delete' or 'detach' an entity Breeze should remove that entity from any navigation collections automatically. So I would try eliminating the

 viewModel.donor().contacts.remove(contact);  

line and see if calling 'setDeleted' by itself does what you need.

Upvotes: 1

RobH
RobH

Reputation: 3612

Have you tried calling valueHasMutated() on your observable array after using option b?

This will notify subscribers that the observable has changed.

Upvotes: 2

Related Questions