Custom JavaScript controls allow you to create user experiences that are tailored to specific use cases and business needs. Given the wealth of code samples, control frameworks, and partner control offerings available today, we wanted to ensure that existing controls could be used as-is within the LightSwitch HTML client.
Having heard some great feedback through the forums and individually, I thought it’d be useful to provide an overview of LightSwitch’s custom control support and share a few tips-and-tricks that have proven useful in my own development.
You can download the LightSwitch HTML Client Preview 2 here.
If you’re new to LightSwitch or the HTML client, you might want to check the following articles before reading through this article:
- Announcing LightSwitch HTML Client Preview 2! Provides an overview of the LightSwitch HTML client preview 2.
- Building a LightSwitch HTML Client: eBay Daily Deals (Andy Kung) If you haven’t had a chance to use the LightSwitch HTML client, Andy’s walkthrough is a great way to get started.
- New LightSwitch HTML Client APIs (Stephen Provine) Stephen’s article offers an in-depth look at all of the programmatic entry points in the HTML client. We’ll be drilling into more detail on a few of the APIs he describes in this article.
An Overview of UI Customization Events
UI Customization in the LightSwitch HTML client is accomplished through the render and postRenderevents. Here’s a basic set of guidelines to help you decide which event to use for a given scenario:
- If you need to insert new elements into the DOM, use the render event. The render event allows you to programmatically create and insert an arbitrary set of DOM elements. For example, if you want to use a control from jQuery UI, use the render event to instantiate the control and insert it into the DOM.
- If you need to augment or otherwise modify DOM elements created by LightSwitch, use the postRender event. The postRender event is commonly used to add a CSS class or DOM attribute to existing DOM elements: for example, you may want to add a jQuery Mobile class to a DOM element created by LightSwitch. We’ll also use the postRender event to change the screen header dynamically later in this article.
A First Step
Let’s jump in and create a simple example to demonstrate how the render event works.
Connect to existing data
We’ll be using the Northwind database for all of the examples in this article. If you don’t have the Northwind database installed, you can download it here.
1. Create a new HTML Client Project (VB or C#)
2. Select “Attach to external data source”
3. Select “Database”
4. Enter the connection information for your Northwind database.
5. Select all entities and click Finish.
Build some basic screens
1. Right-click on the HTML Client node in the Solution Explorer and select “New Screen”
2. Select the “Browse Data Screen” template, using the “Order” entity as a Data Source
Inserting a Custom Control and Handling postRender
With the screen created, the first thing you’ll notice that each control created on the screen has an associated postRender event in the “Write Code” dropdown.
If you need to change or add CSS classes or other DOM attributes on any of the content items shown in the designer, use the postRender event. We’re going to focus on the render event today, though, as it’s the most effective means of creating customized UI in LightSwitch and seems to generate the most interest in the forums.
The render event is only available for custom controls, so we need to add a custom control to the screen before we can use it.
1. Change the default Summary control for the order item to a custom control:
2. Now open the “Write Code” dropdown and select the “RowTemplate_Render” event.
The code stub for the render event includes two parameters, element and contentItem.
myapp.BrowseOrders.RowTemplate_render = function (element, contentItem) {// Write code here.};
The element represents the DOM element-- the
myapp.BrowseOrders.RowTemplate_render = function (element, contentItem) {var orderDate = $(""
+ contentItem.value.OrderDate + ""); orderDate.appendTo($(element)); };
On the first line, we’re using jQuery to create a paragraph element that contains the order date. Since this custom control is hosted inside a list, the content item represents a single Order. “contentItem.value” returns the order instance, and contentItem.value.OrderDate returns the order’s date. Once our custom HTML is created, we use jQuery to append it to the placeholder
Displaying Navigation Properties
Displaying the order date was a good first step, but it probably makes more sense to display the Customer for a given order. Since the Order has a “Customer” navigation property, we can probably just update our code to display the Customer’s CompanyName instead of the date:
myapp.BrowseOrders.RowTemplate_render = function (element, contentItem) {var orderDate = $(""
+ contentItem.value.Customer.CompanyName + ""); orderDate.appendTo($(element)); };
Unfortunately, this doesn’t work; a list of empty list items is displayed:
Let’s set a breakpoint in our code and use the Watch Window to see what’s going on:
Our list items are blank because contentItem.value.Customer is “undefined”. Typically “undefined” errors are rooted in typos in one form or another, but in this case the binding path is correct. In fact, the problem is that the Order.Customer property is not loaded. We need to understand a bit more of the LightSwitch view model to understand why the property isn’t loaded and how we can fix the problem.
When we build LightSwitch UI using the designer, LightSwitch figures out the data elements that should be loaded on the screen based on the controls in the layout tree. For example, if we were to manually drag the Order.Customer property onto the screen, LightSwitch would make sure that the Customer is fetched when the screen is loaded. But in this instance, we’re building custom UI and LightSwitch has no way of knowing what data should be loaded; we have to explicitly tell it to load the customer. This is pretty easy to do using the designer:
1. Click the “Edit Query” link for the Orders collection in the designer.
2. Now find the “Managed included data” link in the property sheet for the query.
3. Change “Customer: Excluded (Auto)” to “Customer: Included”
Changing this setting to “Included” tells LightSwitch to fetch the Customer for each order that’s queried; it is effectively the span of the query. Now we can run our application again and see that the Company name is properly displayed.
Render function Tips
1. When referring to the screen’s view model (e.g., contentItem.value.OrderDate), make sure that the casing of view model properties in code matches the casing used in the screen designer exactly.
var orderDate = $(""
+ contentItem.value.OrderDate + "");
3. If you would like to use jQuery to interact with the container div—the element parameter—make sure to wrap it in $(element). The element passed in is not a jQuery object, so you need to create one.
orderDate.appendTo($(element));
4. If you see “undefined” errors, inspect the “undefined” element under the debugger to ensure that the code does not include a typo. The Visual Studio Watch window is a great tool for this purpose.
5. Whenever a custom control is displaying navigation properties, always include the navigation property in the parent query’s span by using the “Manage included data” feature.
An Introduction to Data Binding
Thus far we’ve been adding custom UI inside list items, but many scenarios will call for custom UI on a standalone screen. It’s possible to replace any UI on a LightSwitch screen with the exception of the header/title using the render event described above, but we have some new challenges to deal with when working outside the context of the list. In particular, it’s much more likely that the data we’re displaying in our custom control will change over the lifetime of the screen.
For example, consider a scenario where we’re displaying the order’s ShippedDate on the OrderDetail screen:
1. The user gestures to “Ship It” on the order detail screen.
2. The “Ship It” button sets the order’s ShippedDate to the current date.
If we use the above approach, our UI will not reflect the new ShippedDate because the render function is only called when the control is created. LightSwitch’s databinding APIs are a much better option for this scenario. In fact, it’s best to always use data binding when displaying LightSwitch data to guard against scenarios where data is loaded or changed asynchronously.
Add a detail screen
1. Add a new screen to the application
2. Select the “Add/Edit” screen template and choose “Order” as the data source
3. Switch back to the BrowseOrders screen and select the OrdersList
4. Click the “Tap” action link in the property sheet
5. Configure the tap action to launch our “EditOrder” screen as follows:
6. Now switch back to the Edit Order screen and select the top-level RowsLayout and gesture to handle the postRender event
Set the screen title dynamically
As our first introduction into LightSwitch databinding, we’re going to set the title of the screen dynamically. We could grovel through the DOM to find the element that contains the screen title, but it’s much easier to update the underlying view model property to which the screen title is bound: “screen.details.displayName”
Add the following to the postRender function:
myapp.EditOrder.Details_postRender = function (element, contentItem) { contentItem.dataBind("screen.Order.ShipAddress", function (newValue) { contentItem.screen.details.displayName = newValue; }); };
The dataBind(…) function accepts a binding path as a string and a function that will be called when the corresponding value is loaded or changed. However simple, the dataBind method saves us from manually hooking up change listeners to the view model. If we run the application, we’ll see the screen title change in accordance with the ShipAddress.
Databinding a scalar field
We can extend the above example to add a new data-bound custom control to the screen.
1. Add a new custom control to the Details tab of the EditOrder Screen
2. Set the binding path for the control to the ShippedDate
3. Open the Write Code dropdown and select “Order_ShippedDate_Render”
4. Add the following code to the render function.
myapp.EditOrder.Order_ShippedDate_render = function (element, contentItem) {var shippedDate = $(''); shippedDate.appendTo($(element)); contentItem.dataBind("stringValue", function (newValue) { shippedDate.text(newValue); }); };
There are a few subtle differences between this code and the earlier example that set the screen title:
1- contentItem in this example represents the ShippedDate property, so the binding path is relative to the ShippedDate.
2- We’re binding to the “stringValue”. As its name implies, the string value is a formatted string representation of the underlying property value. Binding to “stringValue” instead of “value” is advantageous when using a text control, because “stringValue” will give us a properly formatted string; whereas simply displaying the JavaScript Date object in this case does nothing—setting the “text” attribute of a
to something other than a string will throw an error.
Two-way data binding
Two-way data binding works is just an extension of the above example. We’ll just change the paragraph element to an and listen for its change events:
myapp.EditOrder.Order_ShippedDate_render = function (element, contentItem) {var shippedDate = $(''); shippedDate.appendTo($(element));// Listen for changes made via the custom control and update the content // item whenever it changesshippedDate.change(function () {if (contentItem.value != shippedDate.val()) { contentItem.value = new Date(shippedDate.val()); } });// Listen for changes made in the view model and update our custom controlcontentItem.dataBind("stringValue", function (newValue) { shippedDate.val(newValue); }); };
When the input value changes, we set the underlying content item’s value. In this example, the content item maps to a date field, so we need to assign it a date object.
Databinding Tips
Although data binding in LightSwitch only entails a single API, there are a number of things to keep in mind when you’re using it. Let’s recap:
· Always use data binding when displaying LightSwitch data. There is no guarantee that the data you’re trying to display in a render event will be loaded when your render event is fired; databinding allows you to guard against cases where data isn’t loaded when your control is created or when that data changes elsewhere in the application.
· Bind to the stringValue property on the contentItem when displaying data as text. The stringValue gives you the formatted string representation of whatever type you’re displaying.
· When setting view model values, make sure the source and target property values are the same type; otherwise the assignment will fail silently. (See our two-way databinding example above.)
· Whenever possible, set the binding path of your control in the designer and use relative binding in code. Setting the binding path in the designer has several advantages: (1) it tells LightSwitch the data you’re using so it can be loaded automatically; (2) it allows your custom control code to be more generalized and re-used more easily—you aren’t hard-coding the binding to a particular screen; (3) relative binding allows you to easily access relevant content item properties, such as the stringValue and displayName.
Interacting with “live” DOM elements
jQueryMobile and LightSwitch do most of the heavy lifting required to manipulate the DOM, but you may run into scenarios that require you to interact with the “live” DOM. The good news is that the full extent of frameworks like jQuery are at your disposal within LightSwitch. It is important to keep in mind, however, that the DOM that we see inside our _render and postRender methods is not, strictly speaking, the “live” DOM—it’s the DOM as it exists before jQueryMobile expands it. While this is generally a good thing—it enables us to use jQueryMobile classes and other DOM annotations in our code—it does mean that we have to wait for jQueryMobile to complete its expansion before the DOM is live.
Let’s walk through a simple example that popped up on the forums to illustrate the problem. Consider that you want to set focus on our ShippedDate custom control from above. Easy, right? There’s a jQuery focus event we can probably just call…
myapp.EditOrder.Order_ShippedDate_render = function (element, contentItem) {var shippedDate = $(''); shippedDate.appendTo($(element));// Listen for changes made via the custom control and update the content // item whenever it changesshippedDate.change(function () {if (contentItem.value != shippedDate.val()) { contentItem.value = new Date(shippedDate.val()); } });// Listen for changes made in the view model and update our custom controlcontentItem.dataBind("stringValue", function (newValue) { shippedDate.val(newValue); }); shippedDate.focus(); };
If we run the application, you’ll notice that our custom control doesn’t receive focus as expected. The problem is that the control isn’t visible and/or “live” yet so the browser can’t set focus on it. We can work around this by wrapping the call—and any other similar types of operations—in setTimeout(…).
myapp.EditOrder.Order_ShippedDate_render = function (element, contentItem) {var shippedDate = $(''); shippedDate.appendTo($(element));// Listen for changes made via the custom control and update the content // item whenever it changesshippedDate.change(function () {if (contentItem.value != shippedDate.val()) { contentItem.value = new Date(shippedDate.val()); } });// Listen for changes made in the view model and update our custom controlcontentItem.dataBind("stringValue", function (newValue) { shippedDate.val(newValue); });setTimeout(function () {if (shippedDate.is(':visible')) { shippedDate.focus(); } }, 1000); };
While this might seem arbitrary, it’s the simplest way to workaround issues of this sort; keep it in mind if you need to interact with the live DOM.
Wrapping Up
Custom controls and databinding are powerful capabilities within LightSwitch; they are the escape hatch that allow you to create differentiated user experiences atop the default LightSwitch client. It’s our hope that you find the entry points described here useful in your application development. If you run into any issues, head on over to the forums and we’d be more than happy to help you.
As a follow-up to this post, we’ll dive into the CSS side of UI customization and show how to use custom CSS in conjunction with the code you’ve seen today.
Thanks for your support and taking the time to try out Preview 2!
- Joe Binder, Program Manager LightSwitch Team