Tag

webforms

Browsing

There’s may chart out there, free and very expensive ones. You would have expected slightly cheaper prices on the commercials ones, seeing that representing data visually in a chart, is as old as the list box. Lucky, there is Chart.js, which is open source.

It’s a javascript library, so using it with ASP.NET webforms, need some tricks. Making jQuery calls via AJAX is our medicine.

Something else I found on the internet, there is everyone doing it differently and nobody has a complete solution. To be dynamic, it is not just the data that should be dynamics, but also things like chart type, chart options, width, height, container titles and footers, etc. I worked through the night and quickly put together my take on it. Yea, lots of improvements needed as I just slapped things together to get it to work. Cleanup can come later.

The website I’m working on is using bootstrap, charts should be responsive, and it’s tiled together with other elements using Masonry.

Clientside

This is my compact client-side script:

var uCharts = [56];
        var i; for (i = 0; i < uCharts.length; i++) { CreateCharts(uCharts[i]); TileGrid(); }

        function CreateCharts(id) {
            try {
                var ctx = $('#ucc' + id)[0].getContext("2d");
                return $.ajax({
                    url: "/Services/ChartData.asmx/Load", data: '{UserChartId: ' + id + '}',
                    type: "POST", contentType: "application/json; charset=utf-8", dataType: "json",
                    success: function (res, textStatus) {
                        var d = JSON.parse(res.d[0]); var o = JSON.parse(res.d[1]); var c = res.d[6];
                        var t = res.d[2]; var f = res.d[3]; var w = res.d[4]; var h = res.d[5];
                        $('#ucc' + id).attr('width', w).attr('height', h);
                        var myChart = new Chart(ctx, { type: c, data: d, options: o });
                        $('#uct' + id).text(t); $('#ucf' + id).text(f);
                        TileGrid();
                    },
                    error: function (res, textStatus) { }
                });
            }
            catch (err) { }
            finally { }
        }

        function TileGrid() {
            var $grid = $('.grid').masonry({
                itemSelector: '.grid-item',
                columnWidth: '.grid-sizer',
                percentPosition: true,
                horizontalOrder: false,
            });
        }
  1. First I have an array of ids (uCharts). These are the ids of the charts a logged-in user want to display on his dashboard.
  2. I loop through these ids and call the cart creator function CreatCharts for each of the charts. These charts are inside a masonry titled grid, so I call TileGrid each time after a tile was created.
  3. The jQuery ajax call get back and array. Usually “d” only contains the JSON data, but I have added options, title, footer, width, height and chart type to the string being return so that everthing about the chart is done server-side.
  4. When I call the web service, I also pass the users’ chart id, so that the web service can do its thing server side.

Server Side

The service receives the identifier (user’s chart id in my case), and use this id to get information about this chart from wherever needed. In the code below, I have just hard coded some dummy data, but in real life, I will go and fetch the data and chart options from the database.

using System.Web.Script.Serialization;
using System.Web.Services;

namespace WLMT.Services
{
    [System.Web.Script.Services.ScriptService]
    public class ChartData : System.Web.Services.WebService
    {
        [WebMethod(EnableSession = true)]
        public string[] Load(int UserChartId)
        {
            var data = new
            {
                labels = new[] {
                    "South Western", "Western", "Northern", "North Eastern", "Eastern"
                },
                datasets = new[] {
                    new {
                        label = "2016 - 2017",
                        backgroundColor = new[] { "rgba(220, 220, 220, 0.5)" },
                        pointBorderColor = "#fff",
                        data = new[] {"312","260","350","275","230"}
                    },
                    new {
                        label = "2017 - 2018",
                        backgroundColor = new[] { "#3c8dbc", "#00c0ef", "#00a65a", "#f39c12", "#605ca8" },
                        pointBorderColor = "#fff",
                        data = new[] {"350","280","375","300","260"}
                    }
                }
            };

            var options = new
            {
                responsive = true,
                maintainAspectRatio = true,
                scales = new
                {
                    xAxes = new[]
                    {
                       new {
                           gridLines = new { display = true }
                       }
                    },
                    yAxes = new[]
                    {
                        new {
                            gridLines = new { display = true },
                            scaleLabel = new { display = true, labelString = "Millions" }
                        }
                    }
                }
            };
            string ChartTitle = "ChartJS Responsive & Aspect Ratio Test";
            string ChartFooter = "Data is fetch with a web service from client side";
            string Width = "400";
            string Height = "250";
            string ChartType = "bar";

            string[] result = { data.ToJSON(), options.ToJSON(), ChartTitle, ChartFooter, Width, Height, ChartType };
            return result;
        }
    }
+}

Array[0] and Array[1] values is converted to JSON. The other array values just plain strings. If you go back to the client-side script, you will see that I read all array values from the response (res.d[0] – res.d[6]) and use it where applicable.

My JSONHelper is in another class, see below.

Helper

using System.Web.Script.Serialization;

namespace WLMT
{
    public static class JSONHelper
    {
        public static string ToJSON(this object obj)
        {
            JavaScriptSerializer serializer = new JavaScriptSerializer();
            return serializer.Serialize(obj);
        }

        public static string ToJSON(this object obj, int recursionDepth)
        {
            JavaScriptSerializer serializer = new JavaScriptSerializer();
            serializer.RecursionLimit = recursionDepth;
            return serializer.Serialize(obj);
        }
    }
}

The end result is shown below.

