[转] more than one way to skin an app
http://www.theserverside.net/articles/showarticle.tss?id=SkinningAnApp
Visitors and followers of the recent Microsoft PDC got a preview of the next generation of ASP.NET, which included many new features including “master pages” and a technique called skinning. While ASP.NET version 2.0 is well over a year away, you can implement these tools yourself today. In this article, we are going to build a sample project that implements a Master Page and skinning.
What is Skinning & Why use it?
Skinning is the ability to change the appearance of an application at runtime. Several popular windows applications such as Windows Media Player include skinning support. Skinning your ASP.Net web application can enhance the application appearance and make it more appealing.
So the question is why would you want to implement skinning? There are several reasons you might want to implement skinning, a few of which are mentioned below.
- Branding: Web Site branding is vital to any business web presence. This is where skinning is the most useful and functional. Maybe you provide a hosted service for businesses that exposes interfaces to a user. You could implement skinning to allow your clients to make your app look like part of their web site so their customers may not even notice the switch between sites. Another example of branding might be a national (or international) organization wishing to provider a regional look and feel to the application. Even if you are not a multinational mega-corp with downstream partners or local agents, many local enterprises are branching out into horizontal markets in which multiple web sites, with identical (or nearly identical) functionality needs to be implemented with a very different appearances. An example would be a local restaurant company that has several different restaurants (each with its own brand) each site needs to “showMenu”, “acceptReservation”, “takePreOrder”, and “takeDeliveryOrder”. Using skinning the restaurant company can develop one web app that can serve up individually branded sites for each restaurant.
- Globalization: Another great use for skinning is to enhance the globalization support of your web site. .Net provides some great globalization tools for dealing with these issues. But some web site design and content choices may not make sense between different cultures. An example would be the position of the menu. In English cultures a left handed menu makes sense because we read left to right, but for Japanese visitors it might be nice to put the menu on the right. Also with skinning you can embed the language specific content within the skin files themselves instead of having to mess with satellite assemblies which would make it easier for your web designers.
- Site Management & Scaling: An ASP.NET web application, using the techniques we are laying out for you, becomes easier to manage multiple “virtual sites” within a single application context. If you are running just one site, then, well skinning isn’t for you, but if your alternative is running several different sites, then skinning is both more manageable and scalable. Particularly in a web farm environment where you are managing your applications across several servers. If nothing else, the fewer the better.
- More Efficient Development: In development you need to play to your strengths, when you manage a team of developers, it is even more important. The ability to functionally separate the presentation from the logical development allows you to make better use of expensive developer time.
- It’s a Comprehensive Solution: Alternative solutions don’t quite get it all done. Using CSS can ‘alter’ the appearance of your pages and the item level, but not the layout or content. CSS can be (in fact should be) an important element of the skinning solution, but doesn’t get you everything you need. Applying XML style sheets can get you some content management and even some of the layout issues, but you have to increase the complexity of your data and your style sheets to begin to approach the kind of flexibility you get with skinning.
We started playing with skinning in ASP.Net last summer when a client approached us about a web application they wanted to build and share with their sister companies. The application would be hosted on the same server and each sister company wanted to brand the application to match their site. What we settled on doing was creating a single web application with skinning capabilities. We determined what skin to load based on the hostname in the URL. This allows not only the sister companies to just brand the application for their companies but to actually change the look and feel so that it matches their site.
One thing ASP.Net provides is the ability to separate presentation from logic in the form of code behind and presentation files that can inherit from a class. Visual Studio.Net implements this well but it’s implementation only establishes a one to one relationship between the logic class and the presentation (Web Matrix doesn’t provide this feature). With skinning we will extend this to have many presentation files that inherits from single class. This means each skin will perform the same and run the same logic but will have the ability to look different.
In ASP.Net skinning is really easy to implement by taking advantage of ASP.Net’s features for separating code from presentation. You build your application logic in a code behind class that the ASP.Net presentation files inherit from. What we will do in our sample project is create directories for each skin. We will create two skins, one called “Blue’ and another one called “Green”. So the presentation files for “Page.ascx” is “Blue\Pages.ascx” and the other one is “Green\Pages.ascx”. So our application logic will simple choose which presentation file to load and because both presentation files inherit from the same code behind they provide the same functionality. For our sample we will create a drop down box in our Master Page that will change a session state variable that contains the name of the skin.
What is a Master Page?
A Master Page is a single page that provides a consistent layout and style to a series of pages. Take for example a Web Site like below:
Header | |||
Menu | Content Area | ||
Footer |
This is a very typical layout that includes a header at the top, a menu to the side, and an footer at the bottom. In between all this is the content area which is where the main content of the page goes. You can see an example of this style of page at TheServerSide, Microsoft.com and GotDotNet.com, among others. In this type of page typically the header, footer, and menu stay the same between the different pages. When using Master Pages we build our layout and static content(header, footer, and menu) in the master page and then we define a content area to load the content into. In our model we will be doing this by create an ASPX for our master page and then adding a placeholder control to load the content into. Also in this example we are going to add the ability to skin our master page.
Implementing a master page and skinning
To get the master page working in ASP.Net we need to direct all request to a single page, which we will call “default.aspx”. The “default.aspx” will load the master page skin and then use the requested URL to load the appropriate control in the content area. If you were not skinning you could combine this page in with your master page. This page’s ASPX will be pretty simple and will contain only the HTML tags necessary to form a page.
In order to do redirect all request to “default.aspx” we add a line of code to the Application_BeginRequest event. To handle the redirection we will use the Context.RewritePath method and pass it the location of our single page.
(In GLOBAL.ASAX)By using the RewritePath method we can transfer the request on the server side without the client ever realizing that another page is handling the request. When this happens a client requests “Page1.aspx” in his browser, it appears that to the user (and their browser) that they are viewing “Page1.aspx” but in reality they are viewing a page generated by “default.aspx”
Sub Application_BeginRequest(ByVal sender As Object, ByVal e As _
EventArgs)
'Redirect the call to the default.aspx page
Context.RewritePath(Context.Request.ApplicationPath & _
"/default.aspx")
End Sub
Now that we have the redirect functionality built lets create our Default.aspx.
<%@ Page Inherits="SkinningSample.PageDirector"%>
<HTML>
<HEAD>
<title>Skinning Sameple</title>
</HEAD>
<body>
<form id="myForm" method="post" runat="server" /> </body>
</HTML>
Notice the only thing in the body is the ASP.Net form tag named myForm. We will get a reference to the form in the code-behind and load the MasterPage skin into it. The code behind for the Default.aspx is as follows.
Public Class PageDirector Inherits System.Web.UI.Page Protected myForm As HtmlForm Private Sub Page_Load(ByVal sender As System.Object, ByVal e As _ System.EventArgs) Handles MyBase.Load 'First we will parse out the filename of the requested page. Dim sFile As String = _ Request.RawUrl.Substring(Request.RawUrl.LastIndexOf("/") + 1) 'We need to rewrite the path back to that of the requested 'page. Without this ASP.Net postback will not work properly Context.RewritePath(sFile) 'The PageSkin must have a placeholder named "ContentArea". 'This placeholder is where we will add the child control for 'the page content Dim myPageSkin As Control = GetSkinFile("MasterPage.ascx") 'Find the ContentArea Placeholder in the PageSkin Dim myContentArea As PlaceHolder = _ CType(myPageSkin.FindControl("contentArea"), PlaceHolder) 'Load the content control and add it to the Controls collection of the 'ContentArea myContentArea.Controls.Add(GetSkinFile("Hello.ascx")) 'Add the PageSkin to the Page's form. myForm.Controls.Add(myPageSkin) End Sub 'This function will retrieve the path to a skin file. Public Function GetSkinFile(ByVal FileName As String) As Control 'For purposes of this example we are storing the 'skin name in the session state. 'Set Varible to skin name Dim sSkin As String = Session("skin") 'If no skin is set Default to Blue. If sSkin = "" Then sSkin = "Blue" 'Pass back a skin path for the file requested. Return LoadControl(String.Format("{0}\skins\{1}\{2}", _ Request.ApplicationPath, sSkin, FileName)) End Function End Class
The first thing we do is parse out the file name from the URL. This is done so that ASP.Net will write out the form to post back to that URL instead of posting back to Default.aspx. Next we declare the variable myPageSkin and load a control called MasterPage.ascx. We use the GetSkinFile method to retrieve the skinned control. The GetSkinMethod simply creates the path of the control based off the session(“skin”) variable and then returns the control from that path. After we have a reference to our “MasterPage.ascx” skin file we set a reference to its ContentArea placeholder. At this point your can put in logic to choose which control to load as content, in this sample we have one content control so we will just load it using the GetSkinFile method. Our final step is to add the MasterPage control to myForm’s control collection.
Building the MasterPage Skin
Now that we have the Default.aspx built and have it set to load the MasterPage.ascx we need to build both the ASCX files and the code behind.
(Code in Blue\MasterPage.ascx)
<%@ Control Inherits="SkinningSample.MasterPage" %>
<FONT face=Arial color=#336699 size=7>Blue Site</FONT>
<P>Change Skin:<asp:DropDownList id=ddlSkin AutoPostBack="True" runat="server"/></P>
<P><asp:PlaceHolder id=ContentArea runat="server" /></P>(Code in Green\MasterPage.ascx)
<%@ Control Inherits="SkinningSample.MasterPage" %>
<FONT face="Courier" color="#39966" size="7">Green Skin</FONT>
<P><asp:PlaceHolder id="ContentArea" runat="server" /></P>
<HR>
<P>Change Skin:
<asp:DropDownList id="ddlSkin" AutoPostBack="True" runat="server"/></P>
As mentioned before we are keeping our samples as simple as possible and as such our master page is pretty light. We simply have a header, the content area, and a drop down list to switch skins. Between the two skins we change the layout slightly and we switch between two different font colors. We’ve talked previously about the ContentArea placeholder and how we will use it to load the content control. In our particular example here we have a drop down list that has a listItem for each skin. This is the drop down we will use to switch skins. Our code behind class for the Master Page looks like this.
Code in MasterPage.vb Public Class MasterPage Inherits System.Web.UI.UserControl Protected WithEvents ddlSkin As DropDownList Private Sub Form_Load(ByVal sender As Object, ByVal e As _ EventArgs) Handles MyBase.Load 'If we have a skin selected and this isn't a postback set the 'selected skin. If Not IsPostBack And Not Session("Skin") = "" Then ddlSkin.Items.Add("Blue") ddlSkin.Items.Add("Green") ddlSkin.SelectedValue = Session("Skin") End If End Sub Private Sub ChangeSkin(ByVal sender As Object, ByVal e As _ EventArgs) Handles ddlSkin.SelectedIndexChanged 'Set the new skin name in the session. Session("Skin") = ddlSkin.SelectedValue 'Reload this page so the skin changes take place. Response.Redirect(Request.RawUrl) End Sub End Class
All we are doing with the MaserPage class is managing the skin drop down list. So we have code to set the selected item and to populate the Session(“skin”) variable with the post back of the drop down list. When we reset the skin we do a redirect back to the current page so that we can reload the skin.
Building a skinned control
In our sample we have one control called “Hello.ascx”. Building a skinned control works exactly the same way as building the Master Page skin. We will create a ASCX file in each of the skin directories and then a code behind class to add functionality. This control will have three labels, one for the requested URL, one for the current date, and one for the current time.
(Code in Blue\Hello.ascx)
<%@ Control Inherits="SkinningSample.SayHello" %>
<P>
<asp:Label id="lblURL" runat="server" Font-Names="Arial" Font- Italic="True" ForeColor="#00C0C0">The URL you requested was {0}</asp:Label></P>
<P>
<asp:Label id="lblDate" runat="server" Font-Names="Arial" Font- Bold="True" ForeColor="Navy">The Current Date is {0:d}</asp:Label></P>
<P>
<asp:Label id="lblTime" runat="server" Font-Names="Arial" Font- Bold="True" ForeColor="Navy">The Current Time is {0:t}</asp:Label></P>(Code in Green\Hello.ascx)
<%@ Control Inherits="SkinningSample.SayHello" %>
<P>
<asp:Label id="lblDate" runat="server" Font-Names="Courier New"> Date: {0:D}</asp:Label></P>
<P>
<asp:Label id="lblTime" runat="server" Font-Names="Courier New"> Time: {0:T}</asp:Label></P>
<P>
<asp:Label id="lblURL" runat="server" Font-Size="X-Small" Font- Italic="True" Font-Bold="True" ForeColor="Green"> Requested URL: {0}</asp:Label></P>
Once again the files are very simple, each has three labels. The labels have different appearances and positions in the different skins. But both classes in inherit from the SayHello class and thus have the same functionality. Now we just need to build our SayHello class.
Public Class SayHello Inherits System.Web.UI.UserControl Protected lblURL As System.Web.UI.WebControls.Label Protected WithEvents lblDate As System.Web.UI.WebControls.Label Protected WithEvents lblTime As System.Web.UI.WebControls.Label Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load lblURL.Text = String.Format(lblURL.Text, Request.RawUrl) lblDate.Text = String.Format(lblDate.Text, Now.Date.Today) lblTime.Text = String.Format(lblTime.Text, Now.ToLocalTime) End Sub End Class
Again this is a very simple class and all it does is populate the values of the labels. Notice the technique for setting the labels’ text value. When doing skinning it is frequently advantageous to use the string.Format class to set the text of labels, particularly those with dates, times, and currencies. This gives the developer of the skin file more control over how the data is presented. Notice the value of the date label in blue Hello.ascx: The Current Date is {0:d}. The {0:d} indicates where the date value goes in the label so that the skin developer can format the label text however he chooses. The “d” in the {0:d} indicates how the date is formatted if you look at the Green Hello.ascx Date label we use a capital D and it prints out a long string for the date. (For more information about string.format see the documentation.)
Our example project is now complete. You should be able to run the project and get back a skinned page displaying the current URL, date, and time. If you type any page name you should get a response from it with it’s URL. You can also go to the drop down and change the current skin and get a different look and feel with the same functionality. As we mentioned before this is a very simple example with only the bare neccesities to make this work. But with a little of imagination there is a lot of power to these techniques. If we extend the ASP.NET object model by adding a custom user object, for instance, we have a perfect vehicle for delivering personalization. Furthermore, add user editable content controls, with the appropriate security, of course, and you have a site which I.T. can fire and, for the most part, forget.
Figure 1: Ok, so this is not the most spectacular implementation of skinning, but all the pieces are here for you to build a skinnable site to meet your needs.
As currently scheduled, in a little over a year ASP.Net version 2.0 should be released with all of these features built in. What’s more, Visual Studio.Net will have complete designer support for it. While all that will be nice you will find that the model we have showed you here to still have plently of uses. From what we have seen in the early stages of ASP.Net 2.0 there are some limitations to its skinning engine that we believe this model overcomes. Also don’t worry about compatibility, this skinning engine works in ASP.Net 1, 1.1, and version 2. So you won’t be rebulding it when version 2 comes around.
References
For more details on Master pages, see Michele Leroux Bustamente's article on the subject at <a href="http://msdn.microsoft.com/asp.net/community/authors/mlb/default.aspx" target="_blank">http://msdn.microsoft.com/asp.net/community/authors/mlb/default.aspx</a> |
Authors
Cos Callis has been programming in BASIC since 1976 when he was introduced to his uncle's TRS-80 Model I (4K of RAM and a cassette player for storage) and has been an "enthusiast" ever since. He currently works as a Senior Programmer/Analyst for Delta Dental of Oklahoma and is the Program Director for the Oklahoma City .NET Developers Group. In 2000 he and Daniel Clausen formed HyperL, Inc. which does independent consulting and development work. | |
Daniel Clausen, born into a family of computer enthusiasts, started programming at the age of 12. Since then programming has been his life's pursuit. Daniel has a MCSD on the .Net platform. He currently works as a Senior Programmer/Analyst for Delta Dental of Oklahoma and is the Web Master for the Oklahoma City .NET Developers Group. In 2000 he and Cos Callis formed HyperL, Inc. (www.hyperlinc.com) which does independent consulting and development work. |