Quantcast
Channel: Georgi Stavrev » Software | Georgi Stavrev
Viewing all articles
Browse latest Browse all 5

Build a Single Page Application with the HotTowel SPA Template – Part Two

$
0
0

In the previous article I’ve introduced the HotTowel SPA Template and described how to create a simple single page application that displays data coming from an ASP .Net backend. We’ve seen some data binding with Knockout JS, we’ve queried data with Breeze and we have made it all work together thanks to Durandal JS. In this second article we’re going to dig a bit more into what Breeze offers us as a framework and we’re going to create the Create, Update and Delete (CUD) operations. We’re going to see how we can extend the EFContextProvider on the backend and I’m going to present a few common services Durandal has to offer out of the box. I’m going also to discuss how we can secure the CUD operations.

Composition

Composition in Durandal means object composition and visual composition. Object composition is provided by RequireJS and visual composition is provided by the composition module. To find more about composition in Durandal go to http://durandaljs.com/documentation/Using-Composition. We have used declarative object composition in the previous article when we created the restaurantBriefs viewmodel.

define(['services/logger'], function (logger) {
	//...
});

The code above declares a module which depends on the ‘services/logger’ module.

Message boxes

Durandal provides us with the app module which offers us functionality to display messages and modal dialogs on the screen. To show a message or a modal dialog simply uses the showMessage function. We can pass the possible return values of the modal dialog in the options parameter. The result of the modal dialog is a promise. An example of a simple modal dialog can be seen below.

app.showMessage(
		"Are you sure you want to delete this restaurant?" //text
		, "Delete restaurant" //title
		, ['Yes', 'No'] //options
	).then(doSomething);

Durandal modules

To define a module in Durandal we simply call the define function and pass the module dependencies. For example if a module requires the app module we use the code below.

define('durandal/app'], function (app) {
	var myModule =
	{
		sayHello: sayHello,
		activate: activate
	};

	function sayHello()
	{
		app.showMessage("Hello world!", "Hello", ['OK']);
	};

	function activate()
	{
		sayHello();
	};

	return myModule;
});

The module above displays a message box and can be added to our spa application as it has an activate method. (We have to create the corresponding view too obviously)

Adding Update functionnality

Previously we defined a restaurants model.

To update an entity we change its property values and we use the breeze EntityManager saveChanges function. Since we can ask the EntityManager if there are any changes, if there are none, no call will be made to the data service. In the example below you can see that the saveChanges function returns a promise. We also use the router module to be able to navigate back from the edit view to the restaurants view.

define(['services/logger', 'durandal/plugins/router'], function (logger, router) {
 	var vm = {
		editableRestaurant: ko.observable(),
		activate: activate,
		title: "Edit Restaurant",
		saveChanges: saveChanges,
		goBack: goBack
	};
	//...
	function saveChanges() {
		if (manager.hasChanges()) {
			manager.saveChanges()
				.then(saveSucceeded)
				.fail(saveFailed);
		} else {
			toastr.info("Nothing to save");
		};

		function saveSucceeded(data) {
			toastr.success("Saved");
		}

        	function saveFailed(error) {
			toastr.error("Save failed");
		}
	};

 	function goBack() {
		router.navigateBack();
	}
	//...
});

Adding Create functionnality

Sometimes we need to create new entities. But how do we instantiate a new entity on the client side? Well, the EntityManager instantiates it for us when we call the createEntity function with the type name of the entity we want to create and JSON data for its property values. In the example below, I’ve passed ‘Restaurant:#spa.Models’ as a type name. The part after the column character is the namespace of the entity class. The EntityManager will still be unable to find this type if we don’t fetch the model metadata in before calling createEntity. We do this by calling metadataStore .fetchMetadata function the activate method.

	//...
	function activate(context) {
        	if (!manager.metadataStore.hasMetadataFor(serviceName)) {
            		manager.metadataStore.fetchMetadata(serviceName, fetchMetadataSuccess, fetchMetadataSuccess)
        	}

        	function fetchMetadataSuccess(rawMetadata) {
        	}

        	function fetchMetadataFail(exception) {
        	}
    	}

    	function createEntity() {
        	var newRestaurant = manager.
            		createEntity('Restaurant:#spa.Models',
            			{ Name: vm.name(), Description: vm.description(), Address: vm.address() });
        	manager.addEntity(newRestaurant);
        	manager.saveChanges()
            		.then(createSucceeded)
            		.fail(createFailed);

        	function createSucceeded(data) {
            		toastr.success("Create");
            		router.navigateTo('#/restaurantBriefs');
        	}

        	function createFailed(error) {
            		toastr.error("Create failed");
        	}
    	};
	//...

Since we don’t have an Ajax image uploader yet and we would like to display a cover photo for the restaurants we create, we will use a default image. We’re going to set the CoverPhoto property on the server side by extending the EFContextProvider. We’re going to intercept the save changes operation and for each restaurant without a cover photo we’re going to set the value to restaurant.gif.

public class RestaurantContextProvider : EFContextProvider
{
	protected override bool BeforeSaveEntity(EntityInfo entityInfo)
	{
		Restaurant restaurant = (Restaurant)entityInfo.Entity;
		if (string.IsNullOrEmpty(restaurant.CoverPhoto))
		{
			restaurant.CoverPhoto = "restaurant.gif";
		}
		return true;
	}
}

We could intercept the save operation to assure that only the owner/moderator of the restaurant can update its content, or for doing a number of other things before saving the entities.

Adding Delete functionnality

To delete an entity we set its entity state to deleted by calling setDeleted function on the entityAspect property. If calling the saveChanges results in a failure, we should not forget to call rejectChanges on the EntityManager to revert back to the previous entity state. In this example we’re asking the user to confirm the deletion. We can do that very easily by call the showMessage function of the app module.

	//...
	function deleteRestaurant()
	{
		app.showMessage("Are you sure you want to delete this restaurant?", "Delete restaurant", ['Yes', 'No'])
			.then(goDelete);
	}

	function goDelete(data) {
		if (data == 'No') {
			toastr.info('Aborted');
		}
		else {
			vm.restaurant().entityAspect.setDeleted();

			manager.saveChanges()
				.then(deleteSucceeded)
				.fail(deleteFailed);
		}

		function deleteSucceeded(data) {
			toastr.success("Deleted");
			router.navigateTo('#/restaurantBriefs');
		}

		function deleteFailed(error) {
			toastr.error("Delete failed");
			manager.rejectChanges();
			app.showMessage("The restaurant could not be deleted.", "Delete failed");
		}
	}
	//...

Adding filtering

Having the list of all the restaurants is good but we risk having an important number so a filtering functionality could be very useful. We simply add an observable property to the viewmodel and bind it to a textbox on the view, and whenever the user types a word inside the textbox we query the data source with the proper where clause. Breeze gives us the ability to create predicates and combine them.

var Predicate = breeze.Predicate;
var p1 = Predicate.create("Name", "substringof", vm.filterText());
var p2 = Predicate.create("Name", "contains", vm.filterText());
var whereClause = p1.or(p2);

There are quite a few operators we can use with predicates. You can check them on Breeze’s website. The example above creates a predicate to verify if the Name property is a substring or contains the text entered in the filter textbox.

Using the local cache

Querying the data source all the time means generating network traffic. It kind of defeats the purpose of having Ajax calls. Our goal is not only to have a fluid user interface but also to limit the usage of the network. That’s why we should use the local cache whenever possible. We can do that by calling executeQueryLocally on the EntityManager. In our example we can query locally only the entities we have declared in our EntityFramework DbContext. This means that our RestaurantBrief entities are not available in the local cache. We will try to find a solution for this in the next article. An example of executing a query locally is show below.

var query = breeze.EntityQuery.
	from("Restaurants");

manager.executeQuery(query)
	.then(function(data) {
    		var localData = manager.executeQueryLocally(query);
    		vm.restaurants(localData);
	})
	.fail(function(e) {
   		alert(e);
	});

Next time

Durandal

In the next article we’re going to see what other useful feature does Durandal offers us and how can we leverage them to improve our user’s experience.

Breeze

We will explore more of Breeze’s features too and try to optimize our code a bit.

SignalR

Having a not reloading page isn’t sometimes enough to achieve the user experience we want. We should take care of issues like concurrent updates and events. We can do that with the aid of SignalR – a javascript library that provides real-time web functionnality.

Source Code

You can find a working example of the techniques described in this article in the download below.

Other articles in this series

The post Build a Single Page Application with the HotTowel SPA Template – Part Two appeared first on Georgi Stavrev.


Viewing all articles
Browse latest Browse all 5

Trending Articles