Build BigShelf using RIA/JS

Announcement: RIA/JS has now evolved into ASP.NET Single Page Application, and the content has been migrated. The content on this site wll removed by the end of 2012.

Introduction

This walkthrough illustrates how to employ RIA/JS to develop rich, data-centric user experiences for the desktop or mobile browser. The walkthrough scenario covers varied, dynamic user experiences that you see in data-centric web sites like http://mint.com and http://netflix.com as well as more conventional LOB applications employing HTML forms. In this walkthrough, more specifically, you will:

  • use the RIA/JS $.dataSource jQuery plugin to load data from a remote data store (a WCF DomainService) and manage data in a client-side, in-memory cache
  • integrate the data with HTML/JavaScript UI, enabling data visualization and editing
  • see how $.dataSource surfaces tree and graph shaped data as “entities”
  • see how to manage edits to entity fields and associated entities
  • use “entity state” to let users visualize their in-progress data edits
  • see how $.dataSource collects data edits and manages their submission to the remote data store
  • use the client-side query model of $.dataSource to add paging, sorting and filtering

As a starting point, the walkthrough uses a partially developed version of a sample application named BigShelf. The BigShelf application does for the books you read what Netflix does for the movies you watch. It enables you to pick books to read from a collection of available titles, rate titles, and discover new books. It also has a social aspect, where you can create a profile and make friends, which are then used to present book recommendations to you.

The HTML and CSS for BigShelf had been developed previously and starting from this stage allows us to focus on developing the JavaScript data-access features. The unobtrusive JavaScript that renders data dynamically in the starter application was developed to work against mock, in-memory data only; data that consists of a set of JavaScript literal arrays and objects. In the walkthrough, you will replace this mock data using RIA/JS to connect to and modify remote data.

The sequence of steps that make up this walkthrough are as follows:

  1. Load data from a remote data store
  2. Use server-side sorting, paging and filtering to load data
  3. Add editing of BigShelf data, with per-click commit to a remote data store
  4. Apply sorting, paging, filtering queries locally, over in-memory data
  5. Use change-tracking to edit data, with an explicit “Save” step
  6. Add client-side jQuery validation, using the same validation rules specified by the DomainService
  7. Apply entity state to visualize uncommitted data edits and create contextual controls
  8. Enable editing for associated entities

Prerequisites

This walkthrough assumes you are familiar with WCF RIA Services and with JavaScript and jQuery. It also requires the following development tools and samples:

  • Microsoft Visual Studio 2010 SP1
  • Upgrade to WCF RIA Services V1.0 SP2 RC
  • (Optional) If you have an old version of the WCF RIA Services Toolkit on the machine, the sample may not work. To correct this, you have to upgrade to WCF RIA Services Toolkit (September 2011). If you don't have the toolkit installed, you don't need to do anything.
  • (Optional) If you are using SQL Server not SQL Server Express, you will need to modify the connection string. If you are getting errors around user instances, use SQL Server Management Studio to run the command sp_configure 'user instances enabled','1' and then call reconfigure and restart the server instance.
  • The attached starter project and completed project.
  • (Optional) In addition to the starter/completed projects listed above for this walkthrough, a fully functional version is also available here. This version includes further refinements that are not part of this walkthrough.

Inspect the BigShelf starter project

Open the BigShelf starter project in Visual Studio 2010 SP1, build it (F6), and run it (Ctrl+F5) in your browser. When the application starts, click the “Log In” text on the upper right to open the Log In page. Use the demo credentials that are provided in the text of that page to log in. Once logged in, the default BigShelf page should look like this.

image

Recall that the default BigShelf page of this starter application was developed to operate over mock, in-memory data only, data which you can locate in the Scripts\Default.js file. Even in this primitive state, against such mock data, some of the UI interactions work as designed. You can, for example, click on a Save button to put a book in your queue or click on a star rating to assess a book that you’ve completed reading. Note how the status changes to Might read it when you save a book and to Done reading when you rate a book. The Show Me and Sort By selections are also clickable but don’t yet have the desired effect on the list of relevant books.

You can also click on the welcome text to the left of the Log Out button to edit your BigShelf profile. Editing on this profile page is a buffered, form-style experience. This means that data edits must be explicitly committed using the Update Profile button or discarded using the Cancel button.

image

Here all the fields are currently blank and the form is not functional. Once we complete this walkthrough, the form will be able to pull up profile information and edit it.

The component parts of the BigShelf application can be examined in Solution Explorer. BigShelf was developed as a regular ASP.NET Web Application. The front end for BigShelf consists of the two pages shown above. These pages can be found in the top-level BigShelf folder and are named Default.aspx and MyProfile.aspx. The corresponding unobtrusive JavaScript files in the Scripts folder are called Default.js and MyProfile.js, along with CSS file for both pages in Styles\Styles.css. You’ll notice that Default.js contains the mock data that is loaded into memory, mentioned earlier, along with a small amount of jQuery code that allows for a limited BigShelf user experience.

We start with a jQuery List Control, which we bind to the array of mock data. Please find the following code at the top of the render() method in the Default.js file.

  1. // A list UI control over our "books" array.
  2. var booksList = $("#books").list({
  3.     data: books,
  4.     template: $("#bookTemplate"),
  5.     itemAdded: function (book, elements) {
  6.         // Bind edit controls on each book element to a FlaggedBook entity for "profile".
  7.         enableFlaggingForBook(book, elements[0]);
  8.     }
  9. }).data("list");

