Tag

asp.net

Browsing

Sometimes, your User Control needs to get some unique identifier from the page it is hosted in.
Let’s assume you have a side menu that is built server-side, and all the fiddling with friendly URL’s and routes is just not enough of a constant for you to set the active item in the menu.

On the page, declare a variable. In the case below, I call it PageId.
This is because my side menu is built from the database and table.Menu.PageId and table.Page.Id is in a relationship.
I guess you would call this a database first approach because every page in my system is actually listed in the table.Pages and from there you have table.Menu, table.RolePages, table.UserRoles etc, all interrelated.
I therefore set PageId = “5”, because in table.Pages.Id its the primary key. (Of course, if you do not want to ship the DB with your app, you can later use reflection and let the page register itself inside blank-fresh database)

namespace MyAppName.Pages
{
    public partial class MyPageName : System.Web.UI.Page
    {
        public string PageId { get { return "5"; } }

        protected void Page_Load(object sender, EventArgs e)
        {
        }
    }
}

Each participating page in your application now has a unique value for PageId. Over to the ASCX user control.

  1. See the variable at the top called ParentPageId, it’s storing in the ViewState so that I do not lose it over postback etc.
  2. Then in the Page Load event, only executing for !IsPosback, I set the ParentPageId. See SetParentPageId().
  3. Later, in the logic of building the menu, I then compare the PageId coming from the database, with ParentPageId, and can make the list item active.
    if (pageid == ParentPageId) { li.Attributes.Add(“class”, “active”); }
  4. After Note: return ViewState[“ParentPageId”] != null ? (string)ViewState[“ParentPageId”] : “0”;    –> do NOT make this return ViewState[“ParentPageId”] != null ? (string)ViewState[“ParentPageId”] : “”;
    Do not pass back an Empty string, as this will try and enable items that do not have PageIds (some of your pages may not have the PageId set, and in that case, pass some value that you know does not exist, like “0” or “-999”.
using System;
using System.Configuration;
using System.Data;
using System.Data.SQLite;
using System.Linq;
using System.Web.UI;
using System.Web.UI.HtmlControls;

namespace MyAppName.Controls
{
    public partial class Sidebar : System.Web.UI.UserControl
    {
        private string ParentPageId
        {
            get
            {
                return ViewState["ParentPageId"] != null ? (string)ViewState["ParentPageId"] : "0";
            }
            set
            {
                ViewState["ParentPageId"] = value;
            }
        }

        protected void Page_Load(object sender, EventArgs e)
        {
            if (!IsPostBack)
            {
                SetParentPageId();
                BuildSidebarMenu();
            }
        }

        protected void SetParentPageId()
        {
            Type t = Page.GetType();
            System.Reflection.PropertyInfo[] pi = t.GetProperties();
            foreach (System.Reflection.PropertyInfo pinfo in pi)
            {
                if (pinfo.Name == "PageId")
                {
                    ParentPageId = (string)pinfo.GetValue(Page, null);
                    break;
                }
            }
        }

        protected void BuildSidebarMenu()
        {
            // My code to build the sidebar
        }

        protected DataTable GetSidebarMenuTable()
        {
            string sql = @"SELECT ....; ";
            DataSet ds = new DataSet();
            // My code to get the data

            DataTable dt = ds.Tables[0];
            return dt;
        }

        protected HtmlGenericControl ListItem(string id, string menutitle, string menuicon, string page, string pageid, DataTable dt)
        {
            DataRow[] matchingchildrenrows = dt.Select("MenuParentId = " + id);
            HtmlGenericControl li = new HtmlGenericControl("li");
            if (!String.IsNullOrEmpty(pageid))
            {
                if (!String.IsNullOrEmpty(ParentPageId))
                {
                    if (pageid == ParentPageId) { li.Attributes.Add("class", "active"); }
                }
            }
            // The rest of my code here to set classes, create hyperlink-, text-, icon- elements, etc.
            // ...
            // ...
            return li;
        }

    }
}

The same approach can then used form page permissions.

Pages got Ids and your relationship between the logged in UserId, RoleId, PageRoles, etc. then being used to control page access, all pivoting around a simple ID.

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);