Themes in ASP.NET 2.0 is a great concept, but it doesn't give you any control over how stylesheets are added to your pages. ASP.NET won't let you use multiple stylesheets with different media types in a arbitrary order. But you can easily fix it if you want to. I've put together an example which let's you use both themes and have full control over the stylesheet rendering.
The way themes works is that ASP.NET includes all .css files found in the theme folder into you page. The stylesheeets are added as runat at server link elements (HtmlControls with tagname "link"). They're added ordered by their names. The links won't get any media-attribute.
You can read more about the themes problems in Adam Kahtava's post: The Problems with Themes, Skins, and Cascading Style Sheets (CSS) - Where it all Falls Apart
A simple example
You want your web pages to look nice when you print them. The way to do it is to add a stylesheet with media="print" and put it after the default stylesheet. If you don't use themes you could add this to the page's head:
| ASP.NET |
1
2
3
|
<link type="text/css" href="StyleSheet.css" rel="stylesheet" media="all" />
<link type="text/css" href="Print.css" rel="stylesheet" media="print" />
|
Print.css could include rules for hiding elements like navigation and banners that should not be visible when you print it.
If you use themes and the stylesheets are in the theme folder. The output for your page would be like:
| ASP.NET |
1
2
3
|
<link type="text/css" href="App_Themes/TheTheme/Print.css" rel="stylesheet" />
<link type="text/css" href="App_Themes/TheTheme/StyleSheet.css" rel="stylesheet" />
|
If Print.css hides any element, that element won't be visible at all because the StyleSheet.css is rendered after Print.css.
Take control
I made a simple user control to put in the head. It's just a sub class to PlaceHolder so you can add any controls you like in it. I've assumed that stylesheets are written as literal content and when the control prerenders it replaces %Theme with the actual theme path. Put this code in the head of the page:
| ASP.NET |
1
2
3
4
5
6
7
8
|
<head runat="server">
<title>…</title>
<cc1:Styles ID="Styles1" runat="server" ThemeVariableName="%Theme">
<link rel="Stylesheet" type="text/css" href="%Theme/StyleSheet.css" media="all"/>
<link rel="Stylesheet" type="text/css" href="%Theme/Print.css" media="print"/>
</cc1:Styles>
</head>
|
How does it work?
The control does two things in the prerendering:
- Disable default rendering
The page's header must be set runat="server" when you use themes. Therefore it's possible to access it's control collection. You could easily remove any server side links from the head in the page's onPreRender event. But it's not possible to do that from a user control. So instead of removing the links, we make them invisible
- Replace %Theme with the actual theme path
Literal text within a PlaceHolder is added as LiteralControl's. I use a regular expression to replace all occurance of %Theme in LiteralControls to the actual theme path.
Source code
| C# |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
|
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Text;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.HtmlControls;
using System.Text.RegularExpressions;
namespace Dentaku.Web.UI {
[DefaultProperty("ThemeVariableName")]
[ToolboxData("<{0}:Styles runat=\"server\"></{0}:Styles>")]
[Themeable(true)]
public class Styles : PlaceHolder {
[Bindable(true)]
[Category("Appearance")]
[DefaultValue("%Theme")]
[Localizable(false)]
[Description("Name of the variable that should be replaced by the actual theme path")]
public string ThemeVariableName {
get {
String s = (String) ViewState["ThemeVariableName"];
return ((s == null) ? "%Theme" : s);
}
set {
ViewState["ThemeVariableName"] = value;
}
}
/// <summary>
/// Fix controls before render
/// </summary>
protected override void OnPreRender(EventArgs e) {
base.OnPreRender(e);
if (this.Visible) {
// Hide any server side css
foreach (Control c in this.Page.Header.Controls) {
if (c is HtmlControl && ((HtmlControl) c).TagName.Equals("link",
StringComparison.OrdinalIgnoreCase)) {
c.Visible = false;
}
}
// Replace ThemeVariableName with actual theme path
Regex reg = new Regex(ThemeVariableName,
System.Text.RegularExpressions.RegexOptions.IgnoreCase);
foreach (Control c in this.Controls) {
if (c is LiteralControl) {
LiteralControl l = (LiteralControl) c;
l.Text = reg.Replace(l.Text, this.ThemePath);
}
}
}
}
/// <summary>
/// Get the theme path
/// </summary>
public string ThemePath {
get {
return String.Format("{0}/App_Themes/{1}",
this.Page.Request.ApplicationPath,
this.Page.Theme);
}
}
}
}
|
Skin it
Do you need different number of stylesheets for each theme? You can use skin-files for the Styles control. But if you do, you might want to use skins for all themes. Any text within the Styles control in the page's header will be rendered along with any text set in the skin file. Example what to put in your skin file:
| ASP.NET |
1
2
3
4
5
|
<cc2:Styles runat="server">
<link rel="Stylesheet" type="text/css" href="%Theme/StyleSheet.css" media="all"/>
<link rel="Stylesheet" type="text/css" href="%Theme/Print.css" media="print"/>
</cc2:Styles>
|
2007-01-30 13:32:00 | 19 Comments | Posted in
ASP.NET
| Link | digg this
If you precompile a web site with a theme set, the compiler adds the theme setting into the @Page directive on every ASPX page in the site. Probably not what you'd expect. On my last day at work before my vacation I stumbled upon this unexpected problem. The only thing I needed to do before leaving work was to my compile web site and deploy it. The web site was to be deployed in three versions with different themes. I spent the last frustrating hour trying to find out why all three web looked same theme after I had deployed them. I had changed the <pages theme="theme1" />, and all three sites had different themes set. Everything looked right, but still it didn't work. I had compiled the site as an updateble web site. At last I found out that if I didn't set a theme in web.config before I compiled the web. I could change it afterwards. I didn't have the time to check what the real problem was. During my vacation, my co worker Johan Dewe was handling the project. So he had to deal with the problem when I was away. He wrote a post about this on his blog (in Swedish): Theme settings i web.config i kombination med förkompilerad ASP.Net. He also found that K. Scott Allen wrote Caveat With ASP.NET Precompilation and web.config Settings.
Johan also has a solution how to make deployment projects which fix the theme problem: Search and replace med MSBuild och Web Deployment Project. Because it's in Swedish I'll give you a rough translation. You have to use Web Deployment Project (WDP) och MSBuild Community Tasks. Download and install the MSBuild Community Tasks, update your WDP file and add this reference to the target file <Import Project="$(MSBuildExtensionsPath)\MSBuildCommunityTasks\MSBuild.Community.Tasks.Targets"/>
Remove theme before compile
- Create a new WDP and open the project file.
- Make sure that a temporary folder is created for the source files by extend the elementet PropertyGroup:
<EnableCopyBeforeBuild>true</EnableCopyBeforeBuild>
- Update BeforeBuild target by adding a task that removes the theme setting from the <pages>-element:
<Target Name="BeforeBuild">
<FileUpdate
Files="$(CopyBeforeBuildTargetPath)\web.config"
Regex="(\u003Cpages.*)(theme=\u0022.*?\u0022)"
ReplacementText="$1" />
</Target>
So before the site is compiled the theme-setting will be removed and not added to every page.
2006-08-23 23:22:00 | 3 Comments | Posted in
ASP.NET
| Link | digg this
The PostBackUrl problem I wrote about before (How to fix AutoPostBack and PostBack error for ASP.NET pages with PostBackUrl button) is a bug. Got a mail from Microsoft which confirms it. No wonder I didn't find anything about it on Google when I tried to solve the problem. My solution would probably be to change __doPostBack function to always reset the action before submitting the form.
| JavaScript |
1
2
3
4
5
6
7
8
9
|
function __doPostBack(eventTarget, eventArgument) {
if (!theForm.onsubmit || (theForm.onsubmit() != false)) {
theForm.action = "http://<page's url>";
theForm.__EVENTTARGET.value = eventTarget;
theForm.__EVENTARGUMENT.value = eventArgument;
theForm.submit();
}
}
|
2006-06-18 15:35:00 | 25 Comments | Posted in
ASP.NET
| Link | digg this
Do you need to install ASP.NET providers on a SQL Server with a non default collation? This blog is run on a SQL Server 2000 that uses "Finnish Swedish". So when I tried to install the ASP.NET providers for SQL Server I got the error: "Cannot resolve collation conflict for equal to operation.". To get it running you need to edit the script before running it.
1. Generating the script
Generate the script with aspnet_regsql in your .NET 2.0 folder. In my case it's C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727. Use this to create a file in C:
aspnet_regsql -sqlexportonly "c:\i
nstall providers.sql" -A all -d <YourDataBaseName>
Using the -d option includes the name of your database in the script.
2. Fix the collation
The stored procedures aspnet_UsersInRoles_AddUsersToRoles
and aspnet_UsersInRoles_RemoveUsersFromRoles uses temporay tables to store user names. To make the script work, search for DECLARE @tbNames and add COLLATE <your database collation> to row as shown below:
3. Install the providers
Open up the script in a SQL Query Analyzer and execute it.
4. Set the connection string
ASP.NET uses a default connectionstring named LocalSqlServer, remove it and add it again in Web.config:
| XML |
1
2
3
4
5
6
7
8
9
10
11
|
<connectionStrings>
<remove name="LocalSqlServer"/>
<add name="LocalSqlServer"
connectionString="Data Source=<your server>;
Initial Catalog=<database name>;
User Id=<user>;
Password=<password>;"
providerName="System.Data.SqlClient"/>
</connectionStrings>
|
2006-06-09 23:20:00 | 2 Comments | Posted in
ASP.NET
| Link | digg this
I recently worked on a web page which used the new PostBackUrl feature for buttons. On the same page I used an RadioButtonList with AutoPostBack enabled. After testing the page something strange happened in FireFox. When I clicked on an item in the RadioButtonList I was sent to the PostBackUrl page. After some debugging and testing the same page both in FireFox and Internet Explorer I found what was wrong.
When you click on a button with a PostBackUrl, the ASP.NET JavaScript changes the page's form action attribute to the PostBackUrl. When the you go back to the previous page by clicking on the browser's back button, the AutoPostBack on that page is now broken. This also breaks the normal postback for buttons with no value for PostBackUrl. FireFox remembers the page's last state and in this case that the action attribute is set to the PostBackUrl. This is not a problem in Internet Explorer, it doesn't remember it's state when going back. The problem only occurs when the user clicks on the back button or on a JavaScript link which will cause the browser to go back.
The problem is the way __doPostBack works. It relies on the action attribute of the page's form. Whatever action is set to. Of course it's possible to fix this in several ways. If you want you can try to change it on the server side. But it's really not necessary. A few lines of JavaScript will do the trick.
Solution
Put this little piece of code directly after the <form runat="server"> on your page.
| ASP.NET |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
<form id="form1" runat="server">
<script type="text/javascript">
// PostBackUrl hack by Per Zimmerman | www.dentaku.com
var __oldAction = theForm.action;
var __oldPostBack = __doPostBack;
__doPostBack = function(eventTarget, eventArgument) {
theForm.action = __oldAction;
__oldPostBack(eventTarget, eventArgument);
}
</script>
<!-- rest of the page... -->
</form>
|
You can put it anywhere after the form, but it's better that the code is run as soon as possible. The first time the page is loaded, the code stores the original action and postback function in variables and then sets __doPostBack to a new function. When the new __doPostBack is called it first resets the action attibute to the original value before it calls the old __doPostBack function. You could set action attibute to an empty string to get a normal postback, but I wanted to make sure the function use the value ASP.NET supplied.
Test pages
I've uploaded test pages for you to try.
- Default page
- Hacked page
2006-06-03 23:11:00 | 31 Comments | Posted in
ASP.NET
| Link | digg this
My name is Per Zimmerman. I'm a proffessional web developer working mostly with solutions using ASP.NET, SQL Server, HTML, CSS and JavaScript. I have developed this blog engine. My plan is to write about issues about developing web and ASP.NET solutions. The blog is still work in progress and I'm planning to add more features.
2006-06-02 00:49:00 | 4 Comments | Posted in
Other
| Link | digg this