The width and height passed to the chart are only for maintaining the aspect ratio, as Chart.js has “responsive” = true and “maintainAspectRatio” = true set.

In my HTML, I have a bootstrap card, where card title is used for the title, body for the chart canvas etc. These placeholder containers are created by the page, server-side, and so is the javascript array I place on the page. So each placeholder card will be in the HTML when in reach the client browser, from where the client-side script will do its thing to populate the placeholder with its chart.

Stuff can go wrong, as charts are created by users using a web-based wizard. For now, the client-side code is in a try-catch block, so that all client-side stuff does not fall over when something goes wrong here. In the catch, I will be removing the empty bootstrap placeholder card. The issue will be logged to ELMAH, from where action can be taken. A notification will tell the user that the card is not shown due to…

Configuring ASP.NET WebForms, Hangfire and Sqlite storage will work with the following setup:

First, install these packages

<package id="Hangfire.AspNet" version="0.2.0" targetFramework="net472" />
<package id="Hangfire.Core" version="1.7.19" targetFramework="net472" />
<package id="Hangfire.Dashboard.Authorization" version="3.0.0" targetFramework="net472" />
<package id="Hangfire.Storage.SQLite" version="0.2.5" targetFramework="net472" />

The last one is the non-abandoned one with the description of “An Alternative SQLite Storage for Hangfire”
When you install this Hangfire.Storage.SQLite package, it will auto-install these:

<package id="sqlite-net-pcl" version="1.6.292" targetFramework="net472" />
<package id="SQLitePCLRaw.bundle_green" version="1.1.13" targetFramework="net472" />
<package id="SQLitePCLRaw.core" version="1.1.13" targetFramework="net472" />
<package id="SQLitePCLRaw.lib.e_sqlite3.linux" version="1.1.13" targetFramework="net472" />
<package id="SQLitePCLRaw.lib.e_sqlite3.osx" version="1.1.13" targetFramework="net472" />
<package id="SQLitePCLRaw.lib.e_sqlite3.v110_xp" version="1.1.13" targetFramework="net472" />
<package id="SQLitePCLRaw.provider.e_sqlite3.net45" version="1.1.13" targetFramework="net472" />
<package id="Stub.System.Data.SQLite.Core.NetFramework" version="1.0.113.3" targetFramework="net472" />

I guess some of them is not required, but hell, I’ve spent to much time on getting all this to work…

Now, use something like SQLiteStudio (https://sqlitestudio.pl) and create an empty database called Hangfire.db. In my code example, I called it “Jobs.db”)

Now change your startup.cs to look something like this:

using Microsoft.Owin;
using Owin;
using Hangfire;
using System;
using Hangfire.Dashboard;
using System.Web;
using Hangfire.Storage.SQLite;

[assembly: OwinStartupAttribute(typeof(WLMT.Startup))]

namespace WLMT
{
    public partial class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            ConfigureAuth(app);
            string hangfireSqliteDb = String.Format(@"{0}App_Data/Jobs.db", AppDomain.CurrentDomain.BaseDirectory);
            var storeageOptions = new SQLiteStorageOptions();
            storeageOptions.QueuePollInterval = TimeSpan.FromSeconds(30);
            GlobalConfiguration.Configuration.UseSQLiteStorage(hangfireSqliteDb, storeageOptions);
            var backgrounJobServerOption = new BackgroundJobServerOptions { WorkerCount = 1 };
            var dashboardOptions = new DashboardOptions
            {
                Authorization = new[] { new JobDashboardAuthorization() },
                AppPath = VirtualPathUtility.ToAbsolute("~"),
                DashboardTitle = "WLMT",
                DisplayStorageConnectionString = false
            };
            app.UseHangfireDashboard("/jobs", dashboardOptions);
            app.UseHangfireServer(backgrounJobServerOption);
        }
    }
}

You almost there. You will see that I have my own Authorization. This is because the default method did not work in Webforms. Its as if some sequence of something is wrong and I kept getting now page displaying due to Authentication failure. To fix this, create your own IDashboardAuthorizationFilter. Create a new class in your Code / App_Code folder, or wherever…. here is how my class looks like:

using Hangfire.Annotations;
using Hangfire.Dashboard;
using System.Web;

namespace WLMT
{
    public class JobDashboardAuthorization : IDashboardAuthorizationFilter
    {
        public bool Authorize([NotNull] DashboardContext context)
        {
            bool hasAccess;
            try
            {
                hasAccess = HttpContext.Current.User.IsInRole("Admin");
            }
            catch
            {
                hasAccess = false;
            }
            return hasAccess;
        }
    }
}

That’s it!

In my example, once logged in with a user that belongs to the “Admin” role, I can navigate to /jobs (see my path app.UseHangfireDashboard(“/jobs”, dashboardOptions);) and Hangfire GUI display and everything seems to work.

Take note of this line: GlobalConfiguration.Configuration.UseSQLiteStorage(hangfireSqliteDb, storeageOptions);

UseSQLiteStorage(hangfireSqliteDb, storeageOptions); hangfireSqliteDb is the connection string.
Do not try things like File=… or Data Source=… like you do in other connection string. This is NOT A CONNECTION STRING. Its is FILEPATH.

You need to provide a path like “c:\yourdirectory\your.db”.
Because this is serverless and runs within my web application I just defined my path like this:

string hangfireSqliteDb = String.Format(@"{0}App_Data/Jobs.db", AppDomain.CurrentDomain.BaseDirectory);