Quantcast
Channel: Category Name
Viewing all articles
Browse latest Browse all 10804

Dashboard Reports with LightSwitch, WebAPI and ServerApplicationContext– Part Deux (Steve Lasker)

$
0
0

Overview

With a little homage to Hot Shots Part Deux we’ll drill in a bit deeper utilizing the ServerApplicationContext for creating Dashboard reports. Part 1 is here: Create Dashboard Reports with LightSwitch, WebAPI and ServerApplicationContext

LightSwitch Productivity/Extendability disclaimer: As with Part 1, 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.

In Part 2 I’ll cover:

  • Adding a second set of charts that enable drilling into monthly data to see a weekly slice of data based on the click events of the chart
  • Adding date controls to select the date range of the report
  • Interactively making selections in the screen, changing the data displayed
  • Calling Stored Procedures from WebAPI

Let’s get started…

Create the AmazingPie Restaurant Point of Sale (POS) database

As with part one, we’ll utilize the AmazingPie POS Sample database. However, this code also assumes you’ve already completed Part 1.

Adding the new Report

We’ll start by adding a WebAPI, allowing us to query based on a time period of Monthly or Weekly.

1. As in Part 1, we’ll need to switch from the logical view to file view so we can interact directly with files.

2. Within the Reports folder, add the new Web API controller

If you’ve followed the previous step, Visual Studio will remember the last few template types you’ve added, and you’ll see a shortcut in your menu:

image

This will provide an abbreviated prompt. Name our controller SeatedGuestsController

image

3. Remove all the template Get, Put, Post, Delete methods to make way for our new Report methods.

4. 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, and SqlFunctions will require the full namespace


VB
Option Infer OnImports System.NetImports System.Web.Http
Imports Microsoft.LightSwitchImports LightSwitchApplicationImports System.ConfigurationImports System.DataImports System.Data.SqlClientImports
System.Data.Objects.SqlClient
C#using System;using System.Collections.Generic;using System.Linq;using System.Net;using System.Net.Http;using System.Web.Http;using Microsoft.LightSwitch;using System.Data;using System.Configuration;using System.Data.SqlClient;using System.Data.Objects.SqlClient;

5. Add the following Method to expose our WebAPI Method

The get will capture the period, and we’ll call sub methods for each specific report


VB
Public Class SeatedGuestsControllerInherits ApiControllerPublic Function GetValue(period As String, id As String,
beginDate
As DateTime, endDate As DateTime)
Dim retVal As Object = Nothing If period.ToLower() = "monthly" ThenretVal = GetMonthlyReport(id, beginDate, endDate)ElseIf period.ToLower() = "weekly" ThenretVal = GetWeeklyReport(id, beginDate, endDate)End If Return retValEnd Function Public Function GetMonthlyReport(location_Id As String,
beginDate
As DateTime,
endDate
As DateTime) As Object Return Nothing End Function Public Function GetWeeklyReport(location_Id As String,
beginDate
As DateTime,
endDate
As DateTime) As Object Return Nothing End Function End Class
C#public object Get(string period, string id, DateTime beginDate, DateTime endDate) {object retVal = null;if (period.ToLower() == "monthly") {
        retVal = GetMonthlyReport(id, beginDate, endDate);
    } else if (period.ToLower() == "weekly"){
        retVal = GetWeeklyReport(id, beginDate, endDate);
    }return retVal;
}private object GetMonthlyReport(string location_Id, DateTime beginDate, DateTime endDate) {return null;
}private object GetWeeklyReport(string location_Id, DateTime beginDate, DateTime endDate) {return null;
}

6. Add the GetMonthlyReport LINQ Query    

From Part 1, you’ll remember we instance the ServerApplicationContext within a using block to be sure it gets disposed when we’re done. We also call report.Execute() to make sure the LINQ query is executed before the Context is disposed.    

Looks like there is a horse before the cart, so we need to get the ordering correct.

