Sep
17
Written by:
Michael Washington
9/17/2013 6:21 PM
When displaying hierarchical data, or lists inside of lists, in a business application, a tree control is usually used. However, a traditional tree control is not well suited to a mobile friendly application. JQuery Mobile recommends using collapsible content blocks.
To enable collapsible content blocks in Visual Studio LightSwitch HTML Client, it is necessary to use the Jewel Lambert method described in jQuery Mobile Collapsible Content Control with LightSwitch. This will enable collapsible content, but it will not handle dynamic content inside the collapsible sections.
To enable dynamic content we can create dynamic HTML inside the collapsible content blocks.
The Sample Application
The sample application allows you to create and edit Books.
You can add Chapters to the Books.
You enter the contents for an entire Chapter and click Save.
The Chapter is broken up into Pages automatically.
You can add multiple Chapters.
Inside The Application
The sample application only has three tables.
Books contain Chapters, and Chapters contain Pages.
All the important custom code is in the AddEditBook page.
To display the Chapters for the selected Book, we simply use a normal List control.
To wrap each Chapter in a collapsible control, we use the Jewel Lambert method described in jQuery Mobile Collapsible Content Control with LightSwitch.
We select the Rows Layout under the Chapters List control and select Edit PostRender Code.
The following code is used to determine if the section is for the last selected Chapter. If it is, the code sets the section to to be opened.
This is important because we will be making a round trip after any edits:
myapp.AddEditBook.ChaptersTemplate_postRender = function (element, contentItem) {
// Get Chapter Name
var ChapterName = contentItem.value.ChapterName;
// Get Chapter Id
var intChapterId = contentItem.value.Id;
// Wrap contents in a collapsible section
if (myapp.LastChapterOpened == undefined) {
myapp.LastChapterOpened = 0;
};
var option;
if (myapp.LastChapterOpened == intChapterId) {
// If the last Chapter was the one that was opened Open it
option = { collapsed: false };
} else {
option = { collapsed: true };
}
collapsibleContent(element, contentItem, ChapterName, option);
};
The following code is used to create the collapsible section:
function collapsibleContent(element, contentItem, groupTitle, options) {
// From:
// jQuery Mobile Collapsible Content Control with LightSwitch
// http://jewellambert.com/jquery-mobile-collapsible-content-control-with-lightswitch/
// Jewel Lambert - @dotnetlore
// provide some defaults for the optional "options" parameter
var defaults = { dataTheme: 'b', contentTheme: 'd', collapsed: false };
options = $.extend({}, defaults, options);
// create a header based on the displayName of the bound content
var h1 = $('<h1>').text(groupTitle);
$(element).prepend(h1);
// build the <div> for the jQM collapsible content control
var DIV = $('<div data-role="collapsible" data-content-theme="' +
options.contentTheme + '" data-theme="' +
options.dataTheme + '" data-collapsed="' +
options.collapsed + '"/>');
// wrap the existing children with this div
$(element).children().wrapAll(DIV);
// tell jQM to render the new <div>
DIV.trigger("create");
}
This will enable collapsible content, but it will not handle dynamic content inside the collapsible sections.
We will now add the code that will create the dynamic content to display the Pages in the Chapter.
The first step is to add a Custom Control bound to the Id property of the Chapter being displayed.
We click Edit Render Code to write the code to write the code that will create the contents for the control.
The following code is used:
myapp.AddEditBook.Page_render = function (element, contentItem) {
// Get the Id of the Chapter
var intChapterId = contentItem.value;
// Load the Chapters for the Book
getPagesForChapter(intChapterId, element);
};
This code calls the following method that dynamically creates a Button for each Page in the Chapter:
function getPagesForChapter(chapterId, element) {
// Make an Ajax call
$.ajax({
type: 'post',
data: {
// Pass the Id of the Book
ChapterId: chapterId,
},
// Call the file handler
url: '../web/ChapterPages.ashx',
// Get the value returned
success: function success(result) {
// Parse the JSON returned
var colPages = jQuery.parseJSON(result);
// Make an array of buttons
var objButton = new Array();
// Create a JQuery Moble container
var objFieldcontain = $("<div data-role='fieldcontain' />");
var objFieldset = $("<fieldset data-role='controlgroup' />");
objFieldset.appendTo($(objFieldcontain));
var CustomUl = $("<ul class='msls-listview ui-listview' data-role='listview' data-inset='false'></ul>");
CustomUl.appendTo($(objFieldset));
// Loop through each page
$.each(colPages, function (index, paramPageContent) {
// Create Button text
var shortText = jQuery.trim(paramPageContent.PageContent)
.substring(0, 300).split(" ").slice(0, -1).join(" ") + "...";
var CustomDiv = "<li tabindex='0' class='ui-li ui-btn ui-btn-up-a ui-btn-up-undefined' ";
CustomDiv = CustomDiv + " data-msls='true' ";
CustomDiv = CustomDiv + "onclick='editPage(" + paramPageContent.PageId + "," + chapterId + "); ";
CustomDiv = CustomDiv + "return false' rel='external>";
CustomDiv = CustomDiv + "<div class='msls-presenter msls-list-child ";
CustomDiv = CustomDiv + "msls-ctl-summary msls-vauto msls-hauto ";
CustomDiv = CustomDiv + "msls-compact-padding msls-leaf msls-presenter-content ";
CustomDiv = CustomDiv + "msls-font-style-normal'>";
CustomDiv = CustomDiv + "<div class='msls-text-container'>";
CustomDiv = CustomDiv + "<span class='id-element'>" + shortText + "</span>";
CustomDiv = CustomDiv + "</div>";
CustomDiv = CustomDiv + "</div>";
CustomDiv = CustomDiv + "<div class='msls-clear'></div>";
CustomDiv = CustomDiv + "</li>";
// Create the Button
objButton[index] = $(CustomDiv);
// Add Div to the CustomUl
objButton[index].appendTo($(CustomUl));
});
// Add contaner to the element
objFieldset.appendTo($(element));
// Tell JQuery Moble to render
objFieldset.trigger("create");
}
});
}
The method above calls the following generic file handler to get the Pages for the selected Chapter:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
namespace LightSwitchApplication.Web
{
[Serializable]
public class ChapterSummaryPage
{
public int PageId { get; set; }
public string PageContent { get; set; }
}
public class ChapterPages : IHttpHandler
{
public void ProcessRequest(HttpContext context)
{
List<ChapterSummaryPage> colChapterSummaryPages = new List<ChapterSummaryPage>();
// Get the LightSwitch serverContext
using (var serverContext = ServerApplicationContext.CreateContext())
{
// Minimal security is to check for IsAuthenticated
if (serverContext.Application.User.IsAuthenticated)
{
int ChapterId = Convert.ToInt32(context.Request.Params["ChapterId"]);
// Get the pages
colChapterSummaryPages = (from Pages in serverContext.DataWorkspace.ApplicationData
.Pages.GetQuery().Execute()
where Pages.Chapter.Id == ChapterId
orderby Pages.SortOrder
select new ChapterSummaryPage
{
PageId = Pages.Id,
PageContent = Pages.PageContent
}).ToList();
}
}
// Return a response
// Create JavaScriptSerializer
System.Web.Script.Serialization.JavaScriptSerializer jsonSerializer =
new System.Web.Script.Serialization.JavaScriptSerializer();
// Output as JSON
context.Response.Write(jsonSerializer.Serialize(colChapterSummaryPages));
}
public bool IsReusable
{
get
{
return false;
}
}
}
}
When a Page is clicked on, the following code is used to display the Page edit screen:
function editPage(pageNumber, ChapterId) {
// Set LastChapterOpened
myapp.LastChapterOpened = ChapterId;
// Create a filter
var filter = "(Id eq " + msls._toODataString(pageNumber, ":Int32") + ")";
// Use filter in query to get the page
myapp.activeDataWorkspace.ApplicationData.Pages
.filter(filter)
.execute()
.then(function (result) {
// Set the scrollTopPosition
var scrollTopPosition = $(window).scrollTop();
// Open the AddEditPage screen
myapp.showAddEditPage(null, {
beforeShown: function (AddEditPageScreen) {
// Set the page on the screen
AddEditPageScreen.Page = result.results[0];
},
afterClosed: function (AddEditPageScreen, navigationAction) {
// After the Edit screen is closed
// scroll to the saved scrollTopPosition
$(window).scrollTop(scrollTopPosition);
// Are there any changes ?
if (navigationAction === msls.NavigateBackAction.commit) {
// Save all changes on the screen
return myapp.applyChanges().then(function () {
// Reload the page to reflect any changes
myapp.showAddEditBook(_screen.Book);
});
}
}
});
});
};
Reloading Pages
The challenge that we have when creating dynamic content is that we lose the change tracking that LightSwitch normally provides. To see any changes caused by any edits, we must reload the screen.
To see this, we will look at the process to add a new Chapter.
When the New Chapter button is pressed the following code runs to open the screen to allow the user to paste in the Chapter content:
myapp.showNewChapter({
beforeShown: function (addNewChapterScreen) {
var strMessage = "Enter new content here... ";
strMessage = strMessage + "Content will be automatically broken up ";
strMessage = strMessage + "into pages of 3000 letters each.";
addNewChapterScreen.ContentOfNewChapter = strMessage;
}
The user pastes in the contents and clicks the Save button.
The code calls the following generic file handler that breaks up the Chapter into pages:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web;
using System.Configuration;
using System.Net;
using System.Web.Security;
namespace LightSwitchApplication.Web
{
[Serializable]
public class ChapterPage
{
public int PageNumber { get; set; }
public string PageContent { get; set; }
}
public class NewChapter : IHttpHandler
{
public void ProcessRequest(HttpContext context)
{
List<ChapterPage> colChapterPages = new List<ChapterPage>();
// Get the LightSwitch serverContext
using (var serverContext = ServerApplicationContext.CreateContext())
{
// Minimal security is to check for IsAuthenticated
if (serverContext.Application.User.IsAuthenticated)
{
int pageLength = 3000;
string strChapterContent =
Convert.ToString(context.Request.Params["ChapterContent"]);
string[] words = strChapterContent.Split(' ');
string part = string.Empty;
int pageNumber = 1;
foreach (var word in words)
{
if (part.Length + word.Length < pageLength)
{
part += string.IsNullOrEmpty(part) ? word : " " + word;
}
else
{
ChapterPage objChapterPage = new ChapterPage();
objChapterPage.PageNumber = pageNumber;
objChapterPage.PageContent = part;
colChapterPages.Add(objChapterPage);
part = word;
pageNumber++;
}
}
ChapterPage objChapterPageLast = new ChapterPage();
objChapterPageLast.PageNumber = pageNumber;
objChapterPageLast.PageContent = part;
colChapterPages.Add(objChapterPageLast);
}
}
// Return a response
// Create JavaScriptSerializer
System.Web.Script.Serialization.JavaScriptSerializer jsonSerializer =
new System.Web.Script.Serialization.JavaScriptSerializer();
// Output as JSON
context.Response.Write(jsonSerializer.Serialize(colChapterPages));
}
public bool IsReusable
{
get
{
return false;
}
}
}
}
The JavaScript code receives the output from the generic file handler and creates the Chapter and Pages:
afterClosed: function (addNewChapterScreen, navigationAction) {
if (navigationAction === msls.NavigateBackAction.commit) {
// Call NewChapter.ashx
// to break up the chapter into pages
$.ajax({
type: 'post',
data: {
ChapterContent: addNewChapterScreen.ContentOfNewChapter,
},
url: '../web/NewChapter.ashx',
success: function success(result) {
// Parse the JSON returned
var colPages = jQuery.parseJSON(result);
// Create a new Chapter
var newChapter = new myapp.Chapter();
newChapter.ChapterName = '[New Chapter]';
newChapter.SortOrder = screen.Chapters.count + 1;
newChapter.setBook(screen.Book);
// Loop through each page
$.each(colPages, function (index, paramPageContent) {
// Create a new Page
var newPage = new myapp.Page();
newPage.SortOrder = paramPageContent.PageNumber;
newPage.PageContent = paramPageContent.PageContent;
newPage.setChapter(newChapter);
});
It then uses the following code to refresh the entire screen:
// Save all changes on the screen
return myapp.applyChanges().then(function () {
// Reload the page to reflect any changes
myapp.showAddEditBook(_screen.Book);
});
Removing The Back Button
We need to remove the back button from the application because reloading adds a page to the navigation history and things get weird when you hit the back button and see pages that have changed.
We add code to the user-customization.css file to suppress the back button graphic.
The back button is no longer displayed.
The user will now use menu buttons to navigate.
LightSwitch Is a JavaScript Framework
If you simply put one list inside of another in the LightSwitch HTML Client screen designer, it will display, but when you click on each section, the inside list of all the other sections will display the content for the section you just selected (rather than just displaying the content for their own section).
Because LightSwitch is basically a JavaScript framework, you can manipulate the screen as needed as you would any other framework.
Links
Top 10 things to know about the LightSwitch HTML Client
How To Perform Angular.Js Functionality Using Visual Studio LightSwitch HTML Client
Dynamically Creating Records In The LightSwitch HTML Client
Download Code
The LightSwitch project is available at http://lightswitchhelpwebsite.com/Downloads.aspx
(you must have Visual Studio 2012 (or higher) installed to run the code)
4 comment(s) so far...
why not use web api service return json data or post data
By neozhu on
9/18/2013 3:40 AM
|
@neozhu - You can also use web api. My example code returns json.
By Michael Washington on
9/18/2013 4:07 AM
|
I can't seem to change the header text colour in this example. I wouldlike it too be white for use with the dark theme.. The rest changes automatically but the header stays blue. Thanks.
By theurbanangst on
2/5/2015 4:51 PM
|
@theurbanangst That is controlled by the LightSwitch .CSS files. You can Google for information on this, I don't have anything, sorry.
By Michael Washington on
2/5/2015 4:53 PM
|