breeze: many-to-many issues when saving

你说的曾经没有我的故事 提交于 2019-11-28 02:13:41

Server-side

You have server-side modeling problems to deal with first. I noted the absence of PKs in my comment to your question. I suggest that you get that working first, before bothering with the client.

Client-side

I have long experience with this kind of scenario. For me the canonical case is a User who can have any number of Roles and the roles s/he has are in the UserRoles table.

The typical UI:

  • select and present a User
  • present a list of all possible roles for that user with a preceding checkbox
  • the checkbox is checked if the user has that role; unchecked if s/he does not

Uh Oh

Many times I have seen people bind the list of all possible roles to a list of UserRole entities. This rarely works.

Many time I have seen people create and destroy UserRole entities as the user clicks the checkbox. This rarely works.

Too often I have seen UserRole entities added and deleted and added and deleted to the cache. This is usually fatal as the client loses track of whether a UserRole entity does or does not correspond at this moment to a record in the database.

If I read your code correctly, you are making everyone of these mistakes.

Item ViewModel instead

I have had more success when I represented this user's roles as a list of "Item ViewModel" instances and postponed entity manipulation until it was time to save the user's selections.

For our discussion, let's call this object a UserRoleVm. It might be defined as follows in JavaScript

{
    role,
    isSelected,
    userRole
}

When you build the screen,

  • populate a list of UserRoleVm instances, one for every Role

  • set each vm's role property with the appropriate Role entity

  • bind the view to vm.role.name

  • set each vm's userRole property with the pertinent user's UserRole entity if and only if such an entity already exists

  • set vm's isSelected=true if the vm has a userRole and if vm.userRole.entityAspect.entityState is not Deleted.

  • bind the vm's isSelected to the checkbox

Now the user can check and uncheck at will.

I do not create/delete/modify any UserRole entity while this is going on. I wait for the UI signal to save (whatever that signal happens to be).

During the Save preparation, I iterate over the list of UserRoleVm instances

  • if not checked and no vm.userRole, do nothing

  • if not checked and have vm.userRole, then vm.userRole.entityAspect.setDeleted(). If vm.userRole.entityAspect.entityState is Detached (meaning it was previously in the Added state), set vm.userRole = null.

  • if checked and no vm.userRole, create a new UserRole and assign it to vm.userRole

  • if checked and have vm.userRole, then if vm.userRole.entityAspect.entityState is

    • Unchanged, do nothing
    • Modified (why? how?), revert by calling vm.userRole.entityAspect.rejectChanges()
    • Deleted (must have been a pre-existing UserRole that was "unchecked" but still not saved; how did that happen?), revert by calling vm.userRole.entityAspect.rejectChanges()

Now call manager.saveChanges().

  • If the save succeeds, all is well.

  • If it fails, the cleanest approach is call manager.rejectChanges(). This clears the decks (and discards whatever changes the user made since the last save).

  • Either way, rebuild the list from scratch as we did at the beginning.

Ideally you do not let the user make more changes to user roles until the async save returns either successfully or not.

I'm sure you can be more clever than this. But this approach is robust.

Variation Don't bother with UserRoleVm.userRole. Don't carry the existing UserRole entity in the UserRoleVm. Instead, refer to the user's cached UserRole entities while initializing the UserRoleVm.isSelected property. Then evaluate the list during save preparation, finding and adjusting the cached UserRole instances according to the same logic.

Enabling the Save button (update 19 Dec)

Sam asks:

The Save button's disabled attribute is bound to a property set to true when the EntityManager has changes. However, since my ViewModel is NOT part of the EntityManager, when the user adds/removes contacts, that does not change the Model attached to the EntityManager. Therefore the Save button is never enabled (unless I change another property of the model). Can you think of a workaround for that?

Yes, I can think of several.

  1. Define the isSelected property as an ES5 property with get and set methods; inside the set method you signal to the outer VM that the UserRoleVm instance has changed. This is possible because you must be using an ES5 browser if you've got Angular and Breeze working together.

  2. Add an ngClick (or ngChanged) to the checkbox html that binds to a function in the outer vm, e.g.,

    <li ng-repeat="role in vm.userRoles">
        ...
        <input type="checkbox" 
               ng-model="role.isSelected" 
               ng-click="vm.userRoleClicked(role)"</input>
     ...
    </li>
    
  3. Leverage angular's native support for "view changed" detection ("isPristine" I think). I don't usually go this way so I don't know details. It's viable as long as you don't allow the user to leave this screen and come back expecting that unsaved changes to the UserRoleVm list have been preserved.

The vm.userRoleClicked could set a vm.hasChanges property to true. Bind the save button's isEnabled is to vm.hasChanges. Now the save button lights up when the user clicks a checkbox.

As described earlier, the save button click action iterates over the userRoleVm list, creating and deleting UserRole entities. Of course these actions are detected by the EntityManager.

You could get fancier. Your UserRoleVm type could record its original selected state when created (userRoleVm.isSelectedOriginal) and your vm.userRoleClicked method could evaluate the entire list to see if any current selected states differ from their original selected states ... and set the vm.hasChanges accordingly. It all depends on your UX needs.

Don't forget to clear vm.hasChanges whenever you rebuild the list.

I think I prefer #2; it seems both easiest and clearest to me.

Update 3 February 2014: an example in plunker

I've written a plunker to demonstrate the many-to-many checkbox technique I described here. The readme.md explains all.

The Breeze.js client does not support "many to many" relationships at this time. You will have to expose the junction/mapping table as an entity. There are several other posts on this same topic available.

We do plan to add many-many support in the future. Sorry, but no date yet...

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!