Lastly, we use the SqlFunctions to create a string format of Month/Year which we need for our Chart to display properly. Using SqlFunctions is important as the query is sent to SQL Server, which wouldn’t know how to parse:  String.Format("{0}/{1}", x.Key.Month, x.Key.Year)


VB
Public Function GetMonthlyReport(location_Id As String,
                                 beginDate As DateTime,
                                 endDate As DateTime) As Object
   Using context As ServerApplicationContext = ServerApplicationContext.CreateContext()Dim report = From olc In context.DataWorkspace.AmazingPieData.OrderLifeCyclesWhere olc.Order.Location.Location_Id = location_Id AndAlsoolc.EventType_Id = "Complete" AndAlsoolc.EventTime >= beginDate AndAlsoolc.EventTime <= endDateGroup By olc.EventTime.Year,
                              olc.EventTime.Month,
                              olc.Order.Table.TableType.TableType_Id,
                              olc.Order.Table.TableType.DisplayOrder Into Group
                     Order By Year, Month, DisplayOrderSelect MonthYear = SqlFunctions.StringConvert(Month, 2) + "/" +                                         
SqlFunctions.StringConvert(Year, 4), TableType_Id, Seats = Group.Count()Return report.Execute()
End Using End Function
C#
private object GetMonthlyReport(string location_Id, DateTime beginDate, DateTime endDate) {using (ServerApplicationContext context = ServerApplicationContext.CreateContext()) { var report = context.DataWorkspace.AmazingPieData.OrderLifeCycles .Where(olc => (olc.Order.Location.Location_Id == location_Id) && (olc.EventType_Id == "Complete") && (olc.EventTime >= beginDate) && (olc.EventTime <= endDate)) .GroupBy(x =>new { x.EventTime.Year, x.EventTime.Month, x.Order.Table.TableType.TableType_Id, x.Order.Table.TableType.DisplayOrder }) .OrderBy(o =>new {o.Key.Year, o.Key.Month, o.Key.DisplayOrder}) .Select(x =>new { MonthYear = SqlFunctions.StringConvert((decimal) x.Key.Month, 2) + "/" + SqlFunctions.StringConvert((decimal)x.Key.Year, 4), TableType_Id = x.Key.TableType_Id, Seats = x.Count() });return report.Execute(); }; }

7. Add the GetWeeklyReport LINQ Query 

The query is very similar to the Monthly report, however this time we’re grouping by the day of the week using the SqlFunctions TSQL equivalent of DATEPART(DW, EventTime)


VB
Public Function GetWeeklyReport(location_Id As String,
beginDate
As DateTime,
endDate
As DateTime) As Object
Using
context As ServerApplicationContext = ServerApplicationContext
.CreateContext()
Dim report = From olc In context.DataWorkspace.AmazingPieData.OrderLifeCyclesWhere olc.Order.Location.Location_Id = location_Id AndAlsoolc.EventType_Id = "Complete" AndAlsoolc.EventTime >= beginDate AndAlsoolc.EventTime <= endDateGroup By DayOfWeek = SqlFunctions.DatePart("DW", olc.EventTime), olc.Order.Table.TableType.TableType_Id, olc.Order.Table.TableType.DisplayOrder Into Group Order By DayOfWeek, DisplayOrderSelect DayOfWeek, TableType_Id, Seats = Group.Count()
Return report.Execute()End Using
End Function
C#private object GetWeeklyReport(string location_Id, DateTime beginDate, DateTime endDate) {using (ServerApplicationContext context = ServerApplicationContext.CreateContext()) {

        var report = context.DataWorkspace.AmazingPieData.OrderLifeCycles
            .Where(olc => (olc.Order.Location.Location_Id == location_Id) &&
                (olc.EventType_Id == "Complete") &&
                (olc.EventTime >= beginDate) &&
                (olc.EventTime <= endDate))
            .GroupBy(x =>new { 
                DayOfWeek = SqlFunctions.DatePart("DW", x.EventTime), 
                x.Order.Table.TableType.TableType_Id, 
                x.Order.Table.TableType.DisplayOrder
            })
            .OrderBy(o =>new {o.Key.DayOfWeek, o.Key.DisplayOrder})
            .Select(x =>new {
                DayOfWeek = x.Key.DayOfWeek,
                TableType_Id = x.Key.TableType_Id,
                Seats = x.Count()
            });return report.Execute();
    }
}

