Cascading style sheets
In the first article, we saw how to assign a stylesheet based on the user's browser. This is good as it allows you to concentrate specific way of doing those styles in separated stylesheets. On top of that, you can also add a stylesheet customized as per the user settings. That assumes that you have some style settings per user in your Web application stored in a member's table, for example.
Usually, I will put all my styles in one stylesheet. Then, I will rely on a user specific stylesheet for specific style based on the user. This process is then called "cascading stylesheet". As the styles are already defined in the main stylesheet, you are just repeating here specific styles in regards to the user which will take priority over the pre-defined styles. Or, another good way is to define only generic styles in your main stylesheets. Then, you just add the specific user's style after. There are some goods and bad things in this. The first one allows you to benefit of all styles no matter is the user is logged or not. The second one will only make specific styles available when the user is logged. This is why I prefer the first approach.
Saving user's value in a member's table
So, we know that specific styles are defined in each user's record in your members table. Lets assume that the following field values are defined for a specific user:
We can then assume that we have our main stylesheet loaded and what needs to be done in order to have it fully completed is to incorporate the following values in our user stylesheet. Our user stylesheet may looks like the following:
.Texte { Font-Family: Arial, Helvetica, sans-serif; Font-Size: <%=Font%>px; Color: #000000; } TABLE.Test { Border: 1px #626262 solid; Background: #<%=Table%>; Border-Collapse: collapse; }
The following code may return some HTML which takes care of the styles:
FUNCTION GetStyle LOCAL lcHtml lcHtml='' lcHtml=lcHtml+'<script language=JavaScript src=/Javascript/Stylesheet.js>' lcHtml=lcHtml+Response.ExpandTemplate(gcClientFat+'StyleSheet\UserSpecific.html',,,.T.) RETURN lcHtml
In this example, it is assumed that the JavaScript line returns a proper stylesheet based on the user's browser. See the first part of this article for the details. Then, we have a gcClientFat variable which holds a FAT equivalent of your Web site root. UserSpecific.html contains the two styles as defined earlier.
So, this means, whenever a user is logged to your Web application, you are in position to have the styles to be adjusted on that user. For every process, you can then call GetStyle() which will return the proper style header in your HTML code.
Improving performance
But, what is wrong with all this? Well, at first, nothing terrible, it's just that some considerations need to be taken, especially if you have a lot of user's styles. Lets assume we have UserSpecific.html to have a size of 4k. It's not mostly the process of handling all this from the record level which is intensive but the aspect that this 4k file will be downloaded for all transactions. If you have users using your application intensively, that represents an additional bandwidth of 4k for every transaction. You will soon discover that you would like to have something better in order to obtain the same result but with a better flexibility.
Well, there is always a good way to do things. Here is one in this case. Browsers are smart enough to read from cache. Despite the fact that this is usually a source of problems, in this case, we can benefit of it greatly. Using the base approach, nothing is cache as all this style code is returned on top of each transaction you are doing so this doesn't allow a chance to the browser to cache it as a file as it's not a file. We need to find a way to make it cached. In order to obtain that, we need to deal with the style as a file in its entire entity. The concept is simple. Generate a user's style file when necessary and make reference to that file instead.
Keeping dynamic capabilities
I can already here some that say "Well, it has to be dynamic! What would happen when the user will change styles?". If being done with care, you will take care of all those individual issues in a flash. Lets start by creating a directory called "StyleSheet" where each individual stylesheet files for each user will exist. The first rule is to check if a stylesheet exist for that particular user. If it doesn't exist, we will create it. The following enhancements in our GetStyle() function can be implemented:
* expN1 Member ID FUNCTION GetStyle() PARAMETER tnNoMember LOCAL lcMember,lcHtml,lcHtmlStyle lcMember=PADL(tnNoMember,6,'0') lcHtml='' lcHtml=lcHtml+'<script language=JavaScript src=/Javascript/Stylesheet.js>' * If the stylesheet file exists IF FILE(gcClientFat+'StyleSheet\'+lcMember+'.css') * Do something if necessary lcHtml=lcHtml+'' ELSE * The stylesheet doesn't exist, lets create it lcHtmlStyle=Response.ExpandTemplate(gcClientFat+'StyleSheet\UserSpecific.html',,,.T.) STRTOFILE(lcHtmlStyle,gcClientFat+'StyleSheet\'+lcMember+'.css') ENDIF * Add the stylesheet lcHtml=lcHtml+'<link rel="STYLESHEET" href="'+gcClient+'StyleSheet/'+lcMember+'.css" type="text/css">' RETURN lcHtml
Browser specific
Back to our initial purpose, we wanted to return styles based on the browser. As for the user specific stylesheet, we are confronted with the same situation. So, lets add some rules in our logic to take care of that. Another important point we have to consider is the fact that the user may switch browser thus we need to know about it in order to overwrite the existing user specific stylesheet with one that fits within the specific browser. The detection of a browser change relates to saving the last transaction browser used and verify at each transaction if a switch occur. When this is the case, we will regenerate the user specific stylesheet based on the browser.
* expN1 Member ID FUNCTION GetStyle() PARAMETER tnNoMember LOCAL lcMember,lcHtml lcMember=PADL(tnNoMember,6,'0') lcHtml='' lcHtml=lcHtml+'<script language=JavaScript src=/Javascript/Stylesheet.js>' * If the stylesheet file exists IF FILE(gcClientFat+'StyleSheet\'+lcMember+'.css') * If the user is not using the same browser as the one he used for the last transaction IF NOT glIE=Member.IE CreateStyleSheet(tnNoMember) ENDIF ELSE * The stylesheet doesn't exist, lets create it CreateStyleSheet(tnNoMember) ENDIF * Add the stylesheet lcHtml=lcHtml+'<link rel="STYLESHEET" href="'+gcClient+'StyleSheet/'+lcMember+'.css" type="text/css">' RETURN lcHtml * expN1 Member ID FUNCTION CreateStyleSheet PARAMETER tnNoMember LOCAL lcHtml,lcMember,lcUserSpecific,lnOldSel * Set the user specific style sheet based on the browser IF glIE lcUserSpecific='UserSpecific.html' ELSE lcUserSpecific='UserSpecificNonIE.html' ENDIF lcHtml=Response.ExpandTemplate(gcClientFat+'StyleSheet\'+lcUserSpecific+'.html',,,.T.) lcMember=PADL(tnNoMember,6,'0') STRTOFILE(lcHtml,gcClientFat+'StyleSheet\'+lcMember+'.css') * We replace the value of the IE field in the members table * This assumes we are on the user record in the members table lnOldSel=SELECT() SELECT Member REPLACE IE WITH glIE SELECT(lnOldSel)
At this point, we have a user specific stylesheet which will be updated if it doesn't exist or if we detect that he is switching between Internet Explorer browser and another browser.
So far so good, but when there is a need of generating a new stylesheet, there is no guarantee the user will benefit of that immediately. This is because of the cache handling issues by several browsers. Unless the user manually forces a browser refresh, and there is no guarantee that this will work either, he will then force the browser to get the latest version of his stylesheet.
Forcing a browser refresh is one way, in most setups, to get the latest version of a stylesheet. One other consists of just closing and starting the browser again. Depending on your settings, this is likely to work as well. But, we need a bulletproof mechanism which will always work immediately independent of the user browser settings.
Sequencing the user specific stylesheet
I have implemented a technique which works at 100% so far and this has resolved that issue once and for all. The technique consists of sequencing the user specific stylesheet. It is just a matter of adding a number at the end of the file name. So, at first, the stylesheet might be named 0000011.css. The first six digits represent the member ID. The last digit represents the sequence. When we will update the user stylesheet next time, it will be 0000012.css. This is based on the fact that our member ID table won't exceed six digits. You can adjust that based on your specific needs. So, as a new file name is used, the browser won't read from the cache as this file doesn't exist in its cache. Otherwise, the same file name is used and the browser is likely to grab a copy from its cache instead of downloading the latest version.
But, this is not something you will leave as is. You will probably want to set a limit to the sequence and start back to 1 when it is reached. Otherwise, you will end up with large set of numbers without any specific purposes. I usually set it to 20. So, once we reach 20, we start back to 1.
As users are usually not changing browsers that much, the stylesheets usually remain intact for a while. Another thing that would influence the need to update the stylesheet is when the user changes something in his profile within your application. Based on what he changes, you might want to trigger an update of the stylesheet. As this is not something that changes a lot, the sequence will also remain pretty stable.
As by the time you reach 20, a few days would have probably elapsed (and that's quite unlikely to happen unless the user is playing a lot with his settings for specific purposes) and the browser is not likely to remember the specific stylesheet file which had the sequence 1. So, on immediate update of a stylesheet, no matter the local settings, the user will get the latest version of his stylesheet. And, that works well for users switching from different browsers from office to home, for example, as the same rule will apply.
I am not detailing that process here. This is quite easy to implement. For faster process, you can also save the current sequence in the member's table. If it exists, you will read that sequence and see if a related file exists. If yes, you grab it. Otherwise, you create a stylesheet and you update the sequence field with 1.
Cleanup stylesheets
This creates a situation that a lot of stylesheet files remain on the server drive. For those users that are regular users to your application, it doesn't matter that much. But, for those who came to your application once and will only come back on occasional basis or never, this will mean that unnecessary files will remain on the server drive.
It doesn't represent that much of a problem but I prefer to clean something that is not useful. Following the robot approach, as described in the first article of this series, you can easily add a task in it to cleanup files older than 5 days in that directory.
Remember that a stylesheet which doesn't exist will be created on the fly. So, even if you would cleanup all the files in that directory without any timeframe, on the next hit by each a user, the related file will be created. That would then mean that once a day per user, a new file would be created. That is nothing big to worry about it.
Summary
In overall, I've been using that approach on the Universal Thread since a year now. The difference is noticeable as we are saving several kilobytes of transfer on every hit and at the end of the day, this would represent megabytes. Add to that the fact that users are getting their HTML pages faster, you then have a professional stylesheet implementation to benefit of.