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".
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:
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:
public FileContentResult GetCss()
{
var contentType = "text/css";
var bootstrapLessContent = System.IO.File.ReadAllText(
HostingEnvironment.MapPath("~/Content/bootstrap/bootstrap.less")
);
var config = new DotlessConfiguration();
config.MinifyOutput = false;
config.ImportAllFilesAsLess = true;
config.CacheEnabled = false;
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:
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:
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
.
config.LessSource = typeof(VirtualFileReader);
Testing
Run the site and go to the path:
/home/getcss
We should now get a compiled CSS file:
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.
[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:
<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.
<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
.
sendAndReturnVariablesFile: function () {
var model = ko.toJS(viewModel),
lessFileString = "",
i, j, prop;
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";
}
}
$.ajax({
url: "/Home/SendVariables",
type: "POST",
data: lessFileString,
success: function () {
$("#file-download").attr("src", "/Home/GetCss");
}
});
Now, we need to add one line in the GetCss
action of the homeController
.
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
.
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.