Testing with Fiddler

Let’s do our normal Fiddler testing to be sure we have this portion correct before delving into javascript

1. Hit F5 to get the base URL of your app

2. Launch Fiddler and paste the base URL into the Composer

3. Modify it to include the following query string.  

http://localhost:30550/reports/seatedGuests/?period=monthly&id=BelSquare%20&beginDate=2010-05-07&endDate=2013-02-23

Note that we have period =monthly, and we’ve set the begin/endDate using the JSON serialized format.
(I’ve removed the time since we zero it out anyway)

Tip: Had I not tested the output with Fiddler, I wouldn’t have noticed that SqlFunctionsStringConvert was padding the month & year

Without setting the string lengthUsing the StringConvert overload to set the length
MonthYear = SqlFunctions.StringConvert((decimal) x.Key.Month)  
  + "/" +   SqlFunctions.StringConvert((decimal)x.Key.Year)
MonthYear = SqlFunctions.StringConvert((decimal) x.Key.Month, 2)
    + "/" + SqlFunctions.StringConvert((decimal)x.Key.Year, 4)
imageimage

Adding the Graphs

With our WebAPI Reporting Service tested and ready, let’s add the new graphs

1. Switch back to Logical View

2. Open the Locaion_View screen

3. Select the Report Tab Rows Layout control to get the add control menu and select New Custom Control…

image

4. Using the property sheet, rename it ServedGuests

image

5. Choose Write Code–>ServedGuests_render

6. Add the following code to create the chart containers

tags

Note: greyed out code indicates the previous code as the insertion reference point


myapp.Location_View.ServedGuests
_render =
function (element, contentItem) {// Each KendoUI chart needs it's own container // One for the Monthly graphvar customersPerMonthChartContainer = $('
'
); customersPerMonthChartContainer.appendTo($(element));// One for the weekly charts.var customersPerWeekChartContainer = $('
'
); customersPerWeekChartContainer.appendTo($(element));

7. Add Kendo UI Chart Control for the Monthly Report

Note that for now, we’re hardcoding the date range. We’ll build this up slowly to avoid tripping over coding issues with things like javascript dates. We’ve also placed this within a function, which we call immediately. Later on, we’ll add the ability to change the date range, and for each date change, we’ll want to redraw the chart.


