The easiest way to create business applications for the Desktop and the Cloud
Applications are creating and consuming more data than ever before. With so much data, how can your users quickly spot trends and get an overview of what’s important? Just as your car presents a dashboard indicating the top priority items, so can your application.
Matt introduced the ServerApplicationContext in his blog post. This article will build on it to show how to build rich dashboard and reporting apps using LINQ aggregate operators, returning data via WebAPI in a JSON format.
Noticing the length of this post, it’s fair to say this isn’t the normal point & click RAD productivity you’ve likely become used to with LightSwitch. This is more of an advanced scenario, showing the extension points we’ve enabled to extend the out of the box features.
There will be several steps, so let’s recap what we’ll be covering
Let’s get started
Download the AmazingPie POS Sample database
We’ll need a basic app to put the reports within. The app will be fairly straight forward with a few screens using the externally linked Amazing Pie database.
1. First, create a new Lightswtich HTML Application (C# or Visual Basic) named AmazingPieManager
2. Select “Attach to external data source”, connecting to the AmazingPie database we’ve just created
3. Select “Database”
4. Enter the connection information for your AmazingPie database
5. Select all the tables
6. Verify the Data Source is named AmazingPieData, and click [Finish]
1. Right-click on the HTML Client node in the Solution Explorer and select “Add Screen”
2. Select the “Browse Data Screen” template, using the “AmazingPieData.Locations” entity as a Data Source
3. We’ll change the default display a bit:
2. Select the “View Details Screen” template, using the “AmazingPieData.Location” entity as a Data Source
We need to navigate from one screen to another, and we do that with Item Tap
1. Open the Location_Browse screen
2. Select the Tile List for Locations
3. In the property grid, click Item Tap where it says none.
4. Click the dropdown for Choose an existing method
5. Select viewSelected
6. Verify Navigate To: is set to Location View and click [OK]
For the purposes of this post, I’m using Kendo UI DataViz controls. There are many great controls and partners we work with, and the meat of this post will be the same regardless of which controls you use.
1. Download Kendo UI. You can use their 30 day free trial.
2. Copy the Kendo UI .zip file to a location and for the ease of dragging into Visual Studio, extract the files to another directory
3. Copy the Kendo UI css and javascript files
4. Add the style and js file references Since LightSwitch works as a single page application model, we’ll just need to add the references to the default.htm page
With the Kendo UI Control references added, let’s create a place for our graphs
1. Switch back to the Location_View screen
2. At the top of the screen designer, Right-Click on Tabs and select Add Tab
3. In the properties sheet:
4. Right-Click on the Reports Tab and select Add Custom Control
5. Now, add the SeatedGuests render code
Before we worry about end to end service calls, we can leverage the simplicity of JSON and create some sample data to display
1. Add the following code to the SeatedGuests_render function
myapp.Location_View.SeatedGuests_render = function (element, contentItem) { var sampleData = [ { "TableType_Id": "Bar", "Seats": 21 }, { "TableType_Id": "Lounge", "Seats": 23 }, { "TableType_Id": "Dining", "Seats": 48 }]
2. We’ll now add the <div> to contain the chart, and use jquery to append it to the element passed into the render function
var locationSeatingChartContainer = $('<div/>'); locationSeatingChartContainer.appendTo($(element));
3. Add the code for the Kendo UI DataViz Chart
locationSeatingChartContainer.kendoChart({ // set the theme theme: $(document).data("kendoSkin") || "default", // set the datasource to use our sample data for now dataSource: { data: sampleData }, title: { text: "Seats Per Location" }, legend: { position: "bottom" }, // Set the chart to a column (vertical) report type seriesDefaults: { type: "column", labels: { visible: true, // the 0:n0 means parameterize the label with the value - first 0 // and then :n0 means use numeric with 0 decimals format: "{0:n0}" } }, // map the vertical series to the number of seats, // notice the field: "Seats" matches the name in the json payload returned from our WebAPI series: [{ field: "Seats", name: "", colorField: "userColor" }], valueAxis: { labels: { format: "{0:n0}" } }, // And the category from our WebAPI json payload categoryAxis: { field: "TableType_Id" } });
4. Now run the app to see how our graph looks
Now that we’ve got the UI working, and a place to display our results, let’s create the WebAPI feed.
If you’re not familiar with WebAPI, you might want to check out the Getting Started series on the ASP.NET blog for a primer on the technology. Joe Binder introduced WebAPI to LightSwitch projects. I’ll provide the details here for completeness, but I’d suggest reading his post as well.
If you’re not familiar with WebAPI, you might want to check out the Getting Started series on the ASP.NET blog for a primer on the technology.
Joe Binder introduced WebAPI to LightSwitch projects. I’ll provide the details here for completeness, but I’d suggest reading his post as well.
The basic premise is we’ll create an MVC like convention for calling URLs, routing them to classes that represent services. Depending on the quantity and names of the parameters, the request will be routed to the appropriate method within the class and return our result using the JSON serializer.
A note about Project References
Using WebAPI requires a few additional project references:
To eliminate the need to manually add the project references, we’ll take advantage of the Visual Studio Templates that add these automatically. We’ll add the Web API Controller Class first, as it adds most of the references we need, specifically those needed to configure the Web API routing.
Deployment Note: If you want to publish your app to an IIS server, note that you’ll need one extra component. Using the NuGet Package manager on your Server project, add the NuGet package “Microsoft.Web.Infrastructure”. This is installed already on some IIS machines, but not all of them.
Joe covered the basics of using WebAPI. As we generate more reports, or use WebAPI for other scenarios, our root folder can get quite filled. To clean things up, we’ll use nested folders and a slightly different configuration for the route.
1. Toggle the View in Solution Explorer to File View
2. We’ll group all our reports under one folder:
3. Add the WebAPI Reports Controller
4. Select the AmazingPieManager.Server\Reports folder, and select [add] – [new item]
5. Select Web API Controller Class from the Web Templates and name it SeatingController
6. Before we complete the SeatingController, we’ll add the Route configuration
7. Configure the route
Note: For more info on Routes see ASP.net Routing
VB Imports System.Web.Routing Imports System.Web.Http C# using System.Web.Routing; using System.Web.Http;
8. Add the following route to the Application_Start method
VBSub Application_Start(ByVal sender As Object, ByVal e As EventArgs) RouteTable.Routes.MapHttpRoute( "ReportsAPI", "reports/{controller}/{id}", New With {Key .id = System.Web.Http.RouteParameter.Optional}) End Sub
C#protected void Application_Start(object sender, EventArgs e) { RouteTable.Routes.MapHttpRoute( name: "ReportsApi", routeTemplate: "reports/{controller}/{id}", defaults: new { id = System.Web.Http.RouteParameter.Optional } ); }
VB Note: The MapHttpRoute method is missing from VB intellisense, however it is in the System.Web.Http.dll, so simply type or paste the above text and as long as you’ve followed the steps above for adding the global.asax (adds the System.Web.Http.dll) , and verify you’ve added the imports statements your project should compile.
9. If everything is added properly, your project should now compile.
Although WebPAI calls can be made directly in the browser, many browsers fail to display JSON results by default. It’s also helpful to see the HTTP Results code to debug what’s happening under the covers. For the purposes of this post, I’ll use Fiddler to test and monitor the WebAPI requests rather than fiddle with the browser settings.
As we’re building out our Reports Controller, and testing with Fiddler to initiate the request, we’ll need the base URL configured.
1. Hit F5 to launch our app and get the root URL. In my case, it’s http://localhost:22700
2. With your app still running Launch Fiddler
3. Select the Composer tab, and enter [your base URL]/reports/Seating/1, then hit [Enter] or the [Execute] button Remembering our route configuration, we’ve configured all requests that start with reports will use the convention of [name]Controller to route our URL to the SeatingController class. The .NET runtime looks for all types that derive from the ApiController type. Since our request is a Get, it’s routed to the Get method. Because we passed a parameter, it’s passed to the Get(int id) overload 4. If you double click the Web Session, click on the Raw tab for result formatting, you’ll see “value” returned from our basic WebAPI controller.
With the basics running, let’s crate a simple collection and return it to see how this looks in Fiddler, and ultimately how our HTML client will receive it.
1. Open the SeatingController from our AmazingPieManager.Server project and delete all the generated methods.
2. Since we’re returning an anonymous type, we need to change the return type from string to object
3. Add the following code to construct a collection and return it:
VBPublic Function [Get](id As Integer) As Object Dim myList As Dictionary(Of String, Integer) = New Dictionary(Of String, Integer) myList.Add("Bar", 21) myList.Add("Lounge", 23) myList.Add("Dining", 48) Dim result = From r In myList Select New With {.TableType_Id = r.Key, .Seats = r.Value} Return result End Function
C#public object Get(int id) { Dictionary<string, int> myList = new Dictionary<string, int>(); myList.Add("Bar", 21); myList.Add("Lounge", 23); myList.Add("Dining", 48); var result = (from r in myList select new { TableType_Id = r.Key, Seats = r.Value }); return result; }
What we’ve done above is create a simple dictionary of name/value pairs Then, using LINQ, construct a new type, with named properties, correlating to the key and values of our dictionary. Because the project defaults to the JSON serializer, we’ll see the results below.
1. Build the project. Note: It’s not actually necessary to hit F5, although you can and set a breakpoint in the Get method.
2. From Fiddler, hit execute to make the HTTP Get Request.
3. This time, Fiddler notices the JSON payload and defaulted the formatting to JSON. You can also switch to TextView, WebView or Raw to see it formatted in a typical JSON format that you may remember seeing earlier when we prototyped our Kendo UI Graph.
Debugging Tip: If you have your service running, you can copy/paste the result into your javascript to test your UI without having to make the service calls
With the basic WebAPI infrastructure in place, let’s start creating some reports. Reports tend to come in groups. For instance, the manager of Amazing Pie wants to evaluate the effectiveness of seating at various locations. Based on the number of seats at the Bar, Lounge, Dining Room, how often are they filled? Should they possibly expand one area, and reduce another? The manager may look at one or many locations. He may look for trends by month or day of the week. Another report may evaluate the menu and which items are selling. To group the various reports, we’ll create one WebAPI APIController class for each type of report.
This is where we’re going to capture the total number of seats for each location. Using LINQ, we’re going to:
1. Open the SeatingController from the Server\Reports folder
2. Add the Extension Methods for query operators
Note: this is an important step. Without these extension methods, the LINQ query operators will not be available on the DataWorkspace
VB Option Infer On Imports System.Net Imports System.Web.Http Imports Microsoft.LightSwitch Imports LightSwitchApplication
C# using Microsoft.LightSwitch;
3. Remove the additional overloads for Get, Put, Post and Delete methods as we’re only using this ApiController for Getting reports.
4. Change the parameter from int to string in the method we edited earlier The Location_Id from the Table entity in our AmazingPie datamodel is of type string
5. Add the following LINQ query
VB Public Function [Get](id As String) As Object Using context As ServerApplicationContext = ServerApplicationContext.CreateContext()
Dim reportResult = From t In context.DataWorkspace.AmazingPieData.Tables Where t.Location.Location_Id = id Group By t.TableType.TableType_Id, t.TableType.DisplayOrder Into Group Order By DisplayOrder Select TableType_Id, Seats = Group.Sum(Function(t) t.TableSize) Return reportResult.Execute() End Using End Function
C# public object Get(string id) { using (ServerApplicationContext context = ServerApplicationContext.CreateContext()) { var reportResult = context.DataWorkspace.AmazingPieData.Tables .Where(t => t.Location.Location_Id == id) .GroupBy(g => new { g.TableType.TableType_Id }) .Select(g => new { TableType_ID = g.Key.TableType_Id.Trim(), Seats = g.Sum(t => t.TableSize) }); return reportResult.Execute(); } }
To understand how the LINQ query is working here:
Remember, Tables is the name of our SQL Table that contains the list of tables in the restaurant. It’s not the collection of SQL Tables.
Order By Tip: I wanted to sort the graph by Bar, Lounge, Dining. Sorting by TableType would have created Bar, Lounge, Dining. The Lookup table for TableType contains the DisplayOrder for just this purpose. This is a common practice to assure your lists are displayed in a logical sequence such as (OrdrePlaced, OnHold, Picking, Shipped, Returned)
With the above query, we’re returning just 3 rows, and two properties. Dramatically smaller than returning all the rows and attempting to summarize this on the client.
[{ "TableType_Id": "Bar", "Seats": 21 }, { "TableType_Id": "Lounge", "Seats": 23 }, { "TableType_Id": "Dining", "Seats": 48 }]
In the case of AmazingPie, the largest quantity of tables is 'BelSquare' which has 32 tables. However, when we start aggregating the number of guests for a given restaurant, we may be processing hundreds of thousands of records. Yet, we only need to display a dozen values. Using LINQ, the new LightSwitch Aggregates such as GroupBy, WebAPI and JSON we can leverage the processing power of each tier of the application, minimize the data sent across the internet and still provide rich visuals to our end users.
With the code in place, we’ll first evaluate the results with Fiddler. Unit testing makes it easy to dissect the data before we start connecting the HTML client with javascript.
1. Build your app, or launch it with F5
2. In Fiddler, remembering your base URL, compose the request. Here’s a visual mapping of what we’ve done so far:
With our WebAPI written and tested, we can now replace our SampleData with the WebAPI call
1. Rather than switch back to logical view, we can stay in the File view to open our Location_View.js which is located under the UserCode folder.
2. Create the reportsAPI variable. It should look similar to this when complete: http://localhost:22700/reports/Seating/SeaBallard We’ll construct the url using the relative path, passing in the reports API path, report class name and the parameter based on the contentItem parameter passed into the SeatedGuests_render function.
3. Below the point where we append the locationSeatingChartContainer, add the following code.
Note, the existing code is greyed for clarity
// Create a <div> to contain our chart var locationSeatingChartContainer = $('<div id="chartContainer" class="chart-wrapper" style="width:600px"></div>'; locationSeatingChartContainer.appendTo($(element)); // http://localhost:22700/reports/Seating/SeaBallard var reportsAPI = "../reports/Seating/" + contentItem.screen.Location.Location_Id;
4. We can now comment out the data: attribute used for our sample data, and provide the transport:attribute which will fetch the data from the WebAP, using the reportsAPI URL.
// Create a <div> to contain our chart var locationSeatingChartContainer = $('<div id="chartContainer" class="chart-wrapper" style="width:600px"></div>'); locationSeatingChartContainer.appendTo($(element)); // http://localhost:22700/reports/Seating/SeaBallard var reportsAPI = "../reports/Seating/" + contentItem.screen.Location.Location_Id; locationSeatingChartContainer.kendoChart({ // set the theme theme: $(document).data("kendoSkin") || "black", // set the datasource to use our WebAPI reports controller dataSource: { //data: sampleData, transport: { read: { url: reportsAPI, dataType: "json" } } }, title: { text: "Seats Per Location"
5. Run the app and see the results The Details Page for BelSquare
And the Graph of the tables for BelSquare
Whew. That was a lot. I did take some extra time to explain some of the nuances and troubleshooting techniques, which hopefully you’ll find valuable as these were the things I tripped over and will hopefully spare you some pain.
Using LightSwitch, WebAPI and the new ServerApplicaitonContext we were able to:
Thanks,
Steve Lasker Microsoft Program Manager Visual Studio LightSwitch
http://blogs.msdn.com/SteveLasker
Very nice. Could you do a piece on adding a jQuery jq grid for example ? A grid is one of the most important parts of nearly every business application.
Very well done :)
Hi Henn, I'll see what we can dig up for the jQuery grid example.
Thanks Michael
Thanks, Steve that works well. I have not seen a really good JS grid yet, anyone have a suggestion to try?
You should check out the Infragistics jQuery grid. Oh, and it's completely free for a limited time.
www.infragistics.com/.../download
No category labels due to case-sensitive typo in SeatingController.cs : TableType_ID = g.Key.TableType_Id.Trim()
:)
Great post. This is extremely helpful.
Kendo UI comes with a grid already. demos.kendoui.com/.../index.html
Full disclosure, I work for Telerik / Kendo UI
Sasha
good job actually
When installing the amazing pie database i get the following:
PS C:\temp\LightSwitch AmazingPie Sample Database\C#> .\LoadAmazingPieIntoLocalDB.ps1
Security warning
Run only scripts that you trust. While scripts from the internet can be useful, this script can potentially harm your
computer. Do you want to run C:\temp\LightSwitch AmazingPie Sample Database\C#\LoadAmazingPieIntoLocalDB.ps1?
[D] Do not run [R] Run once [S] Suspend [?] Help (default is "D"): r
*** Argument 'Action' has an invalid value: 'Import'.
Hi Rafael,
Did you follow the instructions for running the script here? code.msdn.microsoft.com/Lightswitch-AmazingPie-6b4665c6