Extending the LightSwitch HTML client with a cascading push menu

This article is about a project with the LightSwitch HTML Client and Visual Studio 2013 Update 2 and addresses a shortcoming in LightSwitch that it only provides very basic menu options (to say the least) …

Introduction

In one of our projects, we had the requirement to spice up the screens with a proper menu which should not only provide more flexible and granular menu options but should also a little fancier.

After a little research, I came across the great post from Michael Washington Visual Studio LightSwitch HTML Client Push Menu where he describes and uses the fantastic Multi-level push menu v2.1.4 control.

@Michael: thank you for your great work! :-)

As you can read in the comments of Michaels blog post , there are some things which are not so developer/user friendly:

  • You have to create a custom control in each screen (which is a hard work, if you have more than 30 screens in your application!)
  • You cannot press F5 without loosing the menu (or better: I didn’t find any way to resolve this)
  • It is not a very good way, how to navigate between the screens with an “if” statement depending on the menu item name

So, I tried to find a solution for these “nuisance and here it is …

Create a new ‘page’ control

First, I extended the the menu control with an additional property (I called it “page”) in the jquery.multilevelpushmenu.js. Look for the createItem() method and add (line 15) .attr(‘page’, item.page)

// Create item DOM element
function createItem() {
  var item = arguments[0],
    $levelHolder = arguments[1],
    position = arguments[2],
    $itemGroup = $levelHolder.find('ul:first'),
    $item = $("<li />");
  (position < ($itemGroup.find('li').length) && position >= 0) ?
    $item.insertBefore($itemGroup.find('li').eq(position)) : $item.appendTo($itemGroup);
  $item.attr({ "style": "text-align: " + ((instance.settings.direction == 'rtl') ? "right" : "left") });
  if (item.id != undefined) $item.attr({ "id": item.id });
  var $itemAnchor = $("<a />")
  .prop({ "href": item.link })
  //custom attribute:
  .attr('page', item.page)
  .text(item.name)
  .appendTo($item),
  $itemIcon = $("<i />")
  .prop({ "class": ((instance.settings.direction == 'rtl') ? "floatLeft " : "floatRight ") + item.icon })
  .prependTo($itemAnchor);
  if (item.items) {
    $itemAnchor.bind(clickEventType, function (e) {
      itemGroupAnchorClick(e, $levelHolder, $item);
    });
    createItemGroupIcon($itemAnchor);
    item.items.level = parseInt($levelHolder.attr('data-level'), 10) + 1;
    createNestedDOMStructure(item.items, $item);
  } else {
    $itemAnchor.bind(clickEventType, function (e) {
      itemAnchorClick(e, $levelHolder, $item);
    });
  }
}

Why? Well, in the file which contains the javascript array with the menu items, you can now specify the screen (or better the method to call that screen):

// JS Aray instead HTML Markup

var arrMenu = [
  {
    title: 'Multi-Push Sample',
    icon: 'fa fa-reorder',
    items: [
      {
        name: 'Home',
        icon: 'fa fa-bullhorn',
        link: '#',
        page: 'navigateHome'
      },
      {
        name: 'Test',
        icon: 'fa fa-html5',
        link: '#',
        items: [
          {
            title: 'Section One',
            icon: 'fa fa-html5',
            items: [
              {
                name: 'Users',
                icon: 'fa fa-code-fork',
                link: '#',
                page: 'showBrowseUsers'
              }
            ]
          }
        ]
      },
      {
        name: 'Tasks',
        icon: 'fa fa-bullhorn',
        link: '#',
        page: 'showBrowseTasks'
      }
    ]
  }
];

Create some wire-up code

Then I added a separate js file dfchls.js with following code to my LightSwitch application:

The method addPushMenu accepts 2 parameters:

  • $element – where to put the menu
  • menuoptions – here you can add several options for the menu

The menu options could be null if you have your required options defined in checkMenuOptions.
As you can see in line 30, the method to open another screen is build via the ‘page’ attribute:

