![]() |
|
Spaces home Damo's spot on the webProfileFriendsBlogMore ![]() | ![]() |
Damo's spot on the webThoughts on ASP.NET, CSS & team development
|
|||||||||||||||
|
March 11 IE8 beta 1 browser version targeting and ASP.NET ThemesYou might find that when you add the meta tag required to force IE8 beta 1 into IE7 (or 6) rendering mode into your ASP.NET page (or Master Page) that it doesn't work as expected.... IE8 continues to render the site in full standards mode. The reason might be that in beta 1 of IE8 the "X-UA-Compatible" meta tag must be the first element in the page head element and by default ASP.NET will insert the link tags to the CSS files in your theme at the beginning of the head element of your page (or Master Page), before any other elements that might actually be defined in the .aspx or .master file. To fix this, add the following code to the page's (or Master Page's) PreRender event handler (Page_PreRender): // Move browser tageting meta tag to top of header HtmlMeta meta = null; int metaXUACompatIndex = -1; foreach (Control ctl in page.Header.Controls) { meta = ctl as HtmlMeta; if (meta == null) continue; if (meta.HttpEquiv == "X-UA-Compatible") { // Grab index metaXUACompatIndex = page.Header.Controls.IndexOf(meta); break; } } if (metaXUACompatIndex > 0) { HtmlMeta metaXUACompat = page.Header.Controls[metaXUACompatIndex] as HtmlMeta; page.Header.Controls.RemoveAt(metaXUACompatIndex); page.Header.Controls.AddAt(0, metaXUACompat); } This will move the browser targeting meta tag to the top of the list so that IE8 will honour it. March 06 I'm at MIX08 in Las VegasKeep track of what I'm doing (when I have charge) on twitter: http://twitter.com/DamianEdwards February 15 ASP.NET & CSS: Using skins to "zero out" default style and set default CSS class namesASP.NET includes a feature called "Themes" that allows you to organise styling artifacts including CSS stylesheets and images into logical units that can be easily applied to a page or entire site through configuration. Part of this feature is the ability to create .skin files that set default values for certain properties of ASP.NET server controls as part of a theme. Not all properties of ASP.NET controls are skinnable. Indeed the initial intention of .skin files was to enable the setting of default values for properties such as FontColor and BorderWidth, which when serving to CSS supporting browsers ASP.NET will render as inline style attributes on the emitted HTML controls. So what place do they have when we apply all of our style rules via way of attached CSS stylesheets and selectors (the way we should)? Despite their evil initial purpose we can put .skin files to good use for two purposes:
So how does this work? In your theme folder add a new .skin file called Controls.skin: Now we want to add some declarations to the file to:
<asp:RequiredFieldValidator runat="server" ForeColor="" CssClass="validation-error" /> <asp:RangeValidator runat="server" ForeColor="" CssClass="validation-error" /> <asp:CompareValidator runat="server" ForeColor="" CssClass="validation-error" /> <asp:RegularExpressionValidator runat="server" ForeColor="" CssClass="validation-error" /> <asp:CustomValidator runat="server" ForeColor="" CssClass="validation-error" /> <asp:LinkButton runat="server" CssClass="button" /> <asp:Button runat="server" CssClass="button" /> <asp:TextBox runat="server" CssClass="text-box" /> And that's it! Now when ASP.NET renders these controls when they appear on a page (or in a site) with your theme configured, they'll automatically pick up properties defined in this .skin file. February 06 ASP.NET: Binding the ObjectDataSource to the hosting page or user controlIn my recent RDN presentation on CSS Layout with ASP.NET & Visual Studio 2008 I mentioned a technique I use to get some extra value from the ObjectDataSource control. From speaking with other developers I've found that many people try to use the control only to be dissatisfied with the way it works, and in particular the need to provide it with a separate business logic or gateway class to look for its CRUD methods on. This is most apparent when you are trying to separate the data-access, business and presentation logic through way of a MVC or MVP pattern like "Passive View". In this scenario most people go back to the method of setting the DataSource property of their data bound control directly and manually calling the DataBind() method. This is a shame as it prevents you from realising the productivity gains and ease of maintenance of the no-code support for paging and sorting when using a data source control (plus the ability to fully style the GridView as described in my previous article). Rather than abandoning the ObjectDataSource altogether, we can make it work for us, by sub-classing it and changing it's behaviour slightly so that it looks for its methods on the hosting page or user control instead, i.e. its parent in the control hierarchy. (If you have never looked at the data-source controls and two-way data-binding support in ASP.NET 2.0 before, it might be worth first checking out Scott Mitchell's excellent tutorials on the subject.) Start out by adding a new class to your project called ParentDataSource.cs. I like to put all custom and user controls in a Controls sub-folder and thus namespace.
Make this new class inherit from the ObjectDataSource class and a ToolboxData attribute to define its tag name (you'll need to add some using statements to import the appropriate namespaces too): namespace RDN.CSSReadiDepth.Controls { [ToolboxData("<{0}:ParentDataSource runat=server></{0}:ParentDataSource>")] public class ParentDataSource : ObjectDataSource { Now add a couple of constructors. These do a few things. First they enable paging and sorting by default (you don't need to do this but I use paging and sorting more often than not). Secondly they wire-up some events of the control to local handler methods: public ParentDataSource() : this(String.Empty, String.Empty) { } public ParentDataSource(string typeName, string selectMethod) : base(typeName, selectMethod) { this.EnablePaging = true; this.SortParameterName = "sortExpression"; this.Init += new EventHandler(ParentDataSource_Init); this.ObjectCreating += new ObjectDataSourceObjectEventHandler(ParentDataSource_ObjectCreating); this.ObjectDisposing += new ObjectDataSourceDisposingEventHandler(ParentDataSource_ObjectDisposing); } Now we need to add our event handler methods. They're going to do the following:
void ParentDataSource_Init(object sender, EventArgs e) { #if DEBUG System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch(); sw.Start(); #endif FindParentHost(this); #if DEBUG sw.Stop(); System.Diagnostics.Debug.WriteLine(String.Format("Finding parent host for data source took {0}", sw.Elapsed)); #endif } void ParentDataSource_ObjectCreating(object sender, ObjectDataSourceEventArgs e) { e.ObjectInstance = _parentHost; } void ParentDataSource_ObjectDisposing(object sender, ObjectDataSourceDisposingEventArgs e) { e.Cancel = true; } Note I've also included some timing code in the Init handler so you can measure the impact of this method (it's negligible). Finally we need to add some private members to support the handler methods: Object _parentHost; /// <summary> /// Walks the control tree to find the hosting parent page or user control /// </summary> /// <param name="ctl">The control to start the tree walk at</param> private void FindParentHost(Control ctl) { if (ctl.Parent == null) { // User control was not found, use page base type instead this.TypeName = this.Page.GetType().BaseType.FullName; _parentHost = this.Page; return; } // Find the user control base type UserControl parentUC = ctl.Parent as UserControl; MasterPage parentMP = ctl.Parent as MasterPage; if (parentUC != null && parentMP == null) { Type parentBaseType = ctl.Parent.GetType().BaseType; this.TypeName = parentBaseType.FullName; _parentHost = ctl.Parent; return; } else { FindParentHost(ctl.Parent); } } The method FindParentHost, walks the control tree from the data source control until it finds the user control it is hosted on. If it can't find a hosting user control it assumes the control is hosted on a page. Once it finds the host it sets the TypeName property accordingly (note that the MasterPage class actually inherits from UserControl so we explicitly exclude that case). Now that we have our customised data source control it's time to use it in anger! Add a declaration to the web.config to import the controls from the DLL containing the ParentDataSource control: <pages styleSheetTheme="Theme1"> <controls> <add tagPrefix="cc" namespace="RDN.CSSReadiDepth.Controls" assembly="RDN.CSSReadiDepth" /> </controls> </pages> Now you can place the control in your page or user control mark-up file (.aspx or .ascx) and set the DataSourceID of your data-binding target control: <asp:GridView ID="gvExample" runat="server" DataSourceID="pdsExample" DataKeyNames="Id" AllowPaging="true" AllowSorting="true" PageSize="10" AutoGenerateColumns="false" CssClass="customers-grid"> <Columns> <asp:BoundField HeaderText="First Name" DataField="FirstName" SortExpression="FirstName" HeaderStyle-CssClass="first-name" ItemStyle-CssClass="first-name" AccessibleHeaderText="The customer's first name" /> <asp:BoundField HeaderText="Last Name" DataField="LastName" SortExpression="LastName" HeaderStyle-CssClass="last-name" ItemStyle-CssClass="last-name" AccessibleHeaderText="The customer's last name" /> <asp:BoundField HeaderText="Age" DataField="Age" SortExpression="Age" HeaderStyle-CssClass="age" ItemStyle-CssClass="age" AccessibleHeaderText="The age of the customer" /> <asp:BoundField HeaderText="Member for" DataField="YearsAsMember" SortExpression="YearsAsMember" HeaderStyle-CssClass="years-as-member" ItemStyle-CssClass="years-as-member" AccessibleHeaderText="How many years the customer has been a member for" /> </Columns> </asp:GridView> In this example I need to implement two methods on my page's code-behind file: GetData and GetDataRowCount. The first gets the current page of data to display, the second gets the total number of records (this is needed so the data source controls can calculate paging information): #region << Data Binding Methods >>public IEnumerable<Customer> GetData(string sortExpression, int maximumRows, int startRowIndex) { if (this.Customers == null || this.Customers.Count == 0) return this.Customers; var sortFunc = this.Customers[0].GetPropertySelector<Customer, object>(sortExpression.Replace(" DESC", "")); if (sortExpression.ToUpper().IndexOf(" DESC") >= 0) { return this.Customers // Sort descending .OrderByDescending(sortFunc) // Grab a single page of data .Skip((startRowIndex / maximumRows) * maximumRows) .Take(maximumRows); } else { return this.Customers // Sort ascending .OrderBy(sortFunc) // Grab a single page of data .Skip((startRowIndex / maximumRows) * maximumRows) .Take(maximumRows); } } public int GetDataRowCount() { return (this.Customers != null ? this.Customers.Count : 0); } #endregion In this example the data is stored in a property on the page class itself and I'm using LINQ to simulate sorting and paging. The property is just wrapping calls into the application's session state. The data itself was loaded into session state when the application started up. In a real world example these calls might look up the data from a web-service or database, return data from cache, or in the MVP scenario first raise an event that the presenter class will handle and then return the value of a property on the page (who's contents was set by the event handler in the presenter class). Also, because these methods are instance methods on the actual page object you are free to manipulate other controls or properties of the page class. You can use this method equally well with any of the ASP.NET controls that support the data source controls, such as GridView, DetailsView, FormView and Repeater. It also works with two-way data-binding. February 04 ASP.NET & CSS: Styling sortable columns in the GridView using CSSThe following is an article describing one of the techniques I spoke about during my recent RDN topic, CSS Layout with ASP.NET & Visual Studio 2008. A common user interface requirement for applications is to visually indicate when:
In ASP.NET, we generally use the GridView control to display tabular data and provide the user with the ability to sort and page through that data. There are many examples on the web of techniques you can use to programmatically insert an image to indicate the currently sorted column and its sort direction (including the MSDN documentation). There are a number of problems with this approach:
So what's a better solution I hear you say? The following solution uses ASP.NET, the CSS Friendly Control Adapters and CSS styling to:
The CSS Friendly Control Adapters alter the HTML mark-up produced by ASP.NET for many of the included server controls so that it is more accessible, standards compliant and, importantly, easier to style using CSS. You can grab the latest source code for the adapters from CodePlex (be sure to download from the source code section, not the release section), however the technique shown here requires a small change to the included GridView adapter (further details about incorporating the CSS Friendly Control Adapters into your web project are included in the download). So, once you've downloaded the source, open up the solution and find the GridViewAdapter.cs file:
Replace its contents with this modified version. This adds a new function that sets the CSS class of the GridView's header cells based on which column the GridView is currently sorted by. So for example if I have a GridView defined as such: <asp:GridView ID="gvExample" runat="server" DataSourceID="pdsExample" DataKeyNames="Id" AllowPaging="true" AllowSorting="true" PageSize="10" AutoGenerateColumns="false" CssClass="customers-grid"> <Columns> <asp:BoundField HeaderText="First Name" DataField="FirstName" SortExpression="FirstName" HeaderStyle-CssClass="first-name" ItemStyle-CssClass="first-name" /> <asp:BoundField HeaderText="Last Name" DataField="LastName" SortExpression="LastName" HeaderStyle-CssClass="last-name" ItemStyle-CssClass="last-name" /> <asp:BoundField HeaderText="Age" DataField="Age" SortExpression="Age" HeaderStyle-CssClass="age" ItemStyle-CssClass="age" /> <asp:BoundField HeaderText="Member for" DataField="YearsAsMember" SortExpression="YearsAsMember" HeaderStyle-CssClass="years-as-member" ItemStyle-CssClass="years-as-member" /> </Columns> </asp:GridView> Then when the GridView is rendered sorted by the First name column, the HTML mark-up emitted will look like this: <div class="AspNet-GridView" id="ctl00_main_gvExample"> <table cellpadding="0" cellspacing="0" summary="" class="customers-grid"> <thead> <tr class="AspNet-GridView-Header"> <th class="sortable sorted asc first-name" scope="col"> <a href="javascript:__doPostBack('ctl00$main$gvExample','Sort$FirstName')">First Name</a></th> <th class="sortable last-name" scope="col"> <a href="javascript:__doPostBack('ctl00$main$gvExample','Sort$LastName')">Last Name</a></th> <th class="sortable age" scope="col"> <a href="javascript:__doPostBack('ctl00$main$gvExample','Sort$Age')">Age</a></th> <th class="sortable years-as-member" scope="col"> <a href="javascript:__doPostBack('ctl00$main$gvExample','Sort$YearsAsMember')">Member for</a></th> </tr> </thead> <tbody><!-- Data rows here --></tbody> </table> </div>
Note how we now have CSS classes added to the <th> tags to describe their sorting behaviour (sortable, sorted and asc). We can see that all columns are sortable, and that the First Name column is currently being sorted by in the ascending direction. And because the logic to apply these classes is in the control adapter, it is automatically applied to all GridViews in the application. There is a caveat to this approach however. The adapter relies on two properties of the GridView to determine the current sort column and direction: SortExpression and SortDirection. These are read only properties that are only populated when you provide data to the GridView by way of one of the data source controls introduced in ASP.NET 2.0, e.g. ObjectDataSource, SqlDataSource, etc., and the DataSourceID property. If you provide data to your GridView by setting its DataSource property directly from code behind (and calling DataBind()) the sort properties are not set and thus the adapter cannot insert the required CSS class names. You must also initially call the Sort() method on the GridView from the Page_Load() method in order to force the setting of these properties the first time the page is rendered: protected void Page_Load(object sender, EventArgs e) { if (!IsPostBack) { // Set default sort expression gvExample.Sort("FirstName", SortDirection.Ascending); } } If you are not using the data source controls (the ObjectDataSource in particular) I thoroughly recommended you take a look as they provide you with "free" sorting and paging functionality out of the box without the need to manually handle events on the GridView. If you don't like the way the ObjectDataSource requires a separate business logic class to provide its data access functionality, for instance if you are implementing a MVP pattern in the "Passive View" flavour, then you can sub-class the OjectDataSource and force it to look on the instance of the class for the hosting page (or user control) for its methods instead (this is the method I used in the RDN sample solution and I will post an article on this technique shortly). Now that we have our CSS classes set we can attach some CSS rules that provide some visual indicators on the sorting behaviour. The following CSS rules do just that (I've placed these in a common Grid.css file in the sample project): These style rules use a small number of background images to give our GridView a nice Vista-like look:
You can immediately see that the First Name column is currently the sort column in the ascending direction. Hovering over other sortable columns provides feedback by way of a :hover background image, here I've hovered my mouse over the Last Name column:
I've also added some formatting to the pager section of the GridView to highlight the current page. To apply styles that are specific to this particular GridView, e.g. column widths, we can add some style rules like thus: table.customers-grid { } table.customers-grid th.first-name, table.customers-grid td.first-name { width: 35%; Note: I've tested the included CSS here with IE7 and Firefox. There is a small issue with the Firefox rendering which I haven't bothered to fix (the left-most border is not displayed due to the different ways IE7 and Firefox collapse borders) and you will definitely need to make some changes to properly support IE6 (e.g. adding support for the :hover pseudo class on elements other than <a> using an .htc file) but they are outside the scope of this article. So there you have it. Using this method we have freed our code-behind of nasty image injection code and made our GridViews more accessible and stylable to boot. RDN ReadiDepth: CSS Layout with ASP.NET & Visual Studio 2008Thanks to all of those who attended my sessions on CSS Layout with ASP.NET & Visual Studio 2008 during last week's RDN tour of Australia. Most of you seemed to really enjoy it and I was glad to see many people stopping back to ask me specific questions. Great stuff. As I mentioned during my presentations I will be posting a series of articles on this blog relating to many of the topics I spoke about. Also the slides and demo solution I used are available for download here. In the meantime, don't hesitate to drop me a line with any questions you might have. February 01 HTML Email ArticleSomebody at the Melbourne evening RDN session last night asked me about HTML e-mail. Well today I got this article in my inbox. I hope it helps you in some way. January 22 When can we expect MS tooling & platform support for IE8?Thinking of the lag time between versions of Visual Studio and ASP.NET I'm starting to wonder how long it's going to be before we can expect updates or new versions that better support and even promote the adoption of IE8 and its support for web standards. Today, ASP.NET 2.0 doesn't render in XHTML compliant mode unless you force it too, and Visual Studio's support for the DOCTYPE declaration is limited at best while the VS2008 ASP.NET designer can still be broken by completely valid HTML and CSS. For IE8 to truly reach its potential and have the impact it should we need updated tools and platforms from MS. Is anybody listening? Come see me at RDN next week to learn about CSS based layout with Visual Studio 2008Next week I'll be travelling to Sydney, Canberra and Melbourne to deliver a ReadiDepth session on CSS based layout using Visual Studio 2008. If you've ever struggled making the leap to CSS based layout from table based layout come along to see just how easy it is to get started. I'll be covering the basics of CSS, how Visual Studio 2008 can assist you in developing CSS based layouts, wrestling ASP.NET into being more CSS friendly and my tips for web page and form layouts using clean, semantic mark-up and CSS. The cat's out of the bag, how will IE8 support both old and new sites?After the recent announcement regarding a pre-release version of IE8 passing the Acid2 test, I've been wondering how they were going to keep good on their promise of "don't break the web" with so many sites out there specifically designed to compensate for IE's many shortcomings. Well, Chris Wilson has just let the cat out of the bag over on the IE Blog. In short, if you want your page to render in IE8 in full hi-fidelity standards compliant wonder, you'll need to include a special <meta> tag in the page's <head>. Simple enough, and I agree with Chris' take on it regarding allowing web developers to introduce IE8 site changes on their schedule, not their users'. I'm sure there will be those who think this method is another abomination by Microsoft but personally I think it's a step in the right direction. Writing pages to target a specific versions of browsers is what we do already, Microsoft are simply providing us with a far more elegant way to achieve this. I personally think IE8 will herald a bigger change to the immediate future of the web than other much lauded technologies like Silverlight and RIAs. If it delivers on its promises, it will finally free up web designers and developers to more completely exploit the richness inherit in web standards today, and hopefully banish the use of table tags for layout forever! Also checkout the great article on A List Apart regarding this latest development. | ||||||||||||||