You are here:   Blog
Register   |  Login

LightSwitch News

 

Sep 17

Written by: Michael Washington
9/17/2013 6:21 PM  RssIcon

image

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

image

The sample application allows you to create and edit Books.

 

image

You can add Chapters to the Books.

image

You enter the contents for an entire Chapter and click Save.

 

image

The Chapter is broken up into Pages automatically.

image

You can add multiple Chapters.

 

Inside The Application

image

The sample application only has three tables.

Books contain Chapters, and Chapters contain Pages.

image

All the important custom code is in the AddEditBook page.

image

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.

image

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.

 

image

 

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;
        }

 

image

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

image

 

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.

 

image

We add code to the user-customization.css file to suppress the back button graphic.

image

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)

2 comment(s) so far...


Gravatar

Re: JQuery Mobile Tree Using Collapsible Sections and Dynamic Views in LightSwitch

why not use web api service return json data or post data

By neozhu on   9/18/2013 3:40 AM
Gravatar

Re: JQuery Mobile Tree Using Collapsible Sections and Dynamic Views in LightSwitch

@neozhu - You can also use web api. My example code returns json.

By Michael Washington on   9/18/2013 4:07 AM

Your name:
Gravatar Preview
Your email:
(Optional) Email used only to show Gravatar.
Your website:
Title:
Comment:
Security Code
CAPTCHA image
Enter the code shown above in the box below
Add Comment   Cancel 

Microsoft Visual Studio is a registered trademark of Microsoft Corporation / LightSwitch is a registered trademark of Microsoft Corporation