var dfchls = {
  addPushMenu: function ($element, menuoptions) {
    //see http://multi-level-push-menu.make.rs/
    try {
      //check if options are set:
      if ((undefined === menuoptions) || (null === menuoptions)) {
        var menuoptions = {}
      }
      checkMenuOptions(menuoptions);
      if ($element[0].innerHTML == "") {
        // Create Menu
         $element.multilevelpushmenu({
          menu: menuoptions["menu"],
          collapsed: menuoptions["collapsed"],
          containersToPush: menuoptions["containersToPush"],
          overlapWidth: menuoptions["overlapWidth"],
          onItemClick: function() {
            // First argument is original event object
            var event = arguments[0],
            // Second argument is menu level object containing clicked item (<div> element)
            $menuLevelHolder = arguments[1],
            // Third argument is clicked item (<li> element)
            $item = arguments[2],
            // Fourth argument is instance settings/options object
            options = arguments[3];
            var itemName = $item.find('a:first').text();
            //this property is for using Navigation in LightSwitch: e.g. myapp.showBrowseUsers
            if (menuoptions["useLsNavigation"] == true) {
              var pageToOpen = $item.find('a:first').attr("page");
              eval('myapp.' + pageToOpen + '()');
            }
            // Collapse menu
            $element.multilevelpushmenu('collapse');
          }
        });
      }
    }
    catch (e)
    {
      console.log(e);
    }
  }
}

function checkMenuOptions(menuoptions)
{
  if (!menuoptions.hasOwnProperty("menu")) { menuoptions["menu"] = arrMenu; }
  if (!menuoptions.hasOwnProperty("collapsed")) { menuoptions["collapsed"] = true; }
  if (!menuoptions.hasOwnProperty("containersToPush")) { menuoptions["containersToPush"] = [$("div[data-role*='page']")]; }
  if (!menuoptions.hasOwnProperty("overlapWidth")) { menuoptions["overlapWidth"] = 40; }
  if (!menuoptions.hasOwnProperty("useLsNavigation")) { menuoptions["useLsNavigation"] = true; }
}

Replace startup code in ‘default.html’

Now, replace startup script in the default.hm:

<script type="text/javascript">
  $(document).ready(function () {
	msls._run()
	  .then(function () { dfchls.addPushMenu($('#menu'), null); })
	  .then(null, function failure(error) {
		alert(error);
	  });
  });
</script>

Done

That’s all it take to have a fancy menu in your LightSwitch application:

The LightSwitch push menu

Things to do

Of course thare are always things to improve and this example is no different. So here are some topics for a future roadmap:

  • Make the menu read options from a LightSwitch table and render the menu dynamically

  • Apply and evaluate permissions on the menu items (just like SharePoint does it), so a user would only see ‘his’ menu items where he as has permissions for it

  • Integrate jaydata with it to dynamically query other LightSwitch tables

Download

You can download the complete code from our GitHub Account at biz.dfch.LS.PushMenu.

That’s all for now, folks!

Update

As Michael mentioned below, JAYDATA is not necessary to connect to LightSwitch tables. But it is helpful to consume ODATA controllers, as we used in our project.
@Michael: Thx a lot!

