Introduction
This article continues the JSON WCF Service (part 1) article where we described how to create a WCF service that receives and passes data in the JSON format.
And now we are going to implement a JavaScript-based client side that will interact with our WCF service. However, we won’t write a single line of pure JavaScript code in order to do this. Instead, we will use Script#. It allows writing code in C#, programming language we got used to and then convert it to JavaScript.
You can get some basic knowledge about Script# from the Script# insights article or on the official website of the product [http://projects.nikhilk.net/ScriptSharp]. There you can download the latest version of Script#.
So, let’s get started!
Creating Client Data Model Classes
We start from the message classes: ContinentPopulation
, ContinentDetails
, ContinentDetailRequest
, ContinentDetailsResponse
, ContinentsListResponse
.
As you remember, members of every class above are automatically implemented properties. However, if you try to assemble a Script# project with such properties, you will get the following error: “Feature ‘automatically implemented properties’ cannot be used because it is not part of the ISO-2 C# language specification”. So we have to modify our classes so that they meet the language specifications.
It can be done in several ways. I will show you all of them, so you can choose the one that is the most convenient for you.
The first one and, probably, the easiest way is to create the data model specially for Script# projects. For example, the ContinentPopulation
message class for a common C# projects looks as follows:
[DataContract(Name = "ContinentPopulation")]
public class ContinentPopulation
{
[DataMember(Name = "ContinentName",IsRequired = true,Order = 0)]
public string ContinentName { get; set; }
[DataMember(Name = "TotalPopulation", IsRequired = true, Order = 1)]
public int TotalPopulation { get; set; }
}
In the Script# language, it will be like follows:
public class ContinentPopulation
{
public string ContinentName;
public int TotalPopulation;
}
This is not the best way, and I wouldn’t recommend using it. Yes, it can be good if you create a sample application that contains just few classes of the data model. But real projects include huge amount of data model classes and may reach up to hundred classes. Just imagine that you will need to continuously synchronize changes in these classes.
In order to avoid duplication of data model classes, I suggest using preprocessor directives. First of all, we need to add a conditional compilation symbol to a data model service project. Let’s call it the SERVICEMODEL
. We will use it in the #IF
directive to define server side code which will not be used in Script# classes.
Creating a Data Model Project
Now we add a project to our solution that will contain the data model for Script#. Let’s name it as the ClientDataModel
.
When adding the project, you will be suggested to specify a folder to copy the generated js files: let’s choose the Scripts folder of the web application.
Now it is necessary to add references to the data model service classes in the created projects.
Let’s see how the class code should look like. I won’t show code of each class, you will be able to view it in the sample attached to this article. Let’s see just the code of the ContinentPopulation
class. Changes for the rest of data model classes will be the same.
1. using System;
2. using System.Collections.Generic;
3. using System.Linq;
4. using System.Text;
5. using System.Runtime.Serialization;
6.
7. namespace DataModel
8. {
9. [DataContract(Name = "ContinentPopulation")]
10. public class ContinentPopulation
11. {
12. [DataMember(Name = "ContinentName", IsRequired = true, Order = 0)]
13. public string ContinentName
14. {
15. get;
16. set;
17. }
18.
19. [DataMember(Name = "TotalPopulation", IsRequired = true, Order = 1)]
20. public long TotalPopulation
21. {
22. get;
23. set;
24. }
25. }
26. }
And here is a fragment that contains mixed client and the server side code:
#if SERVICEMODEL
#else
#endif
Now let’s review the listing above. Our class doesn’t use the System.Text
namespace, that is why line 4 may be deleted. The System.Runtime.Serialization
namespace doesn’t exist in Script#, but it is used in C# (contains the DataContract
and DataMember
classes), that is why line 5 should be replaced by the following code:
#if SERVICEMODEL
using System.Runtime.Serialization;
#endif
We don’t need the DataContract
and DataMember
attributes in Script#, that is why we will place them in the directive “#if SERVICEMODEL
” (lines 9, 12, 19).
We’ll do the same with the properties. You can see the code of the changed class below:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
#if SERVICEMODEL
using System.Runtime.Serialization;
#endif
namespace DataModel
{
#if SERVICEMODEL
[DataContract(Name = "ContinentPopulation")]
#endif
public class ContinentPopulation
{
#if SERVICEMODEL
[DataMember(Name = "ContinentName", IsRequired = true, Order = 0)]
#else
[PreserveCase()]
#endif
public string ContinentName
#if SERVICEMODEL
{
get;
set;
}
#else
;
#endif
#if SERVICEMODEL
[DataMember(Name = "TotalPopulation", IsRequired = true, Order = 1)]
#else
[PreserveCase()]
#endif
public long TotalPopulation
#if SERVICEMODEL
{
get;
set;
}
#else
;
#endif
}
}
All data model classes should be transformed this way. The PreserveCase
attribute is used in order to avoid conversion of the class member name when generating JavaScript code. Finally, we will change namespace in classes of our data model from the ServiceDataModel
to the DataModel
. And now let’s proceed with the implementation of the client side of our application.
Creating a Client Script# Application
We add the “jQuery Script Library” project under the “ClientLibrary
” name to our solution.
To avoid copying the js files to web application during assembling of the Script# project, you can set the Scripts folder in the way we did it when adding the Script# project to the data model. Let’s forget that JavaScript has no idea about interface or class, let’s forget about JavaScript at all. Let’s write code in C# in the way we used to (based on the specification ISO-2 of C#).
Service Interaction Layer
I can schematically describe the interaction of a WCF service and a client application the following way.
WCF has two public
methods: the GetContinents
and the GetContinentDetails
which are defined by the IContinentsPopulation
interface.
In a client application, it is possible to define a service layer that will do all the job of getting data from the service – interact service methods: the GetContinents
and the GetContinentDetails
. We define the IService
interface with the following methods: GetContinents
and GetContinentDetails
, and their names are analogous to the names of the service methods.
Interaction with the service will be asynchronous, that is why interface methods will transmit callback methods. They will be executed after successful or failed execution of the query. Code for the interface is shown below.
public interface IService
{
void GetContinents(AjaxRequestCallback successCallback, AjaxErrorCallback errorCallback);
void GetContinentDetails(ContinentDetailRequest request,
AjaxRequestCallback successCallback, AjaxErrorCallback errorCallback);
}
Let’s create a class implementing the IService
interface, name it as ServiceProvider
. The ServiceProvider
class will form AJAX queries to the server, set handlers for successful or failed completion of the query. Implementation of the class is quite simple that is why we won’t review it here. Let’s better review the GetRequestData
and the PrepareDataForRequest
methods.
GetRequestData
method executes conversion of the object to a string
in JSON format by using the standard class provided by Script#.
GetContinentDetails
method on WCF service has argument with the “request
” name. It slightly limits query data, it should be provided in the following way:
{ "request" : { "ContinentName" : "" } }
In this case, an object of the ContinentDetailRequest
type with the request name will be created when serializing data on WCF service. That is why we create a dictionary in the Script# project and place all objects of the ContinentDetailRequest
type with the “request
” key. The code of this method will look as follows after converting it to JavaScript:
...
var dataRequest = {};
dataRequest['request'] = obj;
return dataRequest;
...
It can be done other way: by creating a class that will contain a field with the request name and object type. For example, the RequestData
class as shown below:
public class RequestData
{
public object request;
}
Then it will be possible to create an object of the RequestData
type in the PrepareDataForRequest
method, set a field value to request and pass it for serialization:
private object PrepareDataForRequest(object obj)
{
RequestData t = new RequestData();
t.request = obj;
return t;
}
Add the SetData
method to the IService
interface. This method will process data which are passed from server deleting unnecessary information. And correspondingly the implementation of this method in the ServiceProvider
class will look as follows:
public object GetData(object serverObject)
{
return JsonDataConverter.GetData(serverObject);
}
A client side of an application will contain three layers – one layer has been reviewed already – this is the layer of interaction with server. Next layer is a layer of data processing. It prepares data required for execution of methods on the server side and handles response from the server. The third layer is responsible for displaying data on the page.
Now let’s add a class that will manage the process of getting data from the server (data processing layer) to the ClientLibrary
project and give it the ContinentsManager
name.
We won’t consider code of this class as it quite simple for understanding. It is just worth mentioning that this class should somehow notify the data display layer that the data has been loaded. JavaScript doesn’t have an event, but Script# allows us to describe events in the way we do it in C# hiding from us the whole implementation of the mechanism. That is why we can certainly describe the following events in the ContinentsManager
class:
public event ContinentDetailsLoadedCallback ContinentDetailsLoaded;
public event ContinentsListLoadedCallback ContinentsListLoaded;
Every event type should have a delegate. Corresponding delegates are provided below:
public delegate void ContinentsListLoadedCallback(List continents);
public delegate void ContinentDetailsLoadedCallback(ContinentDetails continentDetails);
And now let’s proceed to the data processing and displaying layer.
Data Processing and Display Layer
Let me show you how a page with data should look like:
The table structure is as follows:
<table id="tbt_t">
<thead>
<tr></tr>
<th>
Name</th>
<th>
Population</th>
</tr>
</thead>
<tbody></tbody>
<tr></tr>
<td></td>
[name]</td>
<td></td>
[population]</td>
</tr>
</tbody>
</table>
Now we add the ViewManager
class to the ClientLibrary
project. This class is responsible for data displaying. During the previous step, we created the ContinentsManager
class that allows getting data from the server. It has two asynchronous methods and the corresponding events. Let’s create a private
field of the ContinentsManager
type with the _continentsManager
name. Initialize this field in the ViewMethod
constructor.
public ViewManager()
{
_continentsManager = new ContinentsManager();
_continentsManager.ContinentDetailsLoaded +=
new ContinentDetailsLoadedCallback(_continentsManager_ContinentDetailsLoaded);
_continentsManager.ContinentsListLoaded +=
new ContinentsListLoadedCallback(_continentsManager_ContinentsListLoaded);
}
Add the SetElement
method to the ViewManager
class. This class sets up and configures element for data display. Let’s review work of the SetElement
method. It calls the InitializeViewElement
method (you can see its code below):
1. jQueryObject viewObject = jQuery.Select(selector);
2. if (viewObject.Length > 0)
3. {
4. _viewElement = (TableElement)viewObject.GetElement(0);
5. jQuery.FromElement(_viewElement).CSS("border", "1px solid black");
6. InitializeHeader();
7. }
In the first code line, we get the page
element. This line looks like this in JavaScript:
var viewObject = $(selector);
The second line checks if the elements are found. The fourth line gets the first element – the JavaScript equivalent:
this._viewElement = viewObject.get(0);
The fifth line sets the CSS property “border
” for our element using JQuery – the JavaScript equivalent:
$(this._viewElement).css('border', '1px solid black');
The code of the InitializeHeader
method is set in the table below where each line of code in Script# corresponds to a line of the code in JavaScript. This method creates a table caption with 2 columns: “Name
” and “Population
”.
Script#
Element headElement = Document.CreateElement("thead");
Element trElement = Document.CreateElement("tr");
Element thNameElement = Document.CreateElement("th");
jQuery.FromElement(thNameElement).Text("Name");
Element thPopulationElement = Document.CreateElement("th");
jQuery.FromElement(thPopulationElement).Text("Population");
trElement.AppendChild(thNameElement);
trElement.AppendChild(thPopulationElement);
headElement.AppendChild(trElement);
_viewElement.AppendChild(headElement);
JavaScript
var headElement = document.createElement('thead');
var trElement = document.createElement('tr');
var thNameElement = document.createElement('th');
$(thNameElement).text('Name');
var thPopulationElement = document.createElement('th');
$(thPopulationElement).text('Population');
trElement.appendChild(thNameElement);
trElement.appendChild(thPopulationElement);
headElement.appendChild(trElement);
this._viewElement.appendChild(headElement);
The _continentsManager_ContinentsListLoaded
method is called when a list containing continents and popularity is loaded from the server. The _continentsManager_ContinentsListLoaded
method searches the list of clients and adds data to the table. When the table row is formed, a cell containing data about continent name is bound to the click event. This event handler gets cell content – continent name in our case – and queries detailed data for this continent.
tdNameElement.AddEventListener(
"click",
delegate(ElementEvent e)
{
_continentsManager.GetContinentDetails(e.Target.InnerText);
},
false);
The _continentsManager_ContinentDetailsLoaded
method is called when the detailed data on continent is loaded from the server. This method forms strings with continent data and displays them in a modal window.
StringBuilder builder = new StringBuilder();
builder.AppendLine(string.Format("ContinentName: {0}", continentDetails.ContinentName));
builder.AppendLine(string.Format("Area: {0}", continentDetails.Area));
builder.AppendLine(string.Format("PercentOfTotalLandmass: {0}",
continentDetails.PercentOfTotalLandmass));
builder.AppendLine(string.Format("TotalPopulation: {0}",
continentDetails.TotalPopulation));
builder.AppendLine(string.Format("PercentOfTotalPopulation: {0}",
continentDetails.PercentOfTotalPopulation));
Script.Alert(builder.ToString());
The ViewManager
class is ready, and correspondingly the data display layer is ready as well.
Let’s see how to use all we created in Script# by adding all necessary scripts to the page:
<script src="Scripts/jquery-1.6.2.js" type="text/javascript"></script>
<script src="Scripts/mscorlib.debug.js" type="text/javascript"></script>
<script src="Scripts/ClientDataModel.debug.js" type="text/javascript"></script>
<script src="Scripts/ClientLibrary.debug.js" type="text/javascript"></script>
And add the following JavaScript code:
var viewManager = new ClientLibrary.ViewManager();
$(document).ready(function () {
viewManager.setElement("#dataTable");
viewManager.loadData();
});
This code creates an object of the ClientLibrary.ViewManager
class. Set an element in the page loaded event handler, this element will be used to display data and call data upload method.
The application looks in the browser as shown in the picture below. Table displays a list of countries with population. If you click the country name, detailed data on this country will be loaded from the server and displayed in the dialog window of the browser.