Nov
5
Written by:
Michael Washington
11/5/2014 8:02 PM
You can create WinJS applications easily using Visual Studio LightSwitch OData as your back-end. This sample demonstrates using WinJS to call LightSwitch directly using the XHR object in WinJS.
The sample application uses the WinJS Pivot Control to allow the user to easily navigate the application using touch with no post-backs.
Users can insert new records.
Users can select and edit existing records.
Users can delete records by first selecting them (right-clicking on them or press, hold, and slide each list element down if using touch), and then clicking Delete Selected.
Full validation and concurrency is supported.
Why would you want to use WinJS with LightSwitch?
- You want to use WinJS controls – WinJS has a a number of controls such as Pivot that create a unique experience that works well on touch devices.
- You need to implement security – Security must be implemented on the server-side. You cannot implement security inside WinJS because it is a client-side technology. LightSwitch makes security easy.
- You need Concurrency – When a user attempts to update a record that has been updated by another user or process since the user last retrieved the record, concurrency alerts the user. When LightSwitch is used as the back-end data source you get this feature automatically.
- You want to use LightSwitch for the Administration screens – There is no need to code all the screens in your application in WinJS. You can mix WinJS and LightSwitch screens in the same application.
- You want to build a OData service layer for use with other applications – LightSwitch is the easiest and most robust way to create an OData service.
- You want to speed up development – There is a lot less code to implement when you use LightSwitch as your back-end. This means faster development and fewer bugs.
Create The Project
We will start with a normal LightSwitch project. If you have the need for user self-registration and management, you may want to start with this sample that provides that: Allow LightSwitch Users To Self-Register and Change Passwords Using MVC.
Create a New Project.
Create a LightSwitch HTML Application.
The LightSwitch Business Layer
In the Server project, right-click on the Data Sources folder and select Add Table.
Create the ToDo table.
In the Write Code menu, select the Validate method.
Use the following code for the method:
// Do not allow a task to be called {New Task]
if (entity.TaskName == "[New Task]")
{
results.AddEntityError(
"Task cannot be named [New Task]"
);
}
// Do not allow more than 1 incomplete Task
if (entity.IsComplete == false)
{
int intCountOfIncomplete =
this.DataWorkspace.ApplicationData.ToDoes
.Where(x => x.IsComplete == false)
.Where(x => x.Id != entity.Id).Count();
if (intCountOfIncomplete > 0)
{
results.AddEntityError(
"Cannot have more than 1 incomplete Task"
);
}
}
Add WinJS To The Project
Right-click on the HTML Client project and select Manage NuGet Packages (note that we could put the code in the Server project and it would still work, see: Using AngularJS Wijmo Grid With LightSwitch OData for an example of this).
Install the WinJS library.
The WinJS JavaScript code will be added to the project.
Create The Default Page
Open the default.htm page and replace the contents with the following code:
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta charset="utf-8">
<title>Adding WinJS controls and styles</title>
<!-- WinJS references -->
<script src="WinJS/js/base.js"></script>
<script src="WinJS/js/ui.js"></script>
<link href="WinJS/css/ui-dark.css" rel="stylesheet">
<!-- AddingWinJSControlsAndStyles references -->
<link href="Content/default.css" rel="stylesheet">
<script src="Scripts/default.js"></script>
</head>
<body>
</body>
</html>
This references the JavaScript and CSS files that the application needs. Some of these files are not created yet, we will create them in later steps. To see the contents of the CSS file, download the sample from the download page.
Add the following code to the body section to create the Pivot control with two Pivot items (edit and view-only):
<!-- PIVOT Control... -->
<div id="pivotControl" class="pivotControl"
data-win-control="WinJS.UI.Pivot"
data-win-options="{ title: 'ToDo', selectedIndex: 1 }">
<div class="listviewpivotitem"
data-win-control="WinJS.UI.PivotItem"
data-win-options="{ 'header': 'edit', managedLV: true }">
<!-- Nothing here yet... -->
</div>
<div class="listviewpivotitem"
data-win-control="WinJS.UI.PivotItem"
data-win-options="{ 'header': 'view-only', managedLV: true }">
<div id="todoListViewControlUnSelected"
data-win-control="WinJS.UI.ListView"
data-win-options="{selectionMode: 'none'}">
</div>
</div>
</div>
Also, add the following code to define the templates for the Pivot control and the ListView:
<!-- Start PIVOT... -->
<div class="ToDoItemTemplate"
data-win-control="WinJS.Binding.Template">
<div class="ToDoItem">
<div class="hitTarget win-interactive"></div>
<h2 class="author"
data-win-bind="innerHTML: author"></h2>
<h5 class="time"
data-win-bind="innerHTML: time"></h5>
<h5 class="title win-pivot-slide1"
data-win-bind="innerHTML: title; style.color: titleColor"></h5>
<h5 class="previewText win-pivot-slide2"
data-win-bind="innerHTML: previewText"></h5>
</div>
</div>
<!-- ListView Template... -->
<div id="ToDoListViewTemplate"
class="itemContainer"
data-win-control="WinJS.Binding.Template">
<h2 class="itemData">
<input id="ToDoToggle"
type="checkbox"
disabled="disabled"
data-win-bind="checked:IsComplete" />
<span data-win-bind="textContent: TaskName" />
</h2>
</div>
Create The JavaScript
Create a default.js file and use the following code that sets up some variables and initializes WinJS:
(function () {
"use strict";
var result;
var todoList = [];
var dataList = [];
var todoListUnSelectedView;
WinJS.UI.processAll().done(function () {
var todoListViewControlUnSelectedElement =
document.getElementById('todoListViewControlUnSelected');
todoListUnSelectedView =
new WinJS.UI.ListView(todoListViewControlUnSelectedElement, null);
var ListViewUnSelectedTemplateElement =
document.getElementById("ToDoListViewTemplate");
var ListViewUnSelectedTemplate =
new WinJS.Binding.Template(ListViewUnSelectedTemplateElement, null);
todoListUnSelectedView.itemTemplate =
ListViewUnSelectedTemplateElement;
todoListUnSelectedView.forceLayout();
});
})();
Add the following code that calls LightSwitch OData and provides data to the ListView control:
// Get the ToDo items
WinJS.xhr({ url: "/ApplicationData.svc/ToDoes" })
.then(ResponseToDoData)
.done(DisplayToDoData);
function ResponseToDoData(response) {
result = response.responseXML;
}
function DisplayToDoData() {
// Clear list
todoList = [];
var items = result.querySelectorAll("entry");
for (var ctr = 0; ctr < items.length; ctr++) {
var todo = {};
todo.Id =
items[ctr].querySelector("Id")
.textContent;
todo.TaskName =
items[ctr].querySelector("TaskName")
.textContent;
todo.IsComplete =
Boolean(items[ctr].querySelector("IsComplete")
.textContent === 'true');
todo.Etag =
items[ctr].attributes['0'].nodeValue;
todo.RowVersion =
items[ctr].querySelector("RowVersion")
.textContent;
todoList.push(todo);
}
dataList =
new WinJS.Binding.List(todoList);
todoListUnSelectedView.itemDataSource =
dataList.dataSource;
}
If we seed the database with some sample items and run the project at this point, we will see the Pivot control and the ListView control inside the view-only Pivot Item populated with data.
CRUD Methods
We will now cover the code required to Create Update and Delete data.
First, add the following code to the edit Pivot Item in the default.htm page to provide a ListView and a form to allow records to be edited:
<div id="todoListViewControl" class="listViewList"
data-win-control="WinJS.UI.ListView"
data-win-options="{selectionMode: 'multi'}">
</div>
<button id="addNew">Add New</button>
<button id="deleteSelected">Delete Selected</button>
<br /><br />
<div id="EditBox">
<input id="ToDoId" type="hidden" />
<input id="ToDoEtag" type="hidden" />
<input id="ToDoRowVersion" type="hidden" />
<div>
Task Name
<input id="ToDoTaskName" type="text" /><br />
</div>
<div>
IS Complete
<input id="ToDoEditToggle" type="checkbox" />
</div>
</div><br />
<div>
<button id="saveSelectedEdit"
class="action secondary">Save</button>
</div>
Next, we add the following code (and the global variables they reference) to the WinJS initialization code section. This wires-up the buttons needed to perform editing:
// Edit buttons
var element = document.body;
element.querySelector("#addNew")
.addEventListener("click", doClickAdd, false);
element.querySelector("#deleteSelected")
.addEventListener("click", doClickDelete, false);
element.querySelector("#saveSelectedEdit")
.addEventListener("click", doClickSave, false);
// Set-up Editing
ToDoTaskName = document.getElementById("ToDoTaskName");
ToDoEditToggle = document.getElementById("ToDoEditToggle");
ToDoEditId = document.getElementById("ToDoId");
ToDoEtag = document.getElementById("ToDoEtag");
ToDoRowVersion = document.getElementById("ToDoRowVersion");
ToDoEditId.value = "-1"; // Default value
Create Code
When a user clicks the Add New button, the following code runs to clear any values in the form and to set the Id to –1 so we will know it is a new record:
function doClickAdd() {
// Create a blank record
ToDoEditId.value = "-1" // So we know it is a new record
ToDoTaskName.value = "";
ToDoEtag.value = "";
ToDoRowVersion.value = "";
ToDoEditToggle.checked = false;
}
When the user clicks the save button, the following code runs that saves the record and checks for any validation errors:
function doClickSave() {
// Get current record
var todo = {};
todo.Id = ToDoEditId.value;
todo.TaskName = ToDoTaskName.value;
todo.IsComplete = ToDoEditToggle.checked;
todo.RowVersion = ToDoRowVersion.value;
var accept1 = "application/atomsvc+xml;q=0.8, ";
var accept2 = "application/json;odata=fullmetadata;";
var accept3 = "q=0.7, application/json;q=0.5, */*;q=0.1";
// Make OData Call.
if (todo.Id == -1) {
// In Insert
WinJS.xhr({
type: "POST",
url: "/ApplicationData.svc/ToDoes",
headers: {
"DataServiceVersion": "3.0",
"MaxDataServiceVersion": "3.0",
"Prefer": "return-content",
"Content-type": "application/json;odata=verbose",
"Accept": accept1 + accept2 + accept3
},
data: JSON.stringify(todo)
}).done(
// When the result has completed, check the status.
function completed(result) {
if (result.status === 201) {
// Refresh list
WinJS.xhr({ url: "/ApplicationData.svc/ToDoes" })
.then(ResponseToDoData)
.done(DisplayToDoData);
}
}, function (result) {
// Get the validation error messages from LightSwitch
var JsonError = JSON.parse(result.response);
var XMLError = JsonError['odata.error'].message.value;
var parser = new DOMParser();
var xmlDoc = parser.parseFromString(XMLError, "text/xml");
var ValidationResults =
xmlDoc.querySelectorAll("ValidationResults");
// Loop through all Valication errors
if (ValidationResults['0'] !== undefined) {
for (var ctr = 0; ctr < ValidationResults['0']
.childNodes.length; ctr++) {
// Show Error
alert(ValidationResults['0'].childNodes[ctr]
.childNodes['0'].textContent);
};
}
});
}
}
Update Code
When a user is updating a record, this code detects what record that was selected in the list and retrieves the record:
function doClickEdit(eventInfo) {
// Get the Id of the selected item
var CurrentId = eventInfo.detail.itemPromise._value.data.Id;
// Make OData Call
WinJS.xhr({ url: "/ApplicationData.svc/ToDoes(" + CurrentId + ")" })
.then(ResponseToDoData)
.done(DisplayToDoItem);
}
When a user clicks the save button the following code runs:
// An Update
WinJS.xhr({
type: "PUT",
url: "/ApplicationData.svc/ToDoes(" + todo.Id + ")",
headers: {
"If-Match": ToDoEtag.value,
"DataServiceVersion": "3.0",
"MaxDataServiceVersion": "3.0",
"Prefer": "return-content",
"Content-type": "application/json;odata=verbose",
"Accept": accept1 + accept2 + accept3
},
data: JSON.stringify(todo)
}).done(
// When the result has completed, check the status.
function completed(result) {
if (result.status === 200) {
// Refresh list
WinJS.xhr({ url: "/ApplicationData.svc/ToDoes" })
.then(ResponseToDoData)
.done(DisplayToDoData);
}
}, function (result) {
// handle error conditions.
// A 412 is an OData concurrency error when IF-Match is passed
// See: http://bit.ly/ZJ1IJE
if (result.status === 412) {
// Set _errorMsg
var alert1 = "The data has been modified on the server.";
var alert2 = "Please refresh before making changes";
alert(alert1 + alert2);
}
// Get the validation error messages from LightSwitch
var JsonError = JSON.parse(result.response);
var XMLError = JsonError['odata.error'].message.value;
var parser = new DOMParser();
var xmlDoc = parser.parseFromString(XMLError, "text/xml");
var ValidationResults = xmlDoc.querySelectorAll("ValidationResults");
// Loop through all Valication errors
if (ValidationResults['0'] !== undefined) {
for (var ctr = 0; ctr < ValidationResults['0']
.childNodes.length; ctr++) {
// Show Error
alert(ValidationResults['0'].childNodes[ctr]
.childNodes['0'].textContent);
};
}
})
Delete Code
When a user deletes a record the following code runs:
function doClickDelete() {
// Get the selected items
if (todoListView.selection.count() > 0) {
var accept1 = "application/atomsvc+xml;q=0.8, ";
var accept2 = "application/json;odata=fullmetadata;";
var accept3 = "q=0.7, application/json;q=0.5, */*;q=0.1";
todoListView.selection.getItems().done(function (items) {
for (var ctr = 0; ctr < items.length; ctr++) {
// Get current record
var todo = {};
todo.Id = items[ctr].data.Id;
todo.TaskName = items[ctr].data.TaskName;
todo.IsComplete = items[ctr].data.IsComplete;
todo.Etag = items[ctr].data.Etag;
todo.RowVersion = items[ctr].data.RowVersion;
// Make OData Call.
WinJS.xhr({
type: "DELETE",
url: "/ApplicationData.svc/ToDoes(" + todo.Id + ")",
headers: {
"If-Match": todo.Etag,
"DataServiceVersion": "3.0",
"MaxDataServiceVersion": "3.0",
"Accept": accept1 + accept2 + accept3
},
responseType: "blob"
}).done(
// When the result has completed, check the status.
function completed(result) {
if (result.status === 204) {
// Refresh list
WinJS.xhr({ url: "/ApplicationData.svc/ToDoes" })
.then(ResponseToDoData)
.done(DisplayToDoData);
}
});
}
})
}
}
Special Thanks
This article would not be possible without code from Dale Morrison (@dale_mo – http://blog.ofanitguy.com).
Links
Try WinJS
Using JayData to Consume the Visual Studio LightSwitch OData Business Layer in a AngularJs CRUD Application
Download Code
The LightSwitch project is available at http://lightswitchhelpwebsite.com/Downloads.aspx
(you must have Visual Studio 2013 (or higher) to run the code)