Comments

  1. Excellent! My only comment is that you should not need to use JayData in this situation (it adds another dependency you don’t need) the LightSwitch msls.js/data.js/win.js contain all the methods you should need to query the database :)

    • Hi, Michael. Thx for your feedback! You are absolutely right. But in this project, we use ODATA controllers for other requirements, so a JAYDATA example would be helpful, right? :-)

      • Oh of course there is nothing wrong with using JayData if you are also using it for other things or you just like the much easier to use filter syntax that it provides :)

  2. Good that you acknowledged Michaels ideas and then listed issues and provided a solution. Great blogging practice. There’s always ‘things to do’ that would further improve on any work (even our own works ;-) and it’s great that you published your list too. I downloaded your sample app and that ‘just worked’. I love it when that happens. Much appreciated.

  3. ramonitor says:

    Any thoughts on the vertical scrollbar that appears on screens with some content? Disabling the menu removes the vertical scrollbar.

    • Hi. I am not sure, what you mean. Could you explain me your problem?

      • Ramonitor says:

        Sure. In the sample project from your GitHub, open the Tasks screen from the menu. There is a vertical scrollbar on the right side of the screen. I tested with my own project first and double checked with your sample. Same vertical scrollbar. I guess Michael Washington used the 5 px spacer control to prevent it somehow.

        I tested his solution, no vertical scrollbar there, but his implementation differs, so I cannot easily combine both (with my current knowledge of JavaScript that is.. ;-)).

      • The 5 px spacer in my solution is a hack :) I would start with the Carsten Kreissl code and work from there :)

    • Ramonitor says:

      I’m using the code Carstens code and also in his code the vertical scrollbar appears on all screens that have some content. Only on blank screens there is no vertical scrollbar.

  4. Nicolas Lope de Barrios says:

    Great post, it took me a while to figure out I had to add a div with id=menu, but I got it to work. I’d like to know how to customize the “Back” button, translate it to another language.
    PS: I don’t know what you mean by the 5px hack. Maybe a screenshot would help.

  5. Nicolas Lope de Barrios says:

    I have another one: what if I need to pass parameters to a screen? something like showSearchNews(null, null)? or even better, code containing a promise object. Something like:
    myapp.activeDataWorkspace.ApplicationData.SourceByName(“Mashable”).execute()
    .then(function (result) {
    myapp.showSearchNews(null, result.results[0], null);
    });

    Thanks!

  6. Grant Clark says:

    Hi Carsten,

    Thank you for the lovely work on this. And thanks to Micheal of course for introducing us to this control. I was hesitant to implement it before you made these modifications and was toying with other menus that would avoid the problems you solved here. So big thanks to the both of you for collaborating and getting it working nicely.

    I have made some modifications on my side that are working well namely:
    1. My implementation is totally off canvas. I.e. the menu is hidden until the user clicks the LS Home icon/button.

    Screen real estate is precious in the mobile realm, even only 40 odd pixels of it (which is a 6th of certain modern phone’s widths), that you would lose on every page.

    I avoid this by setting the mode to ‘cover’, and overlapwidth to ‘0’: there is likely a better implementation than this which retains overlap menu functionality, but it works for me as I prefer cover mode anyway.

    I also hijacked the LS home button by hacking away at msls.js (not recommended for the faint of heart, and I highly recommend devs to document and comment accordingly if they do) and added a showMenu LS command which has a script that collapses or expands the menu accordingly and replaced the homebutton template “thingy” (terrible implementation from MS, but hey, spilt milk) to call that command instead (so that the homeCommand still works elsewhere).

    I still have to hijack msls.js (puke) a little more to ensure all browse actions collapse the menu (I mean the LS tile clicks, LS dialogs, confirmations, alerts, etc). But so far so good, and its looking great. I’ll likely just spam a bunch of collapse commands everywhere and pray occasionally until it works. Haha. I joke. But no, its really no joke, hacking msls.js I mean ;) I know where to start thanks to the great guys at:
    http://lightswitch.codewriting.tips/topics/#/ViewPage/40/25/%5Bde4699460%5D

    2. I removed all references to FontAwesome (the fonts folder and css file). The footprint it requires is a little large, and I prefer to minutely manage embedded images. My overall image/font repo is only about 50kb for a rather large system with many images, many of which are not from FontAwesome. Funnily enough I also use Metro Studio, and regularly select FontAwesome icons ;), but I embed and compress each one separately and embed them into CSS, which leaves me with a much smaller footprint. I guess that’s just my personal preference, for those that aren’t fanatically about download size like I am, this shouldn’t matter. I guess I’m stating that it is possible for those who don’t want the full FontAwesome included.

    So far the menu is working great in Lightswitch HTML, but I did notice some problems with both your and my implementation that I will try to figure out and post back here, but would appreciate outside inputs while I try (in case someone has already resolved them):

    1. Get all the pushed pages to behave correctly. It seems only the original home page menu pushes the content aside. With all other pages the menu overlaps the LS content, even when you browse back to the home page, it then too behaves “badly”. This is likely something CSS can fix. Not sure yet.

    2. Getting all other navigations/pushes/hashes/etc to collapse the menu (as explained above). The great piece of code you inserted to collapse the menu on itemClick works only for when the menu is clicked. Users generally expect the menu to go away when they stop using it / or interact with pages/tiles/buttons etc. Perhaps that’s something the push menu itself should implement. Clicking/Tapping anywhere that’s not the menu should collapse it.

    Anyway. Once again thanks for this, and hope I can contribute more back.

    Tink.

    • Grant Clark says:

      Okay great, problem #2 already solved:
      I added this little gem I found at:
      http://stackoverflow.com/questions/1403615/use-jquery-to-hide-a-div-when-the-user-clicks-outside-of-it
      to my default.htm script as follows:
      /*menu click/tap away fix*/
      $(document).mouseup(function (e) {
      var container = $(“#menu”);
      if (!container.is(e.target) // if the target of the click isn’t the container…
      && container.has(e.target).length === 0) // … nor a descendant of the container
      {
      $(‘#menu’).multilevelpushmenu(‘collapse’);
      }
      });
      Now whenever the user interacts with anything other than the menu, it collapses.

      • Grant Clark says:

        Just forgot to cater for pads and phones of course, here is the final improved solution:
        /*menu click/tap away fix*/ //touchend
        $(‘body’).bind(“touchend mouseup”, function (e) {
        var container = $(“#menu”);
        if (!container.is(e.target) // if the target of the click isn’t the container…
        && container.has(e.target).length === 0) // … nor a descendant of the container
        {
        $(‘#menu’).multilevelpushmenu(‘collapse’);
        }
        });

      • Grant Clark says:

        And I solved problem #1 with the same ‘dirty’ fix.
        Hope this doesn’t affect performance. Binding to such a frequently used event is not best practice. But I’m happy with it for now…

        /*menu fixes*/
        $(‘body’).bind(“touchend mouseup”, function (e) {
        //update containersToPush all the time…
        $(‘#menu’).multilevelpushmenu(‘option’, ‘containersToPush’, [$(“div[data-role*=’page’]”)]);
        //collapse the menu when not using it
        var container = $(“#menu”);
        if (!container.is(e.target) // if the target of the click isn’t the container…
        && container.has(e.target).length === 0) // … nor a descendant of the container
        {
        $(‘#menu’).multilevelpushmenu(‘collapse’);
        }
        });

      • Ronald Rink says:

        Hi Clark, thanks for your comment and work! Glad that you are able to use this and you even found the time to enhance our (and Michael’s) code! Thanks again, Ronald

  7. Hi, all. Thx for your reply, Ronald. @Clark: Many thanks for your great work!

  8. Nicolas says:

    Hi Carsten and all the people that has contributed to this article: I was wondering if there are any news about applying and evaluating permissions on the menu items. Anybody?

    • Ronald Rink says:

      Hi Nicolas, sorry for the late reply. We created an OData Controller that returns the permissions and roles for the current user to the HTMLClient. The client can then decide to display or to hide menu items or commands depending on the permissions.
      Certainly, the permission is still checked on the server nevertheless.

      Regards, Ronald

      • Nicolas says:

        Hi Ronald, thank you. What’s important is that it can be done. So you’re using Web API as Beth Massi described back in 2013, with a Controller and a global persmissions variable? http://blogs.msdn.com/b/bethmassi/archive/2013/04/17/using-lightswitch-serverapplicationcontext-and-webapi-to-get-user-permissions.aspx
        If so, I would like to know how you check the permissions in the JavaScript code that builds the menu items array, since this is loaded before the permissions. Just a few lines of code illustrating that.

        thanks again, you’ve done a great work.

      • Ronald Rink says:

        I am using Web.Api but with an ODATA Controller (as all my other controllers are ODATA Controllers as well). But the concept is the same.

        Imagine during app start the client retrieves the user’s permissions and roles (and other useful information) and saves them to a global variable `myapp.permissions`, within a client screen the code would then look like this:

        javascript
        function setContentItemFromPermission(name, permission) {
        if (null === myapp.permissions || undefined === myapp.permissions) {
        return false;
        }
        if (null === name || undefined === name || 0 >= name.length) {
        return false;
        }
        if (null === permission || undefined === permission || 0 >= permission.length) {
        return false;
        }
        var permissionName = permission;
        if (-1 == permissionName.indexOf(":"))
        {
        permissionName = "LightSwitchApplication:" + permission;
        }
        var fReturn = myapp.permissions[permissionName] ? true : false;
        if(fReturn)
        {
        screen.findContentItem(name).isVisible = fReturn;
        return fReturn;
        }
        if (null === myapp.roles || undefined === myapp.roles) {
        return false;
        }
        var roleName = permission;
        var fReturn = myapp.roles[roleName] ? true : false;
        if (fReturn) {
        screen.findContentItem(name).isVisible = fReturn;
        }
        return fReturn;
        }

        The code for calling this function for a specific screen item would look like this:


        // ShowBroswProducts --- screen name
        // ProductCanRead --- LightSwitch permission name
        setContentItemFromPermission("ShowBrowseProducts", "ProductCanRead");

  9. Jesus I. S. says:

    Hello,

    Thank you very much for sharing this, it works great and its been really useful to me.

    Just one little thing: if the user press one ítem and then quickly, press another one, then gets the error message: “Cannot perform this action while a navigation is in progress”. Do you know a way to avoid that notification for the final user?

    Regards,
    Jesús I.S.

Trackbacks

  1. […] First, thank you all for your feedback to my first post, regarding the Extending the LightSwitch HTML client with a cascading push menu. […]

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: