轻而易举:保存时的多对多问题 [英] breeze: many-to-many issues when saving

查看:50
本文介绍了轻而易举:保存时的多对多问题的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

一段时间以来,我一直在微风应用程序中与多对多关联进行斗争。我在客户端和服务器端都有问题,但是现在,我将只介绍客户端问题。我不知道我想出的方法是否正确,我真的很想从微风团队那里获得有关以下方面的反馈:



我的业务模型:

 公共类Request 
{
public virtual IList< RequestContact> RequestContacts {组; }
}

公共类RequestContact
{
public virtual Contact Contact {get;组; }
公共虚拟Guid ContactId {get;组; }
个公共虚拟请求请求{get;组; }
公共虚拟Guid RequestId {get;组; }
}


公共类联系
{
公共虚拟客户客户{组; }
公共虚拟Guid? ClientId {get;组; }
公共虚拟字符串用户名{get;组; }
}

在getRequest查询的成功回调中,添加 contacts 属性添加到请求中,

  request.contacts = [] ; 
request.requestContacts.forEach(function(reqContact){
request.contacts.push(reqContact.contact);
});

视图绑定到联系人数组,该数组在控制器中定义:

 < select ng-multiple = true Multiple class = multiselect data-placeholder = Select Contacts ng-change = updateBreezeContacts() ng-model = request.contacts ng-options = c作为联系人中c的c.username |过滤器:{clientId:request.client.id}通过c.id进行跟踪< / select> 

控制器:

  //多重选择绑定到的集合:

$ scope.contacts = dataService.lookups.contacts;

