Many of you know that with the installation of our products, we also install a rich variety of demo applications demonstrating the countless cool features in our controls.
Those demos are installed in the “%PUBLIC%\Public Documents\DevExpress Demos 14.1” folder.
Background
What you might not know is that all of our demo applications are designed and developed as if they are real customer projects with various design patterns and best practices in mind.
We have several developers working on those demos for weeks and the demos are being tested thoroughly by our test team.
Since these patterns and best practices could also be useful for you projects as well, I have decided to shine some light under the hood of the SalesViewer demo application.
Data storage
We are using a MS-SQL Server database to feed the application. This database file is ~/AppData/Sales.mdf. In the context of this application, we are only reading data and the database is designed for the purpose of this demo.
Its diagram is displayed below:
Data Access
The Data Transfer Object (DTO) pattern is used to retrieve only the necessary fields from the tables by queries. The performance improvement is considerable compared to a full select.
For example, the DataContext.Product table contains 13 fields and returns values for all fields. The product DTO class is used and the query only returns necessary field values which are 7 in this case.
public class ProductsProvider : BaseProvider<DataContext.Product> { public IEnumerable<Product> GetList() { return TryGetResult<IEnumerable<Product>>(() => { return (from p in DataTable select new Product { Id = p.Id, Name = p.Name, BaseCost = p.BaseCost, Description = p.Description, ListPrice = p.ListPrice, UnitsInInventory = p.UnitsInInventory, UnitsInManufacturing = p.UnitsInManufacturing }).OrderBy(p => p.Name).ToList(); //... } public class Product { public int Id { get; set; } public string Name { get; set; } public string Description { get; set; } public double BaseCost { get; set; } public double ListPrice { get; set; } public int UnitsInInventory { get; set; } public int UnitsInManufacturing { get; set; } }
//... using(ProductsProvider provider = new ProductsProvider()) { ProductsGridView.DataSource = provider.GetList(); ProductsGridView.DataBind(); //...
Caching data
Some values used in calculations and/or selection criteria like the dates of Today / Yesterday / Last week are static for a day, so we stored them in the HttpContext.Current.Cache where they expire after a day.
In this application we also put some smaller datasets, which don’t frequently change, completely in cache like Customer, Product, Location, Contact and Plant.
You will notice considerable performance increase by using objects stored in cache but in your scenario this might not always be possible.
Below is the SalesProvider including caching:
public class SalesProvider : BaseProvider<DataContext.Sale> { //... public double GetSalesRevenue(DateTime minDate, DateTime maxDate) { return TryGetResult<double>(() => { return DataTable.Where(s => s.SaleDate >= minDate && s.SaleDate <= maxDate) .Sum(s => s.TotalCost); }, useCache: true, keySuffix: string.Format("{0}.{1}", minDate, maxDate)); } // Note: keySuffix is suffix for cache key. // The cache key should be unique and based on table name and keySuffix. //... }
Optimizing performance for large datasets
The Sales page of this demo shows sale totals using an ASPxPivotGrid. The table which is used to feed the Grid contains about 260,000 records. To improve the performance of this page we simulate an ‘OLAP cube’ by aggregating the data on the SQL Server which will return the calculated sums.
By using this approach instead of just feed all 260,000 records to the ASPxPivotGrid, we’re able to speed up this operation from around 7 seconds to 0.7 seconds.
The code used to feed the ASPxPivotGrid and let the aggregates be calcultated by SQL Server looks like this:
public IEnumerable<Sale> GetList(DateTime minDate, DateTime maxDate) { return TryGetResult<IEnumerable<Sale>>(() => { return (from s in DataTable where s.SaleDate >= minDate && s.SaleDate <= maxDate group s by new { Year = s.SaleDate.Year, Month = s.SaleDate.Month, ProductName = s.Product.Name } into saleGroup select new { Year = saleGroup.Key.Year, Month = saleGroup.Key.Month, ProductName = saleGroup.Key.ProductName, TotalCost = saleGroup.Sum(x => x.TotalCost) }) .ToList() .Select(s => new Sale() { ProductName = s.ProductName, SaleDate = new DateTime(s.Year, s.Month, 1), TotalCost = s.TotalCost }).ToList(); }); }
More information on this can be found in our Code Examples here.
Making use of the AutoSeries
Because we use the Data Transfer Object (DTO) pattern, it is possible to return data precisely in a way that we can use the AutoSeries feature in all charts of this demo. This approach reduces the amount of code and makes data binding easy. The charts controls are able to automatically create series based on data.
What we have done is the following:
Create a list of ChartDataBase items where every item has a SeriesName property e.g. “Today” series has 6 items with SeriesName = “Today”. Now the Chart Control is able to build the series automatically:
<dxchartsui:WebChartControl ID="VerticalChartControl" ClientIDMode="AutoID" SeriesDataMember="SeriesName"… >
(The SeriesDataMember is assigned to ChartDataBase.SeriesName)
More information about AutoSeries and how to set it up can be found here.
Global site setup, CSS and web-design
The site uses a masterpage called ~/SiteBase.master and a couple of content pages.
The masterpage includes a ~/Content/Css/Demo.css for the positioning and styling of the site. It also has 5 placeholders so the content pages can load the functionality in there.
If you take a close look in your browser to the SalesViewer demo, you might notice that the footer of the pages always stays at the bottom of the page. When the content of a page is smaller as the browser window’s height, the footer will “stick” to the bottom of the browser window. (Check: https://demos.devexpress.com/RWA/SalesViewer/Sales.aspx and resize the browser in height)
This can be accomplished by using a couple of css tricks:
If you look at the markup in the masterpage, there is the following html:
<body><form id="form1" runat="server"><div class="contentHolder">...</div><div class="footerHolder">...</div>
If we apply the following css, the footerHolder will stick to the bottom even if the page length is shorter as the browser width:
html, body { height: 100%; } form { position: relative; min-height: 100%; } .footerHolder { position: absolute; width: 100%; bottom: 0; height: 80px; } .contentHolder { /* should equal the height property of the .footerHolder class*/ padding-bottom: 80px; }
In our case the footer has a height of 80px, but if you need a bigger footer you will need to change it on 2 places; the height of the footer and the padding-bottom of the contentHolder.
In the code-behind of the masterpage there is one line of code worth mentioning; in the Page_Init method, we call the:
protected void Page_Init(object sender, EventArgs e) { ASPxWebControl.SetIECompatibilityModeEdge(this); }
This call enables better touch support (especially for the MS Surface Tablets) which is better described in our knowledge base here.
Use your own hierarchy for pages, user controls and optional master page(s)
If you take a look in the code-behind files of the content pages, you will see that they are not derived from the System.Web.UI.Page but from the BasePage:
public partial class Products : BasePage { protected void Page_Load(object sender, EventArgs e) { //...
This is also the case for the user-controls in the ~/UserControls folder:
public partial class ProductDetails : UserControlBase { private string CityInfoFormatString = "{0}, {1} {2}"; public void LoadContent(int productId) { //...
For the user-controls, we have setup even a small hierarchy with a couple of base classes which derive from each other to support different functionality.
This allows you to optimize and reduce (duplicate) code and makes maintenance and bug-fixing easier.
Styling and coloring the charts
In the SalesViewer demo we have adjusted the colors for the charts to match the design of the site.
This can be done by using the Chart Designer to create and save a charts palette in a file (.xcp).
Such a palette file can be applied to all web chart controls by using the following code:
// Helpers.cs private const string PalettePath = "~/Content/SalesViewerPalette.xcp"; private static Palette GetCommonPalette() { using(FileStream stream = new FileStream(HttpContext.Current.Server.MapPath(PalettePath), FileMode.Open, FileAccess.Read)) return DevExpress.XtraCharts.Native.PaletteSerializer.LoadFromStream(stream); } // ... public static void LoadCommonPalette(WebChartControl control) { control.PaletteWrappers.Add(new PaletteWrapper(CommonPallete)); control.PaletteName = CommonPallete.Name; }
Another interesting trick in the WebChartControl we are using in the ~/RevenueBySector.aspx page is that the Yesterday bar is semi-transparent.
This can be achieved by setting the alpha value in Color:
<dxcharts:SideBySideBarSeriesView Color="128, 219, 219, 219"><border visibility="False" /><fillstyle fillmode="Solid"></fillstyle></dxcharts:SideBySideBarSeriesView>
Using the Bing Map
On the ~/Customers.aspx page, we have a map displaying where the selected customer is located.
We have gotten quite some inquiries from customers about generating an ASP.NET Map control like we have for some of our other product lines.
In a web-page it is actually surprisingly simple to show up the Bing map.
One thing to keep in mind is that version 6 of the Bing map causes some issues in Firefox so we are forcing to use version 7 which doesn’t appear to have this issue.
In the aspx (or ascx) we specify the following markup:
<div id="mapHolder"></div><script type="text/javascript" id="dxss_map"> CreateMap('<%= Location.Latitude %>', '<%= Location.Longitude %>');</script>
Do notice the id of the script block; because it starts with “dxss_” this makes sure this JavaScript is executed after a callback of any of the parent (DevExpress) controls.
The JavaScript function CreateMap() looks like:
function CreateMap(lat, long) { var mapOptions = { credentials: "-- your credentials here --", center: new Microsoft.Maps.Location(lat, long), mapTypeId: Microsoft.Maps.MapTypeId.road, zoom: 9, showScalebar: true, showMapTypeSelector: true, disableKeyboardInput: true } map = new Microsoft.Maps.Map(document.getElementById('mapHolder'), mapOptions); var center = map.getCenter(); var pin = new Microsoft.Maps.Pushpin(center); map.entities.push(pin); CustomersGridView.Focus(); }
Because we want to enable keyboard navigation in the Customer ASPxGridView, we have disabled the keyboard input in the map control.
If you want to use this JavaScript function yourself, make sure you have credentials from Bing.
Callback optimization
Now we have touched some JavaScript with implementing the Bing Map, there is another portion of JavaScript which is quite interesting.
This demo is heavily making use of callbacks to update only certain portions of the page e.g. when changing the focused row in an ASPxGridView.
We also have taken care of good keyboard navigation in this demo particularly in the Grids.
The combination of keyboard support and callbacks while changing focused rows in a grid is often resulting in a huge amount of callbacks where in most circumstances only the last callback will be used.
An example of this is when a user wants to see the results of the fifth record under the current focused row. With the mouse he/she will click on the exact record while with the keyboard, he/she needs to press the down arrow 5 times (equals 5 callbacks).
What we have used for this can be found in the ~/Scripts/Helper.js file and is called the CallbackHelper:
var CallbackHelper = { CallbackControlQueue: [], CurrentCallbackControl: null, UpdateContent: function (callbackControl, args, sender) { if (!this.CurrentCallbackControl) { this.CurrentCallbackControl = callbackControl; callbackControl.EndCallback.RemoveHandler(this.OnEndCallback); callbackControl.EndCallback.AddHandler(this.OnEndCallback); callbackControl.PerformCallback(args); } else this.PlaceInQueue(callbackControl, args, this.GetSenderId(sender)); }, GetSenderId: function (senderObject) { if (senderObject.constructor === String) return senderObject; return senderObject.name || senderObject.id; }, PlaceInQueue: function (callbackControl, args, sender) { var queue = this.CallbackControlQueue; for (var i = 0; i < queue.length; i++) { if (queue[i].control == callbackControl && queue[i].sender == sender) { queue[i].args = args; return; } } queue.push({ control: callbackControl, args: args, sender: sender }); }, OnEndCallback: function (sender) { sender.EndCallback.RemoveHandler(CallbackHelper.OnEndCallback); CallbackHelper.CurrentCallbackControl = null; var queuedPanel = CallbackHelper.CallbackControlQueue.shift(); if (queuedPanel) CallbackHelper.UpdateContent(queuedPanel.control, queuedPanel.args, queuedPanel.sender); } };
What happens here is that we create a queue with controls + callbacks which should be processed. Whenever a callback is added to the queue, we will check if this control already exists in the queue and if so, we will replace the callback. If the control is not found we’ll push this control + callback at the end of the queue.
The queue will be processed whenever any of the controls in the queue raises the EndCallback JavaScript event.
NOTE: We actually find the callback helper such a useful feature that we are implementing this as out of the box functionality for the upcoming major release
The SparkLine control
There is one control visible on every page which is the Sparkline control right above the footer of the page:
This is an interesting control since as you might have noticed, it is not straight out of the DevExpress toolbox.
The thing is that you’re able to slide the beginning and end knobs of the range, which causes the charts and other controls on the page to change.
For this we have used an ASPxTrackBar because it exactly behaves as we want.
The SparkLine above the ASPxTrackBar is not a WebChartControl control but an ASPxImage instead. The ImageUrl will be set in the Code-Behind and is a runtime generated image.
The ~/Chart.aspx contains the WebChartControl to create the chart but instead of rendering html, this page is outputting binary content which is an image containing the SparkLine.
The reason for this construction is (obviously) performance.
If we should have used the chart control instead of the image, this chart is calculated on every request (also callbacks) which will take time. This chart will not change during the day so it is a waste of CPU time to recalculate the values on every request.
The beauty of this approach is that we can cache the image (which is created in the ~/Chart.aspx) and serve it out of the cache for the duration of this day (which is 86400 milliseconds).
It will be cached by specifying the OutputCache directive in the ~/Chart.aspx:
<%@ OutputCache Duration="86400" VaryByParam="start;end;" %>
Conclusion
As mentioned in the introduction of this post; we have built all of our demos as if they are production projects and there are lots of interesting bits and pieces which can be found when going through the demos.
I hope you picked up a couple of tricks in the SalesViewer demo and do let me know if you did.
If you find this interesting material, let me know as well, and we might put some other demo projects in the spotlight as well.