Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web

BootBrander a bootstrap .less Generator UI (Part 4 / Generate the custom-bootstrap.css )

0.00/5 (No votes)
3 Mar 2015CPOL4 min read 16.4K   125  
Creates an MVC site with user inputs to change the bootstrap variables and generate a custom branded bootstrap.css

Introduction

We're at part four of our bootstrap color picker UI. In this series, we will provide the user with a way to download the CSS file they created.

We will be utilizing several techniques, which would be useful on their own as well. The first thing we are going to use is the Dotless library. To transform our less file.

If you've followed the previous series, you will have to get your head around the fact that we are switching to .NET C# now.

Article Index

DotlessClientOnly nuGet

I've installed the DotlessClientOnly nuGet package. This is important! Do not install the full package, because it will transform your .less files at every request blocking our UI from parsing it.

A FileContentResult Action

In the home controller, we will add an action GetCss. And we will make it of the type FileContentResult. The home controller is at the path "Controllers/HomeController.cs".

C#
public FileContentResult GetCss()
{
    var contentType = "text/css";

    var content = "body { background-color : #fff; }";
    var bytes = Encoding.UTF8.GetBytes(content);

    var result = new FileContentResult(bytes, contentType);

    return result;
}

If we run the site and go to the path Home/GetCss, we should get the following output:

CSS
body { background-color : #fff; }

Parsing our bootstrap.less File

Now at first, we are going to ignore the variables the user has created through the UI. We're going to be happy if it parses the bootstrap.less file as it is default.

So let's utilize the Dotless classes:

C#
public FileContentResult GetCss()
{
    //Set the content type
    var contentType = "text/css";
    //Get the bootstrap.less contents
    var bootstrapLessContent = System.IO.File.ReadAllText(
            HostingEnvironment.MapPath("~/Content/bootstrap/bootstrap.less")
        );

    //Create a Dotless Configuration
    var config = new DotlessConfiguration();

    config.MinifyOutput = false;
    config.ImportAllFilesAsLess = true;
    config.CacheEnabled = false;

    //Parse the .less file
    var content = Less.Parse(
        bootstrapLessContent
        , config);

    var bytes = Encoding.UTF8.GetBytes(content);
    var result = new FileContentResult(bytes, contentType);

    return result;
}

Testing

If you've resolved all your references right, this should compile. But if we run, it will throw an exception:

Image 1

This is caused because inside bootstrap.less, the imports to the other files are set without a path. So the Dotless classes do not know how to find them.

IFileReader

Luckily, the dotless library provides us with a solution. We get to decide how it actually finds the files. This is done by implementing the IFileReader interface. And this is great, because we can later utilize this to swap our own variables in place.

But first, we will try to just output the standard .css.

We need to create a class. I did this in subfolder classes and I named it VirtualFileReader. The class should look like this:

C#
internal sealed class VirtualFileReader: IFileReader
 {
     public byte[] GetBinaryFileContents(string fileName)
     {
         fileName = GetFullPath(fileName);
         return File.ReadAllBytes(fileName);
     }

     public string GetFileContents(string fileName)
     {
         fileName = GetFullPath(fileName);
         return File.ReadAllText(fileName);
     }

     public bool DoesFileExist(string fileName)
     {
         fileName = GetFullPath(fileName);
         return File.Exists(fileName);
     }

     private static string GetFullPath(string path)
     {
         return HostingEnvironment.MapPath("~/Content/bootstrap/" + path);
     }

     public bool UseCacheDependencies
     {
         get { return true; }
     }
 }

Next, we need the DotlessConfiguration to use this class. So in our GetCss action, add this line to the config.

C#
config.LessSource = typeof(VirtualFileReader);

Testing

Run the site and go to the path:

/home/getcss

We should now get a compiled CSS file:

CSS
/*! normalize.css v3.0.2 | MIT License | git.io/normalize */

html {
  font-family: sans-serif;
  -ms-text-size-adjust: 100%;
  -webkit-text-size-adjust: 100%;
}
body {
  margin: 0;
}
article,
aside,
details,
figcaption,
figure,
footer,
header,
hgroup,
main,
menu,
nav,
...... and so on...

Saving Our Variables

In this section, we will jump back into our JavaScript to send our variables to the server. But first, we'll make another action in the Home controller that will store them in a Session variable.

C#
[HttpPost]
public EmptyResult SendVariables()
{
    Stream req = Request.InputStream;
    req.Seek(0, System.IO.SeekOrigin.Begin);
    string variables = new StreamReader(req).ReadToEnd();

    HttpContext.Session["variables.less"] = variables;

    return new EmptyResult();
}

Now, we need to jump back into the JavaScript. Basically, we are going to make a function that rewrites the variables.less file from our viewModel.

So in our main.js, we will create a sendVariables function, which will traverse the categories, their variables and rewrite the variables.less file.

But first, we will add some other things. In the _Layout.cshtml file, add an action link to the main menu bar. Like so:

HTML
<div class="navbar-collapse collapse">
    <ul class="nav navbar-nav">
        <li>@Html.ActionLink("Home", "Index", "Home")</li>
        <li>@Html.ActionLink("Elements", "Elements", "Home")</li>
        <li>@Html.ActionLink("Contact", "Contact", "Home")</li>
        <li><a href="#" 
        data-bind="click: sendAndReturnVariablesFile">Get the css</a></li>
    </ul>
    @Html.Partial("_LoginPartial")
</div>

We want the user to get this file as a download. This is not possible through Ajax and sending them to a blank page isn't acceptable. So we're going to set up an invisible iframe.

HTML
<iframe id="file-download" class="hidden"></iframe>

This could be anywhere in the page. But at the bottom before the body close tag seems like the best place.

We've just created the binding sendAndReturnVariables, so we will need to add that to our viewModel.

JavaScript
/**
 * Recreates variables.less and sends it to the server
 * @function sendVariablesFile
 */
sendAndReturnVariablesFile: function () {
    var model = ko.toJS(viewModel),
        lessFileString = "",
        i, j, prop;

    //Recreate the variables.less content
    for (prop in model.categories) {
        var category = model.categories[prop];
        lessFileString += "//== " + prop + '\n';

        for (i = 0, j = category.variables.length; i < j; i++) {
            var varName = category.variables[i];
            lessFileString += varName + ":" + model.variables[varName].value + ";\n";
        }
    }
    //Send it to the server
    $.ajax({
        url: "/Home/SendVariables",
        type: "POST",
        data: lessFileString,
        success: function () {
            //Set the iframe src to the css
            $("#file-download").attr("src", "/Home/GetCss");
        }
    });

Now, we need to add one line in the GetCss action of the homeController.

C#
var bytes = Encoding.UTF8.GetBytes(content);
var result = new FileContentResult(bytes, contentType);

result.FileDownloadName = "bootstrap-custom.css";

return result;

Swap Our Variables in Place

So now, we need to perform one last step. Remember the VirtualFileReader? It has a function GetFileContents where we are going to return our saved variables from Session.

C#
public string GetFileContents(string fileName)
{
    if (fileName == "variables.less")
    {
        var variables = HttpContext.Current.Session["variables.less"];

        return (string)variables;
    }
    else
    {
        fileName = GetFullPath(fileName);
        return  File.ReadAllText(fileName);
    }
}

The End (for now)

It had been a while since I wrote an article, but I regret it. I had a lot of fun creating this series and I learned enough for my actual product. I hope someone out there will appreciate my work.

I might actually create more articles around this example. I can think of some other things we could do. Users could create an account which will let them store multiple versions in a database.

We could then store the whole thing in Azure. Or perhaps publish the .css files to the Azure CDN.

But at the end of part four, I feel like this is a nice finished product.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)