displayMonthlyChart()
function displayMonthlyChart() {//setup the URI to the report, with the Location Parameter, and the date rangevar reportsAPI = "../reports/seatedGuests/?period=monthly&id=" + contentItem.screen.Location.Location_Id +"&beginDate=2010-07-20&endDate=2013-01-04";// Add a Kendo UI BarChartcustomersPerMonthChartContainer.kendoChart({// set the themetheme: $(document).data("kendoSkin") || "default",// Set the sourc to use our WebAPI, with a json payloaddataSource: { transport: { read: { url: reportsAPI, dataType: "json"} },// use the TableType_Id to create groupings in the chartgroup: { field: "TableType_Id"} }, title: { text: "Customers Per Month, By Table Type"}, legend: { position: "bottom"},// Set the chart to a column (vertical) report typeseriesDefaults: { 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 decimalsformat: "{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 WebAPIseries: [{ field: "Seats", name: "", colorField: "userColor", labels: { rotation: 320 } }], valueAxis: { labels: { format: "{0}"} },// And the category is grouped by Month, also from our WebAPI json payloadcategoryAxis: { field: "MonthYear", labels: {//format: "{0:n0}"format: "{0}", rotation: 300 } } }); // Close of Kendo Monthly Chart}

8. Let’s F5 to see how things work so far:

image

Not so bad. We have quite a lot of data here, visually presented, allowing your users to know where to drill in, looking for more details.

Add Weekly Chart

We can see how the Bel Square location is trending during the year, but what about weekly, for a given month? We’ll now add another chart that will display the averaged seated guests, by day of the week for the month the user clicks on.

1. Add the displayWeeklyChart function, just blow our Monthly chart, and still within the render method

Note: we’re still commenting out the date ranges as we’ll plumb this through once we get the basic charts displayed. I’ve also placed the weekly chart within a function as we only want to show this chart when the user clicks on a time period within the monthly chart. Each time they click a different time slice, we’ll want to update the chart by calling the displayWeeklyChart function.


}
});
// Close of Kendo Monthly Chart }function displayWeeklyChart(beginDate, endDate) { customersPerWeekChartContainer.fadeIn(500);//setup the URI to the report, with the Location Parameter, and the date rangevar reportsAPI = "../reports/SeatedGuests/?period=weekly&id=" +
contentItem.screen.Location.Location_Id +
"&beginDate=2010-07-20&endDate=2013-01-04";customersPerWeekChartContainer.kendoChart({ theme: $(document).data("kendoSkin") || "default", dataSource: {transport: { read: { url: reportsAPI, dataType: "json"} }, group: { field: "TableType_Id"} }, title: { text: "Customers Per Week, By Table Type"}, legend: { position: "bottom"}, seriesDefaults: { type: "column", labels: { visible: true, format: "{0:n0}"} }, series: [{ field: "Seats", name: "", colorField: "userColor"}], valueAxis: { labels: { format: "{0}"} }, categoryAxis: { field: "DayOfWeek"} }); };

Add a Click Event Handler to the Bar Chart

To drill into the data, we’ll enable click events on the chart. Most charts support this powerful feature.

1. Below the configuration of the Monthly chart, we’ll add the seriesClick event handler


categoryAxis: { field:
"MonthYear", labels: {//format: "{0:n0}"format: "{0}",rotation: 300 } }, // <-- note the comma to continue the configuration of the chart // hook up the Click event to drill into weekly reportsseriesClick: onSeriesClick }); // Close of Kendo Monthly Chart}function onSeriesClick(e) {// Split the category label into Month/Yearvar dateElements = e.category.split("/");// javascript uses Month 0 as January - so, start from Jan, // and add the selected month, which needs to be decremented to account for 0 base javascript // compared to the rest of the human readable worldvar beginDate = new Date(dateElements[1], parseInt(dateElements[0]) - 1, 1);var endDate = new Date(beginDate.getFullYear(), beginDate.getMonth() + 1,
beginDate.getDate(), beginDate.getHours()); displayWeeklyChart(beginDate, endDate); }
function
displayWeeklyChart(beginDate, endDate) { customersPerWeekChartContainer.fadeIn(500);

Adding Date Parameters

We’ll now add begin and endDate controls to narrow the results.

Adding Screen Parameters

We’ll want these values to come from the user, and don’t forget, LightSwitch uses a ViewModel approach

1. Open the Location_View screen

2. In the Reports Tab, add a new Group

  • Name it dateRange
  • Change the Group to Column layout
  • Move it below the Served Guests custom control

3. Add two new screen properties

  • On the top of the screen designer, select Add Data Item…
  • Change the Member Type to Local Property
    • Type = Date
    • Uncheck Is Required
    • and name it beginDate

clip_image001

  • Add another optional local property for endDate with the same options

4. Add beginDate and endDate screen properties as controls on the screen

  • Below the date Range Columns Layout (grouping), click the add button to add the two local properties

clip_image002[4]

  • Change the Control Types to Date Picker for both of the screen parameters
  • You’re screen designer should now look like this…

clip_image003[4]

5. Lets set the default values for the date range properties:

  • From the Write Code menu, select created

image

  • Default the beginning date to the date the location opened for business and the end date to today
    While we’re here, we’ll also set the screen display name to the Id of the location

myapp.Location_View.created
=
function (screen) { screen.beginDate = screen.Location.LocationOpened; screen.endDate = new Date(); screen.details.displayName = "Location: " + screen.Location.Location_Id; };

6. Change our Monthly Chart to use the new screen parameters  

Note: we use the Date.toJSON() function to convert the date to a format that will serialize across URLs.


function
displayMonthlyChart() { if (contentItem.screen.endDate < contentItem.screen.beginDate) return;
    var reportsAPI = "../reports/seatedGuests/?period=monthly&id=" + 
contentItem.screen.Location.Location_Id +
"&beginDate=" + contentItem.screen.beginDate.toJSON() +"&endDate=" + contentItem.screen.endDate.toJSON();

 

7. Lets hange the displayWeeklyChart() to also use the date range screen parameters by replacing the fixed date with the begin/end date parameters. Note the toJSON() conversion to make sure we encode the dates for URLs


function
displayWeeklyChart() {//setup the URI to the report, with the Location Parameter, and the date rangevar reportsAPI = "../reports/SeatedGuests/?period=weekly&id=" +
contentItem.screen.Location.Location_Id +
"&beginDate=" + contentItem.screen.beginDate.toJSON() +"&endDate=" + contentItem.screen.endDate.toJSON();
customersPerWeekChartContainer.kendoChart({

8. We’ll now Databind the date ranges to re-display the monthly report when the dates change. We’ll also  hide the chart until the user re-clicks a monthly slice.


// add the whole thing to the LightSwitch custom control
customersByTableTypeContainer.appendTo($(element));// Databind the date range changes to rebuild the chartcontentItem.dataBind("screen.beginDate", function () { customersPerWeekChartContainer.fadeOut(500); displayMonthlyChart(); }); contentItem.dataBind("screen.endDate", function () { customersPerWeekChartContainer.fadeOut(500); displayMonthlyChart(); });
displayMonthlyChart();


Run it and notice that now you can click on a month to get the weekly breakdown and when we change the dates, the charts will refresh.

image

Using Stored Procedures

Since reporting is typically based on aggregating large amounts of data, and you may be working with existing systems, your DBA may already have several stored procedures created, and you just need to return the results. Lets replace the weekly query with a stored procedure.

Getting the Connection String

To call the stored procedure, we’re going to use the traditional ADO.net objects directly. SqlConnection, SqlCommand and SqlDataReader. Of course, we need a connection string which we’ll read from the ConfigurationManager. This requires an additional reference.

1. From the File view, select Add Reference on the Server project

2. Check off System.Configuration

image

3. Open the SeatedGuestsController

4. Add the using/import statements for referencing the SQL ADO.net objects and the System.Configuration namespace


VB
Imports System.ConfigurationImports System.DataImports System.Data.SqlClient
C#
using System.Configuration;using System.Data;using System.Data.SqlClient;

5. Modify the GetWeeklyReport code to call the stored procedure

VB
Public Function GetWeeklyReport(location_Id As String,
                                beginDate As DateTime,
                                endDate As DateTime) As Object

    Using context As ServerApplicationContext = ServerApplicationContext.CreateContext()Using conn As SqlConnection =New SqlConnection(ConfigurationManager.ConnectionStrings(
                context.DataWorkspace.AmazingPieData.Details.Name).ConnectionString)Dim cmd As New SqlCommand()
            cmd.Connection = conn
            cmd.CommandText = "GetSeatedGuestsPerTableTypePerLocationByWeekday"cmd.CommandType = CommandType.StoredProcedure
            cmd.Parameters.Add(New SqlParameter("@location_Id", location_Id))
            cmd.Parameters.Add(New SqlParameter("@beginDateRange", beginDate))
            cmd.Parameters.Add(New SqlParameter("@endDateRange", endDate))
            cmd.Connection.Open()Using reader As SqlDataReader = cmd.ExecuteReader(CommandBehavior.CloseConnection)Dim reportResult = (From dr In reader.Cast(Of IDataRecord)()Select DayOfWeek = dr.GetInt32(0),
                                           TableType_Id = dr.GetString(1),
                                           Seats = dr.GetInt32(2)).ToList()Return reportResultEnd Using
        End Using
    End Using
End Function
C#private object GetWeeklyReport(string location_Id, DateTime beginDate, DateTime endDate) {object reportResult = null;using (ServerApplicationContext context = ServerApplicationContext.CreateContext()) {using (SqlConnection conn = new SqlConnection(ConfigurationManager.ConnectionStrings
[context.DataWorkspace.AmazingPieData.Details.Name].ConnectionString)) { SqlCommand cmd =
new SqlCommand(); cmd.Connection = conn; cmd.CommandText = "GetSeatedGuestsPerTableTypePerLocationByWeekday"; cmd.CommandType = CommandType.StoredProcedure; cmd.Parameters.Add(new SqlParameter("@location_Id", location_Id)); cmd.Parameters.Add(new SqlParameter("@beginDateRange", beginDate)); cmd.Parameters.Add(new SqlParameter("@endDateRange", endDate)); cmd.Connection.Open();// Execute the reader into a new named type to be json serializedusing (SqlDataReader reader = cmd.ExecuteReader(System.Data.CommandBehavior.CloseConnection)) { reportResult = reader.Cast() .Select(dr =>new { DayOfWeek = dr.GetInt32(0), TableType_Id = dr.GetString(1), Seats = dr.GetInt32(2) } ).ToList(); } }return reportResult; } }

Bonus Round

In case you’re still reading, and not ready to leave, I’ve got one more enhancement. It sure would be nice to display the names of the week. 

Changing the Weekly chart to show days of the week

1. In the displayWeeklyChart, add the following code to the displayWeeklyChart function to create an array for the days of the week, and use the Kendo chart dataBound event to convert the array to the categories of the chart.


function
displayWeeklyChart(beginDate, endDate) {//setup the URI to the report, with the Location Parameter, and the date rangevar reportsAPI = "../reports/SeatedGuests/?period=weekly&id=" +
contentItem.screen.Location.Location_Id +
"&beginDate=" + beginDate.toJSON() +"&endDate=" + endDate.toJSON();var daysOfWeekJSON = [ { "DayOfWeek": "Mon" }, { "DayOfWeek": "Tues" }, { "DayOfWeek": "Wed" }, { "DayOfWeek": "Thur" }, { "DayOfWeek": "Fri" }, { "DayOfWeek": "Sat" }, { "DayOfWeek": "Sun" }]; customersPerWeekChartContainer.kendoChart({ theme: $(document).data("kendoSkin") || "black"
,
dataSource: { }, categoryAxis: { field:
"DayOfWeek"
}, dataBound: dataBound }); function dataBound(e) {var chart = e.sender, categories = chart.options.categoryAxis.categories, weekNumber;for (var i = 0; i < categories.length; i++) { weekNumber = categories[i]; categories[i] = daysOfWeekJSON[weekNumber - 1].DayOfWeek; } }
};

Wrapping it up

Visual Studio LightSwitch enables rapid productivity for the common components of your app. However, all apps are not created equal. With our extensibility points, developers can take their app in the direction they need, including rich visualizations, leveraging aggregate data from the data source to pixels on the screen.

By leveraging WebAPI, the new aggregate LINQ operators within LightSwitch, the ServerApplicationContext and some great chart controls, we can stitch together some great experiences, building on the “out of the box” features.

With some ADO.net code, we can utilize stored procedures to generate the reports, making sure our DBAs are happy leveraging the power of database query processors. Using parameters, we can narrow the results required by the user, allowing them to drill into specific slices of data.

I hope this blog was helpful and please continue to give us great feedback here and in the forum on what works, what’s tripping you up, and what we should do more of so we can keep you and your customers productive.

Steve Lasker

http://blogs.msdn.com/SteveLasker

Program Manager, Visual Studio LightSwitch


Viewing all articles
Browse latest Browse all 10804

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>