无论何时在多选中选择或取消选择一项,此方法都称为:

  $ scope.updateBreezeContacts = function(){
//清除所有RequestContact实体
$ scope.request.requestContacts .forEach(function(req){
req.entityAspect.setDeleted();
});

//根据选定的联系人
填充RequestContact,其中(var i = 0; i <$ scope.request.contacts.length; i ++){
var requestContact = dataService.createRequestContact($ scope.request,$ scope.request.contacts [i]);
$ scope.request.requestContacts.push(requestContact);
}

dataService的createRequestContact方法实际上是这样做的:

  manager.createEntity('RequestContact',{request:myRequest,contact:myContact}); 

用例场景:







  • 该请求有一个选定的联系人。

  • 用户取消选择联系人,然后从列表中选择另一个联系人。

  • 然后她决定重新选择以前未选择的那个。现在,我们有两个选定的联系人。

  • 用户单击保存按钮,然后调用saveChanges。 Breeze将3个实体发送到服务器:第一个具有已删除状态的联系人,同一联系人再次具有已添加状态,最后是另一个已选择的联系人,也具有已添加状态。






与多对多协会合作时应该这样做吗?



我实际上遇到了服务器错误(非空属性引用了null或瞬态值Business.Entities.RequestContact.Request),但是在得出任何结论之前,我想知道我在客户端执行的操作是否正确。

解决方案

服务器端



您首先要解决服务器端建模问题。我在对您的问题的评论中指出没有PK。我建议您先解决该问题,然后再与客户打扰。



客户端



我有在这种情况下的长期经验。对我来说,一个典型的例子是一个用户,可以拥有任意数量的角色,并且他/他拥有的角色都位于UserRoles表中。



典型的UI:




  • 选择并显示一个用户

  • 显示该用户所有可能角色的列表,并带有一个前面的复选框

  • 如果用户具有该角色,则选中该复选框;未经检查,如果他/她没有



呃哦



很多次已经看到人们将所有可能角色的列表绑定到 UserRole 实体的列表。



很多次我见过人们创建和销毁 UserRole 实体当用户单击复选框时。



我经常看到 UserRole entities 添加和删除,添加和删​​除到缓存。这通常是致命的,因为客户端此时无法跟踪 UserRole 实体是否与数据库中的记录相对应。



如果我正确阅读了您的代码,那么您正在犯这些错误。



Item ViewModel代替



当我将该用户的角色表示为 Item ViewModel实例的列表并推迟实体操作直到保存用户的选择时,我获得了更大的成功。



在我们的讨论中,我们将此对象称为 UserRoleVm 。在JavaScript中可能定义如下

  {
角色,
isSelected,
userRole
}

构建屏幕时,




  • 填充 UserRoleVm 实例的列表,每个 Role


  • 为每个虚拟机的 role 属性设置相应的 Role 实体


  • 将视图绑定到 vm.role.name


  • 使用相关用户的 UserRole 设置每个vm的 userRole 属性实体当且仅当该实体已经存在


  • 设置虚拟机的 isSelected = true ,如果虚拟机具有 userRole ,并且未删除 vm.userRole.entityAspect.entityState


  • 将vm的 isSelected 绑定到复选框




现在,用户可以随意选中和取消选中。



在这种情况下,我不会创建/删除/修改任何 UserRole 实体。我等待UI信号保存(无论该信号是什么)。



在保存准备过程中,我遍历 UserRoleVm的列表实例




  • 如果未选中并且没有 vm.userRole ,不执行任何操作


  • 如果未选中并具有 vm.userRole ,则 vm.userRole.entityAspect.setDeleted()。如果 vm.userRole.entityAspect.entityState 已分离(表示以前处于添加状态),则设置 vm.userRole = null。


  • 如果选中并且没有 vm.userRole ,则创建一个新的 UserRole 并将其分配给 vm.userRole


  • 并具有 vm.userRole ,则如果 vm.userRole.entityAspect.entityState




    • 未更改,不执行任何操作

    • 已修改(为什么?如何?),可通过调用 vm.userRole还原。 entityAspect.rejectChanges()

    • 已删除(必须是以前未存在的 UserRole ,但仍未保存;这是怎么发生的?),通过调用 vm.userRole.entityAspect.rejectChanges()

    进行还原



现在调用 manager.saveChanges()




  • 如果保存成功,一切都很好。


  • 如果失败,最干净的方法是调用 manager.rejectChanges()。这会清除所有内容(并丢弃用户自上次保存以来所做的任何更改)。


  • 无论哪种方式,都像在开始时一样从头开始重建列表。




理想情况下,除非异步保存成功或失败,否则您不要让用户对用户角色进行更多更改



我相信您可以比这更聪明。



变化
不要为 UserRoleVm.userRole 。不要在 UserRoleVm 中携带现有的 UserRole 实体。相反,在初始化 UserRoleVm.isSelected 属性时,请参考用户缓存的 UserRole 实体。然后在准备存储期间评估列表,根据相同的逻辑查找并调整缓存的 UserRole 实例。



保存按钮(12月19日更新)



山姆问:


保存当EntityManager更改时,按钮的Disabled属性绑定到设置为true的属性。但是,由于我的ViewModel不是EntityManager的一部分,因此当用户添加/删除联系人时,这不会更改附加到EntityManager的模型。因此,永远不会启用保存按钮(除非我更改模型的另一个属性)。您能想到一种解决方法吗?


是的,我可以想到几个。


  1. 使用get和set方法将 isSelected 属性定义为ES5属性;在set方法中,您向外部 VM发出信号,通知 UserRoleVm 实例已更改。这是可能的,因为如果Angular和Breeze一起工作,则必须使用ES5浏览器。


  2. 将ngClick(或ngChanged)添加到复选框绑定到外部 vm中的函数的html,例如

     
    < li ng-repeat =在vm.userRoles中的角色>
    ...
    <输入类型=复选框
    ng-model = role.isSelected
    ng-click = vm。 userRoleClicked(role)< /输入>
    ...
    < / li>


  3. 利用angular对视图已更改检测的本地支持(我认为是 isPristine)。我通常不会这样,所以我不知道细节。只要您不允许用户离开此屏幕并返回并希望保留对 UserRoleVm 列表的未保存更改,此方法就可行。


vm.userRoleClicked 可以设置 vm.hasChanges 属性设置为true。将保存按钮的isEnabled绑定到 vm.hasChanges 。现在,当用户单击复选框时,保存按钮将亮起。



如前所述,保存按钮单击操作会在 userRoleVm 列表上进行迭代,创建并删除 UserRole 实体。当然,这些操作由 EntityManager 检测。



您可能会觉得更奇特。您的 UserRoleVm 类型可以在创建时记录其原始选定状态( userRoleVm.isSelectedOriginal )和您的 vm.userRoleClicked 方法可以评估整个列表,以查看当前所选状态是否与原始所选状态不同...并设置 vm.hasChanges 相应地。一切都取决于您的UX需求。


别忘了清除 vm.hasChanges 每当您重建列表时。


我想我更喜欢#2;



2014年2月3日更新:punker中的示例



我'我写了一个插件来演示多对多复选框技术 readme.md 解释了所有内容。


I've been struggling for a while with many-to-many associations in a breeze app. I have issues both on the client and server side but for now, I'll just expose my client-side issue. I don't know if the approach I've come up with is correct or not, and I would really like to get feedback from the breeze team on this:

My business model:

public class Request 
{
    public virtual IList<RequestContact> RequestContacts { get; set; }
}

public class RequestContact 
{
    public virtual Contact Contact { get; set; }
    public virtual Guid ContactId { get; set; }
    public virtual Request Request { get; set; }
    public virtual Guid RequestId { get; set; }
}


public class Contact 
{
    public virtual Client Client { get; set; }
    public virtual Guid? ClientId { get; set; }
    public virtual string Username { get; set; }
}

In the success callback of my getRequest query, I add a contacts property to the Request and I populate it :

request.contacts = [];
request.requestContacts.forEach(function (reqContact) {
    request.contacts.push(reqContact.contact);
});

The view is bound to a contacts array, defined in the controller:

<select ng-multiple="true" multiple class="multiselect" data-placeholder="Select Contacts" ng-change="updateBreezeContacts()" ng-model="request.contacts" ng-options="c as c.username for c in contacts | filter:{clientId: request.client.id} track by c.id"></select>

The controller:

//the collection to which the multiselect is bound: 

$scope.contacts = dataService.lookups.contacts;

whenever an item is selected or unselected in the multiselect, this method is called:

$scope.updateBreezeContacts = function () {
  //wipe out all the RequestContact entities
  $scope.request.requestContacts.forEach(function (req) {
    req.entityAspect.setDeleted();
  });

  //populate the RequestContact based on selected contacts 
  for (var i = 0; i < $scope.request.contacts.length; i++) {
     var requestContact = dataService.createRequestContact($scope.request,    $scope.request.contacts[i]);
     $scope.request.requestContacts.push(requestContact);
  }

where the createRequestContact method of the dataService actually does that:

manager.createEntity('RequestContact', { request: myRequest, contact: myContact});

Use case scenario:


  • The request has one selected contact.
  • User unselect the contact and then select another one from the list.
  • Then she decides to reselect the one that was previously unselected. We now have two selected contacts.
  • User hits the save button and the call to saveChanges is made. Breeze sends 3 entities to the server: the first contact with 'Deleted' status, the same contact again with 'Added' status, and finally the other contact that was selected, also with 'Added' status.

Is this what should be done when working with many-to-many associations ?

I actually get a server error ("not-null property references a null or transient value Business.Entities.RequestContact.Request") but before I draw any conclusions, I'd like to know if what I do on the client-side is correct.

解决方案

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.

这篇关于轻而易举:保存时的多对多问题的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