0 Comments

Almost web developers remembered that ASP.NET support chart controls since ASP.NET 3.5 as separate package that you can download it easily, after awhile they become built-in in ASP.NET 4, for more details you can refer to ScottGu's blog post here.

In this blog post I'll explain how can we develop some of the common charting controls that our applications & websites many need with the help of the tag helpers and morris.js.

Morris.js is an open source javascript library that powers the graphs. It has a very simple APIs for drawing line, bar, area and donut charts.

1. Line Charts

2. Area Charts

3. Bar Charts

4. Donut Charts


For more information about morris.js, please visit this link.

Now let us dig into chart controls and how I Create them. First of all we need to define the chart types using the enumeration

public enum ChartType
{
Area,
Bar,
Donut,
Line
}

After that we 'll create ChartTagHelper that calling morris.js APIs behind the scene.

public class ChartTagHelper: TagHelper
{
private IList<PropertyInfo> Properties => Source.First().GetType().GetProperties().ToList();

private IList<string> PropertyNames => Properties.Select(p => p.Name).ToList();

[HtmlAttributeName("id")]
public string Id { get; set; }

[HtmlAttributeName("type")]
public ChartType Type { get; set; }

[HtmlAttributeName("labels")]
public string Labels { get; set; }

[HtmlAttributeName("source")]
public IEnumerable<object> Source { get; set; }

public override void Process(TagHelperContext context, TagHelperOutput output)
{
output.Attributes.SetAttribute("id", Id);
output.TagName = "div";

if (Source == null)
{
return;
}

if (Type == ChartType.Donut)
{
output.PostElement.AppendHtml($@"
<script>
Morris.{Type}({{
element: '{Id}',
data: [
{ConstructDonutChartData()}
]
}});
</script>");
}
else
{
var xKey = PropertyNames.First();
var yKeys = PropertyNames.Skip(1).ToList();

output.PostElement.AppendHtml($@"
<script>
Morris.{Type}({{
element: '{Id}',
data: [
{ConstructChartData()}
],
xkey: '{xKey}',
ykeys: [{String.Join(",",yKeys.Select(p=> "'" + p + "'"))}],
labels: [{String.Join(",",Labels.Split(',').Select(l => "'" + l + "'"))}]
}});
</script>");
}
}

private string ConstructDonutChartData()
{
var labelProperty = Source.First().GetType().GetProperty("label");
var valueProperty = Source.First().GetType().GetProperty("value");

return String.Join(",", Source.Select(s =>
$"{{label: '{labelProperty.GetValue(s)}', value: {valueProperty.GetValue(s)}}}"));
}

private string ConstructChartData()
{
var xKey = PropertyNames.First();
var xProperty = Properties.First();
var yProperties = Properties.Skip(1);

return String.Join(",",
Source.Select(s => $"{{{xKey}:'{xProperty.GetValue(s)}', " + String.Join(",", yProperties.Select(p => p.Name + ":" + p.GetValue(s))) + "}"
));
}
}

The code above is simply constructs the script that morris.js needs to call its APIs, the code using reflection to get the array properties and their values, I know it needs a lot of improvement, which i 'll try to do if I get a time :)

Last thing we can use our tag helper like this:

@{
var contribution = new []
{
new { year = "2016-1", i = 100, pr = 34, c = 12},
new { year = "2016-2", i = 75, pr = 11, c = 3},
new { year = "2016-3", i = 50, pr = 99, c = 7},
new { year = "2016-4", i = 75, pr = 56, c = 1},
new { year = "2016-5", i = 24, pr = 0, c = 54},
new { year = "2016-6", i = 75, pr = 31, c = 3},
new { year = "2016-7", i = 100, pr = 77, c = 44}
};
}

<chart id="bar-example" type="Bar" labels="Issues,Pull Requests,Comments" source="@contribution"></chart>

The same thing for other chart types except you need to change the ChartType property.

You can download the source code for this blog post from my ChartControls repository on GitHub.

0 Comments
Minification is the process of removing unnecessary characters from the source code without changing its functionality.

In this post I will show you how we can optimize the JavaScript & CSS files using a custom minifiers tag helpers.

Now let us start with the CssMinifier which is nothing but a utility class that using online service https://cssminifier.com to minimize the CSS code an remove unnecessary characters.

CSS Minifier


using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading.Tasks;

namespace HelloMvc.TagHelpers
{
    public static class CssMinifier
{
    private const string URL_CSS_MINIFIER       = "https://cssminifier.com/raw";
    private const string POST_PAREMETER_NAME    = "input";

    public static async Task<String> MinifyCss(string inputCss)
    {
        List<KeyValuePair<String, String>> contentData = new List<KeyValuePair<String, String>>
        {
            new KeyValuePair<String, String>(POST_PAREMETER_NAME, inputCss)
        };

        using (HttpClient httpClient = new HttpClient())
        {
            using (FormUrlEncodedContent content = new FormUrlEncodedContent(contentData))
            {
                using (HttpResponseMessage response = await httpClient.PostAsync(URL_CSS_MINIFIER, content))
                {
                    response.EnsureSuccessStatusCode();
                    return await response.Content.ReadAsStringAsync();
                }
            }
        }
    }
}

}
Similarly JavaScriptMinifier which did the exact thing for the JavaScript content using https://javascript-minifier.com.

JavaScript Minifier


using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading.Tasks;

namespace HelloMvc.TagHelpers
{
    public static class JavaScriptMinifier
{
    private const string URL_JS_MINIFIER       = "https://javascript-minifier.com/raw";
    private const string POST_PAREMETER_NAME    = "input";

    public static async Task<String> MinifyJs(string inputJs)
    {
        List<KeyValuePair<String, String>> contentData = new List<KeyValuePair<String, String>>
        {
            new KeyValuePair<String, String>(POST_PAREMETER_NAME, inputJs)
        };

        using (HttpClient httpClient = new HttpClient())
        {
            using (FormUrlEncodedContent content = new FormUrlEncodedContent(contentData))
            {
                using (HttpResponseMessage response = await httpClient.PostAsync(URL_JS_MINIFIER, content))
                {
                    response.EnsureSuccessStatusCode();
                    return await response.Content.ReadAsStringAsync();
                }
            }
        }
    }
}
}
After we have the seen the core minification process, I will show you the code of both CssMinifierTagHelper and JavaScriptTagHelper that allow us to minifiy both inline styles and scripts as well as external files if it needs.

CSS Minifier TagHelper


using System.IO;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Razor.TagHelpers;
using Microsoft.Extensions.FileProviders;

namespace HelloMvc.TagHelpers
{
    [HtmlTargetElement("style")]
    [HtmlTargetElement("link", Attributes = MinifyAttributeName)]
    public class CssMinifierTagHelper: TagHelper
    {
        private const string MinifyAttributeName = "minify";
        private readonly IFileProvider _wwwroot;
        private readonly string _wwwrootFolder;
       
        public CssMinifierTagHelper(IHostingEnvironment env)        
        {
            _wwwroot = env.WebRootFileProvider;
            _wwwrootFolder = env.WebRootPath;
        }
       
        [HtmlAttributeName("rel")]
        public string Rel { get; set; }
       
        [HtmlAttributeName("href")]
        public string Href { get; set; }
       
        [HtmlAttributeName(MinifyAttributeName)]
        public bool? Minify {get; set;}
       
        public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
        {
            if (output.TagName == "style")
            {
                var content = output.GetChildContentAsync().Result.GetContent();
                var result = await CssMinifier.MinifyCss(content);
           
                output.Content.SetContent(result);
            }
           
            if (Rel == null || Href == null)
            {
                return;
            }

            if (output.TagName == "link" && Rel == "stylesheet")
            {
                if (!string.IsNullOrEmpty(Href))
                {
                    if(Minify.HasValue && !Minify.Value)
                    {
                        return;
                    }
                   
                    var fileInfo = _wwwroot.GetFileInfo(Href);
                    var cssDirectory = Href.Substring(0,Href.IndexOf(fileInfo.Name)-1);
                    var minFileName = fileInfo.Name.Replace(".css", ".min.css");
                    var minFilePath = Path.Combine(_wwwrootFolder, cssDirectory, minFileName);
                   
                    if (Rel != null)
                    {
                        output.Attributes.SetAttribute("rel", "stylesheet");
                    }
                   
                    if (File.Exists(minFilePath))
                    {
                        if (Href != null)
                        {
                            output.Attributes.SetAttribute("href", Href.Replace(".css", ".min.css"));
                        }
                       
                        return;
                    }
                   
                    using (var readStream = fileInfo.CreateReadStream())
                    using (var reader = new StreamReader(readStream, Encoding.UTF8))
                    {
                        var content = await CssMinifier.MinifyCss(await reader.ReadToEndAsync());
                       
                        using(var writer = new StreamWriter(File.Create(minFilePath), Encoding.UTF8))
                        {
                            await writer.WriteAsync(content);
                        }
                    }
                   
                    if (Href != null)
                    {
                        output.Attributes.SetAttribute("href", Href.Replace(".css", ".min.css"));
                    }
                }
            }
        }
    }
}

JavaScript Minifier TagHelper


using System.IO;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Razor.TagHelpers;
using Microsoft.Extensions.FileProviders;

namespace HelloMvc.TagHelpers
{
    [HtmlTargetElement("script")]
    public class JavaScriptMinifierTagHelper: TagHelper
    {
        private const string MinifyAttributeName = "minify";
        private readonly IFileProvider _wwwroot;
        private readonly string _wwwrootFolder;
       
        public JavaScriptMinifierTagHelper(IHostingEnvironment env)        
        {
            _wwwroot = env.WebRootFileProvider;
            _wwwrootFolder = env.WebRootPath;
        }

        [HtmlAttributeName("src")]
        public string Src { get; set; }
       
        [HtmlAttributeName(MinifyAttributeName)]
        public bool? Minify {get; set;}
       
        public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
        {        
            if (output.TagName == "script")
            {
                if (Src == null)
                {
                    var content = output.GetChildContentAsync().Result.GetContent();
                    var result = await JavaScriptMinifier.MinifyJs(content);
           
                    output.Content.SetContent(result);
                }
                else
                {
                    if(!Minify.HasValue)
                    {
                        if (Src != null)
                        {
                            output.Attributes.SetAttribute("src", Src);
                        }
                       
                        return;
                    }
                   
                    var fileInfo = _wwwroot.GetFileInfo(Src);
                    var jsDirectory = Src.Substring(0,Src.IndexOf(fileInfo.Name)-1);
                    var minFileName = fileInfo.Name.Insert(fileInfo.Name.Length-3,".min");
                    var minFilePath = Path.Combine(_wwwrootFolder, jsDirectory, minFileName);
                    
                    if (File.Exists(minFilePath))
                    {
                        if (Src != null)
                        {
                            output.Attributes.SetAttribute("src", Src.Replace(".js", ".min.js"));
                        }
                       
                        return;
                    }
                   
                    using (var readStream = fileInfo.CreateReadStream())
                    using (var reader = new StreamReader(readStream, Encoding.UTF8))
                    {
                        var content = await JavaScriptMinifier.MinifyJs(await reader.ReadToEndAsync());
                       
                        using(var writer = new StreamWriter(File.Create(minFilePath), Encoding.UTF8))
                        {
                            await writer.WriteAsync(content);
                        }
                    }
                   
                    if (Src != null)
                    {
                        output.Attributes.SetAttribute("src", Src.Replace(".js", ".min.js"));
                    }
                }
            }
        }
    }
}
For sake of the demo I didn't implement kind of watchers to monitor if some of the external styles or scripts change to minify them again, but this is totally possibly with the newly File System APIs in ASP.NET Core.

Finally I hope this post give you some of thought to how to start with your own custom tag helper.

Hint: The entire source code is hosted in Github

0 Comments

Previously I talked about localization in much details, today I will look to it from different angle. While the multilingual websites is common nowadays, there should be a way to let the user to switch between the languages to see the content in the language that he/she prefers.

Let's build a customizable language switcher with the help of tag helper and bootstrap.

In this article I will not dig in much details about tag helpers and bootstrap, so for those who don't know what the tag helper is, please read this link.

1. Preparing the assets

Our tag helper needs a set of icons to show the flags of the supported languages, there are some open source projects that provide a rich SVG country flags such as flag-icon-css but for simplicity I used some icons from this link. Of course we need bootstrap too, so you can get it from this link.

2. Configuring the dependencies

The main two dependencies that we need in our tag helper are  "Microsoft.AspNetCore.Mvc.TagHelpers"
"Microsoft.AspNetCore.Mvc.Localization"

3. Authoring LanguageSwitcherTagHelper

Now let's see some code

public enum DisplayMode
{
Image = 0,
ImageAndText = 1,
Text = 2
}

our tag helper contains three display modes:

  1. Image: which is display the flag of the supported languages
  2. ImageAndText: which is display the name and the flag of the supported languages
  3. Text: which is display the name of the supported languages
[HtmlTargetElement("language-switcher")]
public class LanguageSwitcherTagHelper : TagHelper
{
private IOptions<RequestLocalizationOptions> _locOptions;

public LanguageSwitcherTagHelper(IOptions<RequestLocalizationOptions> options)
{
_locOptions = options;
}

[ViewContext, HtmlAttributeNotBound]
public ViewContext ViewContext { get; set; }

public DisplayMode Mode { get; set; } = DisplayMode.ImageAndText;

public override void Process(TagHelperContext context, TagHelperOutput output)
{
var selectedCulture = ViewContext.HttpContext.Features.Get<IRequestCultureFeature>().RequestCulture.Culture;
var cultures = _locOptions.Value.SupportedUICultures;

output.TagName = null;

switch (Mode)
{
case DisplayMode.ImageAndText:
output.Content.AppendHtml(@"<ul class='nav navbar-nav navbar-right'>
<li class='dropdown'>
<a href='#' class='dropdown-toggle' data-toggle='dropdown' role='button' aria-haspopup='true' aria-expanded='false'><img src='/images/" + selectedCulture.TwoLetterISOLanguageName + ".png' /> " + selectedCulture.EnglishName + @"<span class='caret'></span></a>
<ul class='dropdown-menu'>");
foreach (var culture in cultures)
{
output.Content.AppendHtml($"<li><a href='#' onclick=\"useCookie('{culture.TwoLetterISOLanguageName}')\"><img src='/images/{culture.TwoLetterISOLanguageName}.png' /> {culture.EnglishName}</a></li>");
}
break;
case DisplayMode.Image:
output.Content.AppendHtml(@"<ul class='nav navbar-nav navbar-right'>
<li class='dropdown'>
<a href='#' class='dropdown-toggle' data-toggle='dropdown' role='button' aria-haspopup='true' aria-expanded='false'><img src='/images/" + selectedCulture.TwoLetterISOLanguageName + @".png' /> <span class='caret'></span></a>
<ul class='dropdown-menu'>");
foreach (var culture in cultures)
{
output.Content.AppendHtml($"<li><a href='#' onclick=\"useCookie('{culture.TwoLetterISOLanguageName}')\"><img src='/images/{culture.TwoLetterISOLanguageName}.png' /></a></li>");
}
break;
case DisplayMode.Text:
output.Content.AppendHtml(@"<ul class='nav navbar-nav navbar-right'>
<li class='dropdown'>
<a href='#' class='dropdown-toggle' data-toggle='dropdown' role='button' aria-haspopup='true' aria-expanded='false'> " + selectedCulture.EnglishName + @"<span class='caret'></span></a>
<ul class='dropdown-menu'>");
foreach (var culture in cultures)
{
output.Content.AppendHtml($"<li><a href='#' onclick=\"useCookie('{culture.TwoLetterISOLanguageName}')\">{culture.EnglishName}</a></li>");
}
break;
}
output.Content.AppendHtml(@"</ul>
</li>
</ul>");

output.Content.AppendHtml(@"<script type='text/javascript'>
function useCookie(code) {{
var culture = code;
var uiCulture = code;
var cookieValue = '" + CookieRequestCultureProvider.DefaultCookieName + @"=c='+code+'|uic='+code;
document.cookie = cookieValue;
window.location.reload();
}}
</script>");
}
}

the LanguageSwitcherTagHelper is simply render a bootstrap dropdown menu that contains the supported languages based on the DisplayMode property.

after that you can use our tag helper in the view as the following:

<language-switcher></language-switcher>

the final result will look like this

Hint: The entire source code is hosted in Github.