I want to create a form where the user creates a list and then saves it. The list can have an arbitrary number of rows. The user cannot save the list until it is complete (all rows/items are added) and I cannot use javascript to do it all on the clientside before posting.
The form will create a contact, like in an address book or something. The entity looks like this:
public class Contact
{
public string Name { get; set; }
public string Company { get; set; }
public List<ContactRow> ContactRows { get; set; }
}
The ContactRow looks like this:
public class ContactRow
{
public string Type { get; set; }
public string Value { get; set; }
}
So, basically it is a contact with a name and a company. The contact has a list of contact rows, where each row has a type (phone, email, etc) and a value (555 - 12334, test#email.com, etc).
My form will contain two ordinary textboxes for Name och Company respectively. But I'm not sure how to handle the list with contact rows. What I want the user to be able to do is adding new rows, editing rows and deleting rows. All this must be done before the save button i clicked, i.e. the list must be all done when saving.
This is not very hard to to with javascript and I've already done that. But now I want to implement this functionality without javascript. It is ok to post to the server while building up the list, but the list cannot be saved until it is done.
Ideas?
If you do not wish to use javascript a postback whilst building up the list is all you can do.
This user experience is often deemed poor as work is ongoing, but is the best starting point when using progressive-enhancement to make your application more user friendly.
The OP stated Javascript was not an option. I feel your pain as one of our applications had a similar requirement. We have a construct called an updateable list with the same functionality as you outlined.
We perform data handling on the server. Each list is displayed in a table. Each row contains the item data and has an action icon associated with it. The default is to display and we support new, updated and deleted actions.
After the user enters data for an item they press a button to add or remove the item from the list. Selecting existing items allows for an update operation. The list, and status, is determined on the server and the client does not run Javascript.
As #rich.kelly pointed out you will need to go to the server for each operation. It takes longer to do anything and your session gets a workout but it meets the client requirements.
Related
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.
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.
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>
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...
I am trying to implement user management functionality for a web site.
I am using ASP.NET MVC 3, Entity Framework 4.1, MvcScaffolding.
Let's consider the entities:
The user entity:
public class User
{
public int Id
{
get;
set;
}
public string FirstName
{
get;
set;
}
public string LastName
{
get;
set;
}
public virtual ICollection<UserGroup> Groups
{
get;
set;
}
}
The user group entity:
public class UserGroup
{
public int Id
{
get;
set;
}
public string Name
{
get;
set;
}
public virtual ICollection<User> Users
{
get;
set;
}
}
As you see, there is the many-to-many relationship between the user and the user group entities.
So, I would like to have the following UI for editing an user group:
There are two grids:
1. Users grid contains current state of the user group which is being edited.
2. Browse users grid contains all users (except the users which already belong to the user group). When the user row of this grid is clicked, the user will be moved to the users grid. Also, this grid should support paging, filtering and sorting to provide nice user browsing.
So, the user picks users for the user group and then clicks the "Save" button. User group controller class should save the changes.
Now the question: how can the functionality be implemented? Is there any good example for the such many-to-many relationship problem?
If there is no simple solution, what UI for the user group management could you advise me to use?
P.S. I am quite novice with ASP.NET, so I don't realize how to implement such dynamic grids.
Update 1:
I have looked through the jqGrid examples. link
See Advanced -> Multi Select
There is the problem, the checkboxes' selection is reset when you change the filter. How to store all selected IDs despite the filter change?
Telerik has great grid: http://demos.telerik.com/aspnet-mvc/grid/detailsajax.
I could not understand your whole scenario, especially this: "Browse users grid contains all users (except the users which already belong to the user group). When the user row of this grid is clicked, the user will be moved to the users grid".
I think scenario could be this:
If user is at concrete group, two grids are shown:
a) users existing in group, where each row contains user info and button remove
b) users not existing in group, where each row contains user info and button add
In such case, all logic is quite straightforward, you don't need any fancy logic, as everything is on same page.
In user page, there could be a grid with groups, and if user is in that group, in that row is button remove, and if user is not, there is button add. With well chosen user dto for edit view, that will be also quite straigtforward to implement.
i have this problem that has been buggin me for the last hours.
Lets suppose i have this Signup form, that i need to fill it up, all propertieshave the RequiredAttribute, the model is a EF entity named "User".
i have this second edit account details form, and at this moment a specific field ("Username") is no longer required, because i already have it, the user doesnt need to fil it again and in matter of fact it doest enven show up on the Edit form.
The problem:
when posting the second Edit form, obviously i am stucked with the Username RequiredAttribute.
I would solve this just by adding a "Bind" attribute with "Exclude" option, BUT, this is my current model :
public class AccountDetailsModel
{
public User user { get; set; }
public string NEWPASSWORD1 { get; set; } // new password
public string NEWPASSWORD2 { get; set; } // new password comparison
}
and just looks like Bind Attribute with Exclude option doesnt handle complex Model types. I cant get it to work on this scenario.
Im stuck, scratching my heads for a long time now...
How can i overcome this?
I just want to re-use my EF Entity (User) on 2 different views along with its DataAnnotations.
Thanks in advance.
I just want to re-use my EF Entity (User) on 2 different views along with its DataAnnotations.
Here's the problem. You shouldn't do this. I would recommend you setting up view models which are classes specifically tailored for a given view and contain the necessary validation attributes for this view only. To ease the mapping between your EF models and the view models you could use AutoMapper.
Put the UserName in a hidden input field for the details page.
If I have a property whose display format is dependent on the value of another property in the view model how do I create a display template for it?
The combination of field1's display being dependent on field2's value will be used throughout the app and I would like to encapsulate this in a MVC 2 display template.
To be more specific, I've already create a display template (Social.ascx) for custom data type Social that masks a social security number for display. For instance, XXX-XX-1234.
[DataType("Social")]
public string SocialSecurityNumber { get; set; }
All employees also have an employeeID. Certain companies use the employee's social security number as either the whole employee id or as part of it. I need to also mask the employeeID if it contains the social. I'd like to create another display template (EmpID.ascx) to perform this task.
[DataType("EmpID")]
public string EmployeeID { get; set; }
The problem is that I don't know how to get both properties in the "EmpID" template to be able to perform the comparison.
Thanks for the help.
This might not directly answer your question but I'm wondering why the Employee ID is only sometimes marked out. I know there are legal requirements to doing so for the social but the employee ID is (or should be) somewhat sensitive as well. I would think it would be better to default to marking out both unless the logged in user had whatever privileges made them fully readable.
If you can do this that would probably simplify your logic/design somewhat.
Cant you create a custom ViewModel class containg both SocialSecurityNumber and EmployeeID and create a custom editor template for that class?
For a given report, the user will want to have multiple filtering options. This isn't bad when the options are enumerations, and other 'static' data types, however things can get silly fast when you need a select list that is populated by fields stored in a table in the backend.
How do you handle this scenario? I find myself constantly reshaping the View data to accommodate the additional filter fields, but it really is starting to be a bit much tracking not only the selected options, but also the options themselves...
is there not a better way?
I’m currently building out a new reporting section for one of our products at work and am dealing with this same issue. The solution I’ve come up with so far, though it hasn’t been implemented yet so this is still a work in progress, is along the lines of this.
There will be a class that will represent a report filter which will contain some basic info such as the label text and a list of option values.
public enum DisplayStyle
{
DropDown,
ListBox,
RadioList,
CheckList,
TextBox
}
public class FilterOption
{
public string Name { get; set; }
public string Value { get; set; }
public bool Selected { get; set; }
}
public class ReportFilter
{
public string Title { get; set; }
public DisplayStyle Style { get; set; }
public List<FilterOption> Options { get; set; }
}
And then my model will contain a list of these option classes that will be generated based on each report’s needs. I also have a base report class that each report will inherit from so that way I can handle building out the option lists on a per report basis and use one view to handle them all.
public class ReportModel
{
public string Name { get; set; }
public List<ReportFilter> Filters { get; set; }
public DateTime StartDate { get; set; }
public DateTime EndDate { get; set; }
}
Then inside my view(s) I’ll have some helper methods that will take in those option classes and build out the actual controls for me.
public static string ReportFilter(this HtmlHelper htmlHelper, DisplayStyle displayStyle, FilterOption filterOption)
{
switch (displayStyle)
{
case DisplayStyle.TextBox:
return string.Format("<input type=\"text\"{0}>", filterOption.Selected ? (" value=\"" + filterOption.Value + "\"") : string.Empty);
break;
...
}
}
My route would look like this
Reports/{reportID}/start/{startDate}/end/{endDate}/{*pathInfo}
All reports have a start and end date and then optional filters. The catchall parameter will have lists of filter values in the form of “Customer/1,4,7/Program/45,783”. So it’ll be like a key/value pair in list form. Then when the controller loads it’ll parse out those values into something more meaningful.
public static Dictionary<string, string> RouteParams(string pathInfo)
{
if (string.IsNullOrEmpty(pathInfo))
{
return new Dictionary<string, string>();
}
var values = new Dictionary<string, string>();
// split out params and add to the dictionary object
return values;
}
Then it will pass them off to the report class and validate them to make sure they’re correct for that report. Then when the options are loaded for that report anything that’s been set in the URL will be set to Selected in the ReportOption class so their state can be maintained. Then the filter list and other report data will be added to the model.
For my setup some filters will change when another filters selection changes so there will be some AJAX in here to post the data and get the updated filter options. The drilldown will work sort of like the search options at amazon or newegg when you narrow your search criteria.
I hope that all makes sense to someone beside me. And if anyone has some input on improving it I’d be happy to hear it.
You could go and retrieve the data asynchronously on the screen using jQuery and JsonResults from your MVC application, this is how we populate all of our lists and searches in our applications. I have an example of how it is done here.
This way the view data is loaded on demand, if they don't use the extra filters then they don't have to get the view data and if one selection relates to another then it's clear which set of data you need to retrieve.
Another option, though I don't like this one as much but jQuery solution may not suit you, is to have your model object for your view contain all the view data so that all you need to do is set the single model object and all the lists are loaded directly and strongly typed. This will simplify the view and the back end code because it will be more clear that for this view the only thing you need is a complete version of this model object.
For example if you had two lists for combo boxes then your model might look like:
public class MyViewMode
{
public int MyProperty { get; set; }
public string SomeString { get; set; }
List<string> ComboListA { get; set; }
List<string> ComboListB { get; set; }
}
Hope that makes sense, if not please comment and I'll expand on it.
Ad-hoc filtering on reports is indeed a tricky issue especially when you want to show a custom user interface control based on the data type, do validation, make some filters to be dependent on one another and others not, etc.
One thing I think that is worth considering is the old "build vs buy" issue here. There are specialized tools out there for ad-hoc reporting that do provide a UI for ad-hoc filters help with this such as the usual suspects Crystal Reports, Microsoft's Reporting Services, or our product ActiveReports Server. In ActiveReports Server we support cascading prompts (where available values in prompts depend on one another) and make it easy for anyone, even non-technical business users to modify the prompts (assuming they have permissions obviously). More information about using prompts in ActiveReports Server is here. ActiveReports Server is also, all managed .NET code, and provides ASP.NET controls and web services that allows you to integrate it into your web apps.
Scott Willeke
Product Manager - ActiveReports Server
GrapeCity inc.