The List Control renders book data objects using jQuery templating. As each book is rendered by the List Control, it uses the bookTemplate template (which you can find near the top of the Default.aspx page) to create a new table row (<tr>) for every book. The placeholder values in the template are replaced with values from the book being rendered. The last step happens in the enableFlaggingForBook callback, which attaches some extra behaviors to the DOM element before it gets added to the page. One such behavior is the use of a star rating jQuery plug-in. Clicking on the star rating translates into data edits against the Rating field of a FlaggedBook entity. Another behavior is that clicking on the Save button creates a new entity (a new JavaScript object) in the flaggedBooks array.

Refresh your browser here, however, and you’ll find that all edits you’ve made are lost, as they are applied to our mock data in-memory only.

Another interesting chunk of code comes right under the List Control declaration. This control allows us to sort the collection of books in different ways.

  1. // Our "Sort By:" sort control.
  2. $(".sortButton").click(function () {
  3.     var newSort = $(this).data("book-sort"),
  4.         $currentSortElement = $(".sortButton.selected"),
  5.         currentSort = $currentSortElement.data("book-sort");
  6.     if (newSort !== currentSort) {
  7.         // Only clicked sort button gets the "selected" class.
  8.         $currentSortElement.removeClass("selected");
  9.         $(this).addClass("selected");
  10.  
  11.         // Refresh those books displayed, based on the new sort.
  12.         refreshBooksList();
  13.     }
  14. })
  15. .eq(0).addClass("selected");

The sort control adds or removes a selected class based on button clicks and then calls refreshBooksList method, which in the finished application will cause a request to the service for the book list with the new sort applied on it. In the starter project the refreshBooksList method is empty, so the sorting functionality is not implemented yet.

Lastly, we associate an auto-complete control with the <input> field for our search text box. The autocomplete control is part of the jQuery UI library (look at jquery-ui.js). You will also see us use the watermark control, which enables us to place some watermarked default text in the input field. The watermark control comes from the jquery.bb.watermark.min.js file.

  1. $("#searchBox").autocomplete({
  2.     source: function () {
  3.         // A pause in typing in our search control should refresh the search results.
  4.         refreshBooksList();
  5.     },
  6.     minLength: 0
  7. }).watermark();

When the user types something in and pauses, the autocomplete control will call the refreshBooksList method. The exact behavior and properties of the autocomplete are developed as part of jQuery UI and we don’t need to do extra work. Calling the refreshBooksList method will cause a request to the service for the book list with the filter applied. The filter is determined by what the user has typed into the field. In the starter project the refreshBooksList method is empty, so the sorting functionality is not implemented yet.

One of the key objectives of this walkthrough is to replace this mock data with data that is loaded from a remote data store using RIA/JS. The back end for the BigShelf application, which you connect the BigShelf client to as part of the walkthrough, has been developed as a WCF DomainService. The DomainService wizard was run on the EF Code First model defined in the Models folder (BigShelfModel.cs). The wizard generated an initial DomainService for that model, which was then modified to add some application logic. You can see the resulting service in the Services/BigShelfService.cs file.

The start project also contains intermediate versions of the script files that will be changed during the walkthrough - if you have any problems in any of the procedures, you can try comparing those versions with the code you have. They're located in the Scripts folder.

Walkthrough

Step 1: Load data from a remote data store

Our first step is to delete the mock, literal data from our books array at the top of the Default.js file. In its place, we’ll apply the $.dataSource plug-in to our books array, supplying the service URL and query method from which to fetch data. Replace the declaration of the “books” variable with the following code:

  1. var books = [];
  2. $([books]).dataSource({
  3.     serviceUrl: serviceUrl,
  4.     queryName: "GetBooksForSearch",
  5.     bufferChanges: false
  6. });

By applying this plug-in, we’ve established a binding between our books array and the new data source instance specified by the serviceUrl and queryName. This data source manages the loading of data into the books array. The data source also listens for changes to the data it contains. It manages such data changes and is responsible for committing the changes back to a remote data store via the DomainService.

The options serviceUrl and queryName specify the service and query method on that service from which we’ll load our data. The bufferChanges option indicates whether data changes are to be submitted back to our service directly or whether they should be accumulated in a change list. In this step, changes applied to data from this data source will be committed directly to our service by way of XmlHttpRequest POSTs, as we’ve elected not to buffer changes.

So far, we’ve merely established a binding between our books array and a new data source instance. To cause the data source to load data into its array, we call refresh on the data source instance. Here, we follow a conventional jQuery plug-in pattern. Earlier we instantiated a data source by supplying options as we applied the plug-in. Here, we retrieve the previously instantiated data source by reapplying the plug-in without options. Next we replace the entire refreshBooksList function in the helper function section of the Default.js file with the following code.

  1. function refreshBooksList(all) {
  2.     var dataSource = $([books]).dataSource();
  3.  
  4.     dataSource.refresh({ all: all });
  5. };

With the addition of this refresh call, we are now loading data from our remote DomainService when you refresh your browser. The data source issues an XmlHttpRequest GET to the service and then deserializes the HTTP response body into JavaScript objects, caches these in-memory (in a hidden “data context”) and then places them in the books target array for use by the application.

We have a modest number of books in the database for this application, or this loading could take some time. In the next step of our walkthrough, we’ll make modifications to limit the number of books that we load when a user clicks in the UI.

Step 2: Use server-side sorting, paging and filtering to load data

Note that this refreshBooksList function just implemented in Step 1 is called in two other places in the Default.js file outside of the call at the bottom of the render() method. In the sort control, refreshBooksList is called whenever a new sort button is clicked. In the jQuery autocomplete control used for filtering books by title, refreshBooksList is called whenever the user pauses while typing in our search box.

What we’ll do next is modify the implementation of the refreshBooksList function to set query options on our data source that control the data loaded into our books array so as to match that specified in our sort and filter controls. Modify the refreshBooksList function to look like this:

  1. function refreshBooksList(all) {
  2.     var dataSource = $([books]).dataSource();
  3.  
  4.     var filter = {
  5.         property: "Title",
  6.         operator: "Contains",
  7.         value: $("#searchBox").val() || ""
  8.     };
  9.     dataSource.option("filter", filter);
  10.  
  11.     dataSource.option("sort", { property: $(".sortButton.selected").text() });
  12.  
  13.     dataSource.refresh({ all: all });
  14. };

This is a common use pattern for a data source. On first render or in a click handler (for instance), an application will dynamically modify query options on a data source and proceed to refresh it.

With this simple query/load model, reusable jQuery controls can be developed that similarly affect what data is loaded. Use the pattern just exhibited with refreshBooksList to implement a simple pager control, that sets “paging” options on the data source and calls refresh, by adding the following code snippet above the call to refreshBooksList() in the body of the render() method. The pager control control is something we developed for the purposes of this sample, but you could image this being replaced by a jQuery UI control down the line.

  1. $("#pager").pager({
  2.     dataSource: $([books]).dataSource(),
  3.     pageSize: 6
  4. });

The query-processing behind the paging, sorting and filtering functionality we have developed here is accomplished server-side. On refresh , the data source translates its paging/sort/filter options into the query portion of the URL that is used by an XmlHttpRequest GET to load the data. In our DomainService, this query is translated into an equivalent SQL query against the BigShelf database. Note that only the paged/sorted/filtered subset of data is transmitted from our remote store to our client-side data source.

Step 3: Add editing of BigShelf data, with per-click commit to a remote data store

At this point in the walkthrough, clicking on the Save button or star rating for a given book still modifies the mock, literal data in our flaggedBooks array. To make these user interactions modify data in our remote store, we’ll first change our app so that it loads the flaggedBooks array with data from our remote store. First, delete the flaggedBooks literal AND the call to render() near the start of the Default.js file and replace it with the following code. Note that this code follows the same pattern we saw in Step 1.

  1. var flaggedBooks;
  2. $.dataSource({
  3.     serviceUrl: serviceUrl,
  4.     queryName: "GetProfileForSearch",
  5.     refresh: function (profiles) {
  6.         flaggedBooks = profiles[0].get_FlaggedBooks();
  7.         render();
  8.     }
  9. }).refresh();

One noteworthy difference here is that we’re calling $.dataSource as a static jQuery method and not in the plug-in style we used earlier. This illustrates how $.dataSource can be used in a common JavaScript request/response pattern. In this scenario, we delay the first render of our books until we’ve loaded our flaggedBooks data, as the Save button and star rating control are not usable until this editable data is available to our page. This is the result of moving the call to render() into the callback after $.dataSource has loaded its data.

According to the BigShelf database schema, adding a new entity to our flaggedBooks array requires that its ProfileId foreign key column be set. It is the data source that fills in this foreign key value, as part of its entity data model support. Since the flaggedBooks array comes from a property on our profile entity, the data source uses the primary key value of profile for the foreign key of any adds to flaggedBooks. Those who are familiar with the Entity Framework and WCF RIA Services in Silverlight should appreciate the simplicity of navigating and editing data without having to make direct use of foreign key values. Alternatively, should you care to set foreign key fields directly, computed properties like profile.get_FlaggedBooks and someFlaggedBook.get_Profile will stay in sync with modified foreign key values.

Compile and run the application and refresh your browser here, you’ll discover that the Save button and star rating control have the desired effect on data from our remote store (save and rate some books, then refresh your browser to see this). To understand why this was accomplished by merely loading the flaggedBooks array via $.dataSource, we’ll take a look at the implementation of enableFlaggingForBook() function. You do not need to paste this code, it should already be in the script file.

  1. function enableFlaggingForBook(book, bookElement) {
  2.     // Will be null if current profile hasn't yet saved/rated this book.
  3.     var flaggedBook = getFlaggedBook(book),
  4.         $button = $("input:button[name='status']", bookElement),
  5.         ratingChanged;
  6.  
  7.     if (flaggedBook) {
  8.         // Style the Save button based on initial flaggedBook.Rating value.
  9.         styleSaveBookButton();
  10.  
  11.         // Clicks on the star rating control are translated onto "flaggedBook.Rating".
  12.         ratingChanged = function (event, value) {
  13.             $.observable(flaggedBook).setProperty("Rating", value.rating);
  14.             styleSaveBookButton();
  15.         };
  16.     } else {
  17.         // If this book has not yet been flagged by the user create a new flagged book
  18.         flaggedBook = { BookId: book.Id, Rating: 0 };
  19.  
  20.         // Clicking on the Save button will add the new flagged book entity to "flaggedBooks".
  21.         $button.click(function () {
  22.             $.observable(flaggedBooks).insert(0, flaggedBook);
  23.             styleSaveBookButton();
  24.         });
  25.  
  26.         // Clicks on the star rating control are translated onto "flaggedBook.Rating". Also, since the book
  27.         // was not previously flagged, this will also add a new flagged book entity to "flaggedBooks".
  28.         ratingChanged = function (event, value) {
  29.             $.observable(flaggedBook).setProperty("Rating", value.rating);
  30.             $.observable(flaggedBooks).insert(0, flaggedBook);
  31.             styleSaveBookButton();
  32.         };
  33.     }
  34.  
  35.     // Bind our ratingChanged method to the appropriate event from the starRating control
  36.     $(".star-rating", bookElement)
  37.         .starRating(flaggedBook.Rating)
  38.         .bind("ratingChanged", ratingChanged);
  39.  
  40.     function styleSaveBookButton() {
  41.         $button
  42.             .val(flaggedBook.Rating > 0 ? "Done reading" : "Might read it")
  43.             .removeClass("book-notadded book-saved book-read")
  44.             .addClass(flaggedBook.Rating > 0 ? "book-read" : "book-saved")
  45.             .disable();
  46.     };
  47. };
  48.  
  49. function getFlaggedBook(book) {
  50.     return $.grep(flaggedBooks, function (myFlaggedBook) {
  51.         return myFlaggedBook.BookId === book.Id;
  52.     })[0];
  53. };

First, let’s consider how the star rating control works. Depending on whether the book has been rated or flagged before, we define two different ratingChanged handlers on lines 12 and 28. If you look at line 36, you see that those handlers execute when the user clicks the star rating control for a given book. In these handlers, when the user selects a rating, the $.observable setField method (in jquery.observable.js) changes the Rating property on the flagged book and triggers a propertyChange event. In this particular scenario, our data source is listening on these events. In this way, in response to a click in the star rating control, the data source commits this data change by issuing an XmlHttpRequest POST.

Similarly, when the user clicks the Save button on a book (or rates a book for the first time), an entity is added to our flaggedBooks array using the insert method from the $.observable jQuery plug-in (lines 22 and 30). This insert method – along with remove and move – raises arrayChange events in the course of modifying an array. The data source listens on these events and, as it did for events from $.observable setField method, translates these events into XmlHttpRequest POSTs to our remote store. You can witness this yourself using Fiddler or the Network tab in your browser tools as you click on a Save button or a star rating control.

Step 4: Apply sorting, paging, filtering queries locally, over in-memory data

To enable fast, responsive sorting, filtering, and paging in our HTML/JavaScript application, we can use a local copy of the data stored in-memory on the client. Our books array is currently populated via a request to the remote DomainService. To enable local querying, we will add a second array which will be a filtered, local view of the remote data. We will associate a new data source with that array, which will chain to the original “remote” data source. To illustrate that, let’s rename the books array to remoteBooks. Then we’ll add a new books array, this time for the local data. This way all the logic we have already written against $([books]).dataSource() will continue to work, only that now it will execute against a client-side view of the data.

In the code, we replace the previous definition of the books array and $([books]).dataSource() with this:

  1. var remoteBooks = [];
  2. $([remoteBooks]).dataSource({
  3.     serviceUrl: serviceUrl,
  4.     queryName: "GetBooksForSearch",
  5.     bufferChanges: false
  6. });
  7. var books = [];
  8. $([books]).dataSource({
  9.     inputData: remoteBooks
  10. });

Notice how both data sources are chained by specifying that the input data of the second data source is the remoteBooks array (line 9 above).

The actual paging, sorting, and filtering operations take place in the refreshBooksList() method. Notice that we didn’t make any changes to that method. Well… almost. We only want to refresh the remote data source the first time the page loads. Subsequent times when a new filter or sort is applied by UI controls, we only want to refresh the local data source. This is expressed by passing the all parameter on the data source (line 13 below). If all is true, then all data sources chained to this one are refreshed. If all is false, only the current data source is refreshed. The code below should already be in the script file, do not paste it again.

  1. function refreshBooksList(all) {
  2.     var dataSource = $([books]).dataSource();
  3.  
  4.     var filter = {
  5.         property: "Title",
  6.         operator: "Contains",
  7.         value: $("#searchBox").val() || ""
  8.     };
  9.     dataSource.option("filter", filter);
  10.  
  11.     dataSource.option("sort", { property: $(".sortButton.selected").text() });
  12.  
  13.     dataSource.refresh({ all: all });
  14. };

So we need to make one small change. At the bottom of the render() method pass true to refreshBookList() so all is set to true only the first time the page loads.

  1. refreshBooksList(true);

$([books]).dataSource() is now working against local data, and the same sorting and filtering operations are supported. The paging, sorting and filtering API remains the same, only now it’s much more responsive because it’s evaluated in-memory now. Try it.

Step 5: Use change-tracking to edit data, with an explicit “Save” step

We have completed all the changes needed for the main page to work, so let’s now focus our attention on the profile page, which can be reached by clicking on the “welcome” text on the top right of the window. You will see an empty HTML form that we would use to edit the current user’s profile data.

This presents a different problem than the front page. On the front page we browsed a large collection of entities and sorted, paged, and filtered them according to our UI. There was some limited editing in the form of starring a book or flagging it for later. All edits were written through directly to the server. On this page we are now focusing on a single entity and its fields. We want to enable the user to edit these fields in a batch and submit the changes together, or revert one or all of them. Validation is another concern in this context as the fields contain different types of data. All of these concerns will be addressed below.

First, let’s populate the form fields with the entity fields. Open the Scripts/MyProfile.js file and use a data source to bring in the current user profile entity from the server. This should feel familiar from the first part of this walkthrough. Let’s replace the call to render() with the following code, so the render() method will only get called once the profile entity is loaded.

  1. var serviceUrl = 'BigShelf-BigShelfService.svc';
  2.  
  3. // Load our "profile" record
  4. var profile,
  5.     profileDataSource = $.dataSource({
  6.         serviceUrl: serviceUrl,
  7.         queryName: "GetProfileForProfileUpdate",
  8.         bufferChanges: true,
  9.         refresh: function (profiles) {
  10.             profile = profiles[0];
  11.             render();
  12.         }
  13.     }).refresh();

Notice that we are setting bufferChanges to true on line 8. This is different from the first part of this walkthrough, since we now buffer all changes before submitting them to the server. Instead of every change being written immediately, the data source will wait until the commitChanges() method is called.

As a next step, let’s use data-linking to bring the profile information into the UI. Paste the following at the top of the render() method:

  1. // Data-link "profile" in the <form> and page title, so our UI updates live.
  2. $("#profileForm").link(profile);
  3. $("#profileName").link(profile);

On line 2 we use the data-linking to establish a bi-directional link to <input> fields in the HTML form. Note that on MyProfile.aspx, the template for linking chooses which field from the profile it will link to which element of the controls. On line 3 we set up a link to the title of the page so it can update as the user changes their name, and we use a function (firstName) to modify the linked data prior to displaying on the page.

If we run the sample at this stage, you’ll notice that the scalar fields of the profile object now show up in the UI. If you change the name “Demo User” to “DemoJS User” and tab out of the control, you’ll notice that the page heading also updates (“Hey DemoJS …”), showing that data-linking is in fact working. However all edits are lost if you refresh the page, so let’s add the logic needed to save the data back to the server, or cancel the pending changes. Paste the following at the bottom of the render() method, before the helper functions.

  1. // Temporary: enable Save and Cancel buttons
  2. $("#submit, #cancel").enable();
  3. $("#submit").click(function () { profileDataSource.commitChanges(); });
  4. $("#cancel").click(function () { profileDataSource.revertChanges(true); });

You can now try running the application again, and you should be able to save changes to the user profile, or cancel any changes made. You can try that by changing a value, hitting Update Profile, and then refreshing the page.

Step 6: Add client-side jQuery validation, using the same validation rules specified by the DomainService

We can now save data back to the server, but we offer no validation to the user to ensure they are entering data in the format we expect.

Add the following code to the bottom of the render() method, before the helper methods.

  1. // Set up validation of property fields, validation rules come from
  2. // the server where they were extracted from data annotations on the object's type
  3. $("#profileForm").validate({
  4.     rules: profileDataSource.getEntityValidationRules().rules,
  5.     errorPlacement: function (error, element) {
  6.         error.appendTo(element.closest("tr"));
  7.     }
  8. });

We use the jQuery validate plugin and we associate it with profileForm. If we were doing validation manually, we would have to define validation rules either inline in the HTML form, or hand-craft an object and pass it to the rules property. Instead, we see that by using the getEntityValidationRules() method on the data source, we get a pre-configured object that we can pass to the rules property. The rules are generated by extracting the data annotations we’ve applied to the model types exposed by our DomainService, and flow automatically to the client. Observe the validation attributes (e.g. the Required and DataType attributes on EmailAddress) in the server-side Profile type, which exists already (you do not need to paste this).

  1. public partial class Profile
  2. {
  3.     public Profile()
  4.     {
  5.         this.Categories = new HashSet<Category>();
  6.         this.FlaggedBooks = new HashSet<FlaggedBook>();
  7.         this.Friends = new HashSet<Friend>();
  8.     }
  9.  
  10.     public int Id { get; set; }
  11.  
  12.     [Required]
  13.     public string Name { get; set; }
  14.  
  15.     [Required, DataType(DataType.EmailAddress)]
  16.     public string EmailAddress { get; set; }
  17.  
  18.     public string AspNetUserGuid { get; set; }
  19.  
  20.     [Include]
  21.     public virtual ICollection<Category> Categories { get; set; }
  22.  
  23.     [Include]
  24.     public virtual ICollection<FlaggedBook> FlaggedBooks { get; set; }
  25.  
  26.     [Include]
  27.     public virtual ICollection<Friend> Friends { get; set; }
  28. }

Now we have validation behavior associated with our form, but clicking the Update Profile button will cause the data source to commit the changes, regardless if the form is valid. Replace the code under the comment starting with “Temporary: enable Save and Cancel buttons” with the following code to ensure that the form will only be submitted if all the fields are valid.

  1. // Temporary: enable Save and Cancel buttons
  2. $("#submit, #cancel").enable();
  3. $("#submit").click(function () {
  4.     if ($("#profileForm").valid()) {
  5.         profileDataSource.commitChanges();
  6.     }
  7. });
  8. $("#cancel").click(function () { profileDataSource.revertChanges(true); });

Now you can run the sample and you’ll notice that if you try to enter a malformed email address and tab out of the field, you’ll get a validation error:

image

Likewise, if you leave any field blank and you hit Update Profile, you will also see an error.

Step 7: Apply entity state to visualize uncommitted data edits and create contextual controls

We have rudimentary functionality implemented to read and save form data fields to the server. However our application is fairly static at this stage and the user’s experience when editing the data is quite poor. By learning more about the state of the profile entity which we are editing, we can provide a much richer editing experience, for example:

  • If a field in the profile entity has been edited, we can highlight the corresponding <input> field
  • If a field in the profile entity has been edited, we can contextually provide an “undo” button to let the user easily revert just that field change
  • If any field on the profile entity has changed, we can contextually enable the Cancel button
  • If any field on the profile entity has changed, and all changed fields pass validation, we can contextually enable the Update Profile button.

To implement these capabilities, we need to be able to query the data source to determine the state of an entity. Let’s start again by replacing the code under the comment starting with “Temporary: enable Save and Cancel buttons” with the following code. You can also remove the “Temporary …” comment, since this is the final code.

  1. // Set up UI styling updates in response to changes in the object
  2. $(profile).bind("propertyChange", function () {
  3.     // When a property changes on the profile object enable the Cancel button.
  4.     // If all fields have passed validation, ALSO enable the Save button.
  5.     updateSaveCancelButtonState();
  6.  
  7.     // If a property changes, we highlight the changed field in the UI
  8.     updatePropertyAdornments();
  9. });
  10.  
  11. // When profile leaves its "Unmodified" state, we'll enable the Save/Cancel buttons.
  12. // When we complete a save, we'll want to remove our updated scalar property adornments.
  13. profileDataSource.option("entityStateChange", function () {
  14.     updatePropertyAdornments();
  15.     updateSaveCancelButtonState();
  16. });
  17.  
  18. // Bind the revert button on each form <input>.
  19. $("tr.profile-property.updated span.revertDelete").live("click", function () {
  20.     var propertyName = $(this).siblings("input").attr("name");
  21.     profileDataSource.revertChanges(profile, propertyName);
  22. });
  23.  
  24. // Bind our Save/Cancel buttons.
  25. $("#submit").click(function () {
  26.     if ($("#profileForm").valid()) {
  27.         profileDataSource.commitChanges();
  28.     }
  29. });
  30. $("#cancel").click(function () { profileDataSource.revertChanges(true); });

To start off, the Update Profile and Cancel buttons have been disabled in HTML. By binding to the propertyChange event of the profile entity (line 2), we know when the user types something into the form and tabs out of the field. We also bind to the entityStateChange event of the data source itself (line 13), so we can know when the user has clicked submit and the data has successfully been saved to the server. We also add a handler that lets us revert a field change when the user clicks on a special span.revertDelete element.

Now let’s see how we actually know inside the updateSaveCancelButtonState() method whether an entity has changed. We can focus our attention on the hasChanges() method on line 10. We can see that we go through every entity the data source is tracking, and then on line 12, we call the getEntityState() method on that entity, and decide based on the resulting value whether the entity has changed. Based on that, we enable/disable the Update Profile and Cancel buttons accordingly. You do not need to paste this method into the script file, it is already included in the helpers section.

  1. function updateSaveCancelButtonState() {
  2.     // Temporary: will update as we add more data sources
  3.     var haveChanges = hasChanges(profileDataSource);
  4.     var changesValid = $("#profileForm").valid();
  5.     $("#submit").toggleEnabled(haveChanges && changesValid);
  6.  
  7.     // Can cancel changes regardless if they are valid or not
  8.     $("#cancel").toggleEnabled(haveChanges);
  9.  
  10.     function hasChanges(dataSource) {
  11.         return $.grep(dataSource.getEntities(), function (entity) {
  12.             switch (dataSource.getEntityState(entity)) {
  13.                 case "ClientUpdated":
  14.                 case "ClientAdded":
  15.                 case "ClientDeleted":
  16.                     return true;
  17.  
  18.                 case "Unmodified"// No changes to commit.
  19.                 case "ServerUpdating"// Commit is in progress, so disable Save/Cancel button.
  20.                 case "ServerAdding":
  21.                 case "ServerDeleting":
  22.                     return false;
  23.             }
  24.         }).length > 0;
  25.     };
  26. };

The other interesting method that we call is the updatePropertyAdornments() method. Again, this is already included and you do not need to paste this.

  1. function updatePropertyAdornments() {
  2.     $("tr.profile-property")
  3.         .removeClass("updated")
  4.         .filter(function () {
  5.             return isModifiedProfileProperty($(this).find("input").attr("name"));
  6.         })
  7.         .addClass("updated");
  8.  
  9.     function isModifiedProfileProperty(propertyName) {
  10.         var profileEntityState = profileDataSource.getEntityState(profile)
  11.         switch (profileEntityState) {
  12.             case "ClientUpdated"// Profile entity is only updated on the client.
  13.             case "ServerUpdating" // Profile entity is updated on the client and sync'ing with server (but unconfirmed).
  14.                 return profileDataSource.isPropertyChanged(profile, propertyName);
  15.  
  16.             default:
  17.                 return false;
  18.         }
  19.     };
  20. };

You see a similar approach where we apply an updated class whenever a field has changed. The addition of the updated class will also activate the revert button on an updated input field. We declared the revert behavior in a click handler for span.revertDelete in a prior code snippet.

Try running the page. When you make a change and tab out of the control, the field should highlight in orange, and a revert button should appear. Also the Update Profile and Cancel buttons should light up only when the object is changed.

Step 8: Enable editing for associated entities

You may have noticed that so far we only allowed for edits to the scalar properties of the profile entity. However, that entity also holds associations to another entity set: the friends of user. Each profile contains a set of friend records of the shape {Id: 1, ProfileId: 1, FriendId: 2} which in this example means that the user with profile ID 1 is friends with the user with profile ID 2. By adding and removing to this set of associated entities, we are effectively managing the friend relationships between users. Let’s first see how to access this set. Paste the following at the bottom of the render() method before the helper functions.

  1. // Now let's implement the friends list which uses associated entities
  2. var friends = profile.get_Friends();
  3.  
  4. // First, implement loading the friends list and populating a
  5. // control with the result
  6. var friendsNames = {};
  7. var friendsList = $("#friend-list").list({
  8.     data: friends,
  9.     template: $("#profile-friend-template"),
  10.     templateOptions: {
  11.         getFriendName: function (friend) {
  12.             var profile = friend.get_FriendProfile();
  13.             if (profile) {
  14.                 return profile.Name;
  15.             } else {
  16.                 return friendsNames[friend.FriendId];
  17.             }
  18.         }
  19.     }
  20. }).data("list");

Notice that our profile entity contains a method get_Friends(), which returns the array of associated entities (line 2). This method was generated automatically for us. We then proceed to bind the list of friends to a jQuery List widget, just like we did in the first part of this walkthrough. One interesting thing here is that if you look at the template we are using (open the MyProfile.aspx page and look for profile-friend-template), you’ll notice we don’t bind to a property on the friend directly, because remember, the friend entity itself only contains the ID of the profile of the friend. To get the name from the profile, we define a converter method called getFriendName, which looks up the profile name based on the friend entity. The key here is line 12, where we see that the friend entity has a generated method get_FriendProfile(), which lets us retrieve the profile object for that friend. There is some extra complexity in that method due to the fact that the profile entity for the friend may not have been loaded yet, but you can ignore that for now.

You can now run the sample, and you should see the list of friends get populated in the big box in the middle of the page.

As a next step, let’s consider how to enable us to add a new friend to the current user’s profile. Add the following (rather large) chunk of code to the bottom of the render() method before the helper functions.

  1. // Now create an auto-complete over the add-friend-text <input> field
  2. // to make it easier to load friends into it
  3. $("#add-friend-text").autocomplete({
  4.     source: function (term, callback) {
  5.         var filter = [
  6.             { property: "Name", operator: "Contains", value: term.term },
  7.             // Filter out the profile of the current user
  8.             { property: "Id", operator: "!=", value: profile.Id }
  9.         ].concat($.map(friends, function (friend) {
  10.             // Filter out existing friends
  11.             return { property: "Id", operator: "!=", value: friend.FriendId };
  12.         }));
  13.  
  14.         $.dataSource({
  15.             serviceUrl: serviceUrl,
  16.             queryName: "GetProfiles",
  17.             filter: filter,
  18.             refresh: function (friendProfiles) {
  19.                 // Transform each result into a label/foreignKey pair
  20.                 callback($.map(friendProfiles, function (friend) {
  21.                     return { label: friend.Name, foreignKey: friend.Id };
  22.                 }));
  23.             }
  24.         }).refresh();
  25.     },
  26.     select: function (event, data) {
  27.         // Stow the foreign key value where our "Add Friend" button's click handler can find it.
  28.         $("#add-friend").data("friendId", data.item.foreignKey);
  29.  
  30.         // Enable the "Add Friend" button since we have a selected friend to add
  31.         $("#add-friend-button").enable().focus();
  32.     }
  33. }).keyup(function (event) {
  34.     // Any keystroke that doesn't select a Friend should disable the "Add Friend" button
  35.     if (event.keyCode !== 13) {
  36.         $("#add-friend-button").disable();
  37.     }
  38. }).watermark();

The goal here is to create an auto-complete text box that will allow the user to search all the other registered users in the system and find potential friends. On line 3 you will see that we create a jQuery autocomplete control over the add-friend-text <input> field. The autocomplete needs a source of data that we supply to source parameter. Ultimately, we will use a remote data source, which we declare on line 14. We build up a rather complex filter expression on lines 5-12, which includes only names containing the characters typed in by the user, and excludes the user him/herself, and any of their existing friends. We then shape the result into the format {label: "Brad Olenick", foreignKey: 1}, as you see on lines 20-21. The label will be used by the autocomplete control to display the suggestion as the user types. Also notice that the foreignKey field is stored as DOM data behind the add-friend td (line 28), so it can be retrieved later on. Here is the relevant HTML piece from MyProfile.aspx that is useful for context (should already be in the file, no need to paste this):

  1. <td id="add-friend">
  2.     <input id="add-friend-text" type="text" class="profile-property-input" title="add friend..." />
  3.     <button type="button" class="profile-button" id="add-friend-button" disabled="disabled">Add Friend</button>
  4. </td>

Now that the user has selected the profile of the person they want to friend, we have their name inside the add-friend-text <input> and their foreignKey hidden as DOM data inside add-friend. We can now add the following chunk of code to the bottom of the render() method before the helper methods.

  1. // Now implement adding a friend once the friend has been populated in
  2. // the add-friend-button <input> field
  3. $("#add-friend-button").click(function () {
  4.     var friendId = $("#add-friend").data("friendId");
  5.     friendsNames[friendId] = $("#add-friend-text").val();
  6.     $.observable(friends).insert(friends.length, { FriendId: friendId });
  7.  
  8.     $("#add-friend-text").val("");
  9.     $(this).disable();
  10. });

This is an interesting function to study. First, note that we’re attaching a click handler to the Add Friend button. When the user clicks that button, we retrieve the friendId that we previously hid as DOM data (line 4). The key here is line 6, where we actually create a new friend association, but adding a new entry into the friends entity set (line 6).

Now we are ready to display and add new friends. Run the sample (a refresh may be necessary) and type in characters… typing “ho” should bring up the name “Howard Dierking”. Typing “ya” should bring up “Yavor Georgiev”.

The last piece of the sample is similar to Step 7, as we plan to add add some contextual behaviors based on the state of the friends association array.

  • If a friend association has been added/removed, we need to enable the Update Profile and Cancel buttons
  • Add a behavior that will let us delete an existing friend association
  • Add a behavior that will let us revert a deleted or added friend association

To accomplish this, paste the following at the bottom of the render() method before the helper methods.

  1. // Update the Submit / Cancel button state when a friend is reverted
  2. $([friends]).bind("arrayChange", function () {
  3.     updateSaveCancelButtonState();
  4. });
  5.  
  6. // For each Friend child entity, transitioning from the "Unmodified" entity state
  7. // indicates an add/remove.  Such a change should update our per-entity added/removed styling. 
  8. // It should also enable/disable our Save/Cancel buttons.
  9. $([friends]).dataSource().option("entityStateChange", function (entity, state) {
  10.     updateFriendAddDeleteAdornment(entity);
  11.     updateSaveCancelButtonState();
  12. });
  13.  
  14. // Bind revert/delete button on friends.
  15. // It's convenient to use "live" here to bind/unbind these handlers as child entities are added/removed.
  16. $("#friend-list span.revertDelete").live("click", function () {
  17.     var friendElement = $(this).closest("li");
  18.     var friend = friendsList.dataForNode(friendElement[0]);
  19.     if (friendElement.hasClass("deleted") || friendElement.hasClass("added")) {
  20.         $([friends]).dataSource().revertChanges(friend);
  21.     } else {
  22.         friends.deleteEntity(friend);
  23.     }
  24. });

Notice that on line 2 we bind to the arrayChange event since we are now looking at an array of entities instead of field changes on an individual entity (we used propertyChange in Step 8). We also bind to the entityStateChange event, just like we did in Step 7. Another interesting piece is line 16, where we attach a click handler to span.revertDelete elements, which represent a little button on the right-hand side of every element in the jQuery List control. Depending on the state of the item, that button will either allow us to delete a friend association, or revert a deleted or added friend association. Check out lines 20 and 22 for the calls that respectively revert and delete an entity.

We already examined the updateSaveCancelButtonState() function, but we need to make a small change so it recognizes the new data source over the friends collection. Replace the line starting with “Temporary…” and the line immediately after it with the following:

  1. var haveChanges = hasChanges(profileDataSource) || hasChanges($([friends]).dataSource());

You also see we use the  updateFriendAddDeleteAdornment() function, which you can explore yourself, as it is similar to what we saw in Step 7.

Run the sample one last time. Clicking on the X button on right of a friend name will delete the entity, and change the button look and behavior to revert that delete if clicked again. For a newly added entity, you can click the revert button on the right and the add will be reverted.

You are done! Now you have a fully functioning sample.

Conclusion

In this walkthrough you learned how to build a data-driven application using jQuery and WCF DomainServices, supporting both per-click commit of changes, and a buffered change-tracking mode. A completed solution is also included with this walkthrough (see the Prerequisites section for a link), in case you got lost with some of the steps, or just want to see the finished app.

A more advanced version of the completed sample can be found here.

Last edited Feb 18, 2012 at 2:06 AM by yavorg, version 35

Comments

abonfanti Nov 10, 2011 at 9:34 AM 
I get this error after login, "Microsoft JScript runtime error: 'Metadata' is null or not an object" in jquery-1.6.3.js[dynamic]....What's up? I've miss some configuration?

sparmar112 Sep 26, 2011 at 11:15 PM 
This is the error i am facing ! Microsoft JScript runtime error: 'length' is null or not an object

sparmar112 Sep 26, 2011 at 11:11 PM 
The starter sample project is not working. Getting run time errors in JavaScript files. As i m not much stronger in JavaScripting so it is really giving my hard time!!!

CodeFuzion Sep 21, 2011 at 3:13 PM 
It would be nice if the Entity State changes it can automatically adorn the bound element / input with a conventional Class attribute. That would cut out most of the helper methods needed, and you could focus on just the css styling. If you utilize the new Render/View Data Linking and the Data List control with RiaJS, this could be built in. If not then it would not affect it.

Also, what this really needs is documentation to state and show all of the properties the datasource has, and all of the states it can be in.

Question: Why do you have to use $.observable(entity).setProperty("fieldName", fieldValue) on the Rating and not on the Profile? Is the data link handling the observable setProperty in it? I can understand using $.observable(entities).insert(arrayPosition, item) to notify the datasource that something added to the entityCollection, but the individual field should be necessary. Maybe you can do similar to knockout and use entity.fieldName(fieldValue) to auto use observable in that case.

carlosfigueira Aug 5, 2011 at 10:10 PM 
Thank you for reporting this issue. There was some mismatch between the version on the ZIP files and the actual ones, I've uploaded a corrected start / completed files now. The issue should be fixed.

r3ap3r Aug 5, 2011 at 4:56 PM 
Same JS files also appear to be missing from the complete version, however it does run. When I try to login however I get the error "Failed to generate a user instance of SQL Server due to a failure in starting the process for the user instance. The connection will be closed. "

r3ap3r Aug 5, 2011 at 4:52 PM 
The starter sample project is not working. When I extract the zip it is missing many js files: http://imgur.com/7nGxR
When I run the application I get this error: http://imgur.com/59ohZ which is strange because I was following the instructions on http://jeffhandley.com/archive/2011/04/13/RIAJS-jQuery-client-for-WCF-RIA-Services.aspx initially and I ended up getting the exact error, which is why I came to this website instead to try the sample. What is going on? It is because I am running the May release of ria toolkit? I figured one later than the april one would be acceptable? Is this project kicking?