ASP.NET adaptive rendering

Một phần của tài liệu ASP.NET 4.0 in Practice phần 4 pps (Trang 39 - 50)

Adaptive rendering was introduced in the first version of ASP.NET to provide different rendering for different devices. The first incarnation was used to differentiate between browsers and platforms: Internet Explorer, Netscape, Palm, and so on. It might seem that we’re talking about something that happened ages ago. In fact, the first version of ASP.NET was shipped in 2002, and the web was very different than it is now. Things like XHTML and HTML 4.01 support, cookies, and tables are established features in today’s browsers, but they weren’t then. The idea behind this adaptive ren- dering engine was to adapt the output to different devices to provide better results with different features. To accomplish this task, a database with different browser pro- files (called browser capabilities) was created.

You can access the current browser capabilities by querying the Browser property on the HttpRequest class, which contains an instance of the HttpBrowserCapabili- ties class. For example, if you want to know whether the current browser supports a specific feature, such as XMLHTTP (used in Ajax applications), you can simply write something like this:

<%=Request.Browser.SupportsXmlHttp%>

You can find more information on this class on MSDN at http://mng.bz/N94X.

Where ASP.NET browser capabilities failed in the past was in the lack of updates for new browsers and devices. To name a few, FireFox wasn’t supported by ASP.NET 1.1, and iPhone wasn’t recognized by ASP.NET 3.5. What that means is that specific fea- tures (like Ajax, validator controls, or mobile controls) couldn’t be activated, and the rendered markup probably didn’t reflect the true power of the device requesting the resource. Unfortunately, the definitions weren’t updated after the initial version, so they didn’t reflect the current market.

NEW BROWSER CAPABILITIES DEFINITION FILE IN ASP.NET 4.0 The browser capabilities definition format was updated for ASP.NET 4.0 with a new format that isn’t compatible with the previous one. If you need to migrate your definitions, you have to copy the old files under the global configura- tion directory.

Version 4.0 introduces a new set of devices previously not supported, such as Google’s Chrome, RIM’s BlackBerry, and Apple’s iPhone. Major browsers like Internet Explorer, FireFox, Opera, and Safari (for different platforms, where available) are already supported.

You can specify browser definitions globally (under %SystemRoot%\Microsoft.NET\ Framework\v4.0.30319\Config\Browsers\), or locally for a single application using a .Browser file under the special directory \App_Browsers\.

Figure 7.1 gives you a look at how adaptive rendering works.

When a specific control is ready to generate its markup, the ASP.NET infrastructure looks for a special class, called the control adapter. A control adapter indicates the rendering strategy associated with a particular control. By changing the control adapter (via browser capabilities), you can change the rendering strategy imple- mented by a control. You can change the strategy for a built-in control, for a given set of browsers, or globally. Implementing a control adapter can be an easy and fun task, and you can adapt your controls to your specific needs. You can find more informa- tion about control adapter architecture on MSDN at http://mng.bz/gz37.

Add OptionGroups to DropDownList

When you need to change the markup of a simple control, you have the choice of com- pletely replacing it with another one. Doing this is easy, except when you’re substituting complex controls, and reproducing all the features by hand costs you time. There are cases where you need to modify the output globally, and though you can do that by writ- ing a custom control and replacing the previous one, that will also cost you some time.

To simplify these scenarios, you can modify the output generated by a given con- trol globally by simply implementing a control adapter. And you can reuse it in other projects if you need to.

PROBLEM

The problem with this scenario involves markup generation and how you can adapt the output to your specific needs. In this first dive into ASP.NET adaptive rendering,

Control

RenderInternal()

Control.Render() Adapter.Render() Adapter?

No Yes

Control adapters Browser definitions

Figure 7.1 ASP.NET adaptive rendering works at both the page and control level. You can modify the output of any controls before rendering. If an adapter is associated with the control, rendering is performed by calling its Render method; otherwise, normal control rendering occurs.

TECHNIQUE 42

165 TECHNIQUE 42 Add OptionGroups to DropDownList

we’ll take a look at how you can increase the benefits of using the classic DropDown- List by adding support for option groups.

SOLUTION

To build a control adapter, you have to implement a class that overrides System. Web.UI.Adapters.ControlAdapter or System.Web.UI.WebControls.Adapters.WebControl- Adapter. You’ll have to override System.Web.UI.WebCon- trols.Adapters.WebControlAdapter for web controls, and it includes some basic features. If you want more con- trol, the first option is the preferred choice. You can also specify a page adapter to alter page rendering, but this tech- nique isn’t widely used; it’s more common to alter a single control markup than the Page markup.

A control adapter typically overrides the rendering logic. As we talked about in chapter 6, server controls usu- ally generate their markup in a series of RenderSomething methods, such as RenderBeginTag, RenderInput, and so on. These methods, per convention, are invoked from the Render method, which is associated with the rendering of the control in the page.

Note that a control adapter is not the way to go if you need to alter the behavior of a control. If you use a control adapter, you can’t add new properties or methods to the original control; you can only overwrite its rendering.

To understand what we’re going to do, look at the option groups in figure 7.2.

From a markup point of view, option groups are based on the following tags:

<select name="ProductsList" id="ProductsList">

<optgroup label="Milk and dairy product">

<option value="Yogurt">Yogurt</option>

<option value="Butter">Butter</option>

</optgroup>

<optgroup label="Eggs">

<option value="Eggs">Eggs</option>

</optgroup>

...

</select>

Unfortunately, the classic DropDownList doesn’t support option groups. You can only add ListItem inside the controls, so you have to change the control rendering.

To implement this feature, you have to write a new control adapter that will generate the child controls, honoring a new OptionGroup attribute. This attribute on the list item will be passed to the container, thanks to the IAttributeAccessor interface that List- Item implements. This interface lets you specify custom attributes inside a control. By

Figure 7.2 Option groups are used in a drop-down list to visually group elements.

By organizing elements with groups, you can increase the usability of your page.

default, these attributes are rendered as they’re written. In our scenario, we’ll take advantage of this behavior to intercept the value and generate the markup accordingly.

To modify the output, we have to override the RenderContents method. In this method, the control generates its output. (To understand how the control works, we suggest that you get Red Gate’s .NET Reflector at http://reflector.red-gate.com/.) The rendering is performed by the ListControl class, which DropDownList inherits from. The following listing contains the code to reproduce the original behavior.

C#:

public class DropDownListAdapter : WebControlAdapter {

protected override void RenderContents(HtmlTextWriter writer) {

DropDownList list = this.Control as DropDownList;

uniqueID = list.UniqueID;

string lastOptionGroup = null;

string currentOptionGroup = null;

foreach (ListItem item in list.Items) {

currentOptionGroup = item.Attributes["OptionGroup"] as string;

if (currentOptionGroup != null) {

if (lastOptionGroup == null ||

!lastOptionGroup.Equals(currentOptionGroup,

StringComparison.InvariantCultureIgnoreCase)) {

if (lastOptionGroup != null)

RenderOptionGroupEndTag(writer);

RenderOptionGroupBeginTag(currentOptionGroup, writer);

}

lastOptionGroup = currentOptionGroup;

}

RenderListItem(item, writer);

}

if (lastOptionGroup != null) RenderOptionGroupEndTag(writer);

} } VB:

Public Class DropDownListAdapter Inherits WebControlAdapter

Protected Overloads Overrides Sub RenderContents(

ByVal writer As HtmlTextWriter)

Dim list As DropDownList = TryCast(Me.Control, DropDownList) uniqueID = list.UniqueID

Listing 7.1 The control adapter code

Adapt control Cycle through items

Render closing tag

Render by item

Adapt control

167 TECHNIQUE 42 Add OptionGroups to DropDownList

Dim lastOptionGroup As String = Nothing Dim currentOptionGroup As String = Nothing

For Each item As ListItem In list.Items

currentOptionGroup = TryCast(item.Attributes("OptionGroup"), String) If Not currentOptionGroup Nothing Then

If lastOptionGroup Is Nothing OrElse

Not lastOptionGroup.Equals(currentOptionGroup, StringComparison.InvariantCultureIgnoreCase) Then If Not lastOptionGroup Is Nothing Then

RenderOptionGroupEndTag(writer) End If

RenderOptionGroupBeginTag(currentOptionGroup, writer) End If

lastOptionGroup = currentOptionGroup End If

RenderListItem(item, writer) Next

If lastOptionGroup IsNot Nothing Then RenderOptionGroupEndTag(writer) End If

End Sub End Class

The markup to include the control remains the same, but a new attribute is added to every ListItem (unfortunately, this attribute isn’t supported by IntelliSense):

<asp:DropDownList runat="server" ID="ProductsList" AutoPostBack="true">

<asp:ListItem OptionGroup="Milk and dairy product">Yogurt</asp:ListItem>

<asp:ListItem OptionGroup="Milk and dairy product">Butter</asp:ListItem>

<asp:ListItem OptionGroup="Nuts">Tree nuts (walnuts)</asp:ListItem>

<asp:ListItem OptionGroup="Soy">Soy</asp:ListItem>

<asp:ListItem OptionGroup="Other">Other</asp:ListItem>

</asp:DropDownList>

Thanks to the IAttributeAccessor interface implemented by ListItem, you can add the attribute without any problem. In the following listing, you’ll find the code that generates the single option in the list.

C#:

private void RenderListItem(ListItem item, HtmlTextWriter writer) {

writer.Indent++;

writer.WriteBeginTag("option");

writer.WriteAttribute("value", item.Value, true);

if (item.Selected)

writer.WriteAttribute("selected", "selected", false);

foreach (string key in item.Attributes.Keys) {

Listing 7.2 Code necessary to generate a single item

Cycle through items

Render closing tag

Render by item

if (!key.Equals("optiongroup",

StringComparison.CurrentCultureIgnoreCase)) writer.WriteAttribute(key, item.Attributes[key]);

}

writer.Write(HtmlTextWriter.TagRightChar);

if (Page != null) {

Page.ClientScript.RegisterForEventValidation(uniqueID, item.Value);

}

HttpUtility.HtmlEncode(item.Text, writer);

writer.WriteEndTag("option");

writer.WriteLine();

writer.Indent--;

} VB:

Private Sub RenderListItem(ByVal item As ListItem, ByVal writer As HtmlTextWriter) writer.Indent += 1

writer.WriteBeginTag("option")

writer.WriteAttribute("value", item.Value, True) If item.Selected Then

writer.WriteAttribute("selected", "selected", False) End If

For Each key As String In item.Attributes.Keys If Not key.Equals("optiongroup",

StringComparison.CurrentCultureIgnoreCase) Then writer.WriteAttribute(key, item.Attributes(key)) End If

Next

writer.Write(HtmlTextWriter.TagRightChar) If Page IsNot Nothing Then

Page.ClientScript.RegisterForEventValidation(uniqueID, item.Value) End If

HttpUtility.HtmlEncode(item.Text, writer) writer.WriteEndTag("option")

writer.WriteLine() writer.Indent -= 1 End Sub

Much of the code in listing 7.2 reflects the code implemented by the original Drop- DownList control. The changes to the behavior are, in fact, included in listing 7.1 to support the <optgroup/> tag. The code to generate the optgroup tag is simple and is shown in the following listing.

Every attribute will be rendered

Necessary for PostBack

Every attribute will be rendered

Necessary for PostBack

169 TECHNIQUE 42 Add OptionGroups to DropDownList

C#:

private void RenderOptionGroupBeginTag(string name, HtmlTextWriter writer) {

writer.Indent++;

writer.WriteBeginTag("optgroup");

writer.WriteAttribute("label", name);

writer.Write(HtmlTextWriter.TagRightChar);

writer.WriteLine();

} VB:

Private Sub RenderOptionGroupBeginTag(ByVal name As String,

ByVal writer As HtmlTextWriter) writer.Indent += 1

writer.WriteBeginTag("optgroup") writer.WriteAttribute("label", name) writer.Write(HtmlTextWriter.TagRightChar) writer.WriteLine()

End Sub

To apply the adapter to the control, you need to create a new file with the extension .browser under the \App_Browsers\ directory. This file will register the adapter locally to the application:

<browsers>

<browser refID="Default">

<controlAdapters>

<adapter

controlType="System.Web.UI.WebControls.DropDownList"

adapterType=" ASPNET4InPractice.DropDownListAdapter,App_Code" />

</controlAdapters>

</browser>

</browsers>

The refID attribute is used to specify the kind of browser the adapter should be applied to. If you use Default, the adapter will be applied globally.

DISABLING AND FORCING ADAPTIVE RENDERING If you don’t want to let ASP.NET decide on the adapter for a particular control, you can set the Adapter- Enabled property to false. The original markup is generated with that value.

If you want to specify an attribute for only a particular set of browsers, you can use this syntax:

<asp:label IE:Text="IE only text" Text="Other browsers text" runat="server" />

The IE: filter sets the Text property only when the page is accessed via Inter- net Explorer.

If you omit the OptionGroup property, the DropDownList will work like it usually does;

using the control adapter isn’t intrusive and will preserve your existing forms. When you need to, you can leverage this new feature by simply writing the correct markup.

Listing 7.3 Each option group is generated when needed

DISCUSSION

The scenario we’ve covered here is simple and pretty self-explanatory. By registering the control adapter, you can alter the control behavior without changing the markup already present in your application. Being able to do this is a big advantage because you can enhance the markup produced without compromising the functionalities. A custom control will achieve the same goals, but a control adapter has the advantage of letting you decide when to implement the markup, from project to project, at a cen- tral point and without changing anything in the application.

To better understand how deeply you can influence the inner workings of a cus- tom control, the next scenario will cover an advanced solution: how to modify the DataList control to produce a table-less layout, using <div/> tags instead of a table.

Build a table-less control adapter for the DataList DataList is probably considered obsolete in ASP.NET applications. You might be ask- ing, why are they even mentioning DataList in this book? We already have ListView, GridView, and all their friends. The answer is simple: DataList is the only control capable of displaying more than one item per row (natively, without having to do any CSS hacking).

To implement this formatting, DataList generates a table. Tables are good for dis- playing numerical data or reports, but they’re not intended to be used as the basis for the layout. For layout problems, the solution is to use CSS.

PROBLEM

Tables aren’t intended to be used to compose layout, but rather to display tabled data, such as financial lists and similar information. If you need to work with accessibility constraints, using a table for layout purposes could represent a problem. You can achieve the same results by using a table-less, CSS-based layout. By implementing a control adapter for the DataList control, you can continue to use a built-in control and change its behavior globally, like we did in the previous scenario.

SOLUTION

The solution proposed to handle this scenario is to use a new control adapter to mod- ify the markup generated by DataList, without replacing the control in the markup.

DataList can show multiple items per row by setting the RepeatColumns and Repeat- Direction properties. You can specify a vertical or horizontal alignment, and the items will be organized automatically in rows. This effect might be useful in a lot of sit- uations. From product catalogs to image galleries, it can help you organize the layout visually in a better way.

As in the previous example, the central task is to write a new class that inherits from WebControlAdapter and change the rendering process by overriding the Render- Contents method. This example differs from the previous one in that DataList is a templated control, but this isn’t a critical problem; we can create the inner controls man- ually as the control does originally. This new adapter ignores SeparatorItemTemplate, but if you think that template might be useful to you, you can always implement the TECHNIQUE 43

171 TECHNIQUE 43 Build a table-less control adapter for the DataList

corresponding code. As a good starting point, we suggest that you take a look at the code that’s generated with Red Gate’s .NET Reflector disassembler. The following listing shows the code that generates the structure.

C#:

protected override void RenderContents(HtmlTextWriter writer) {

DataList dataList = Control as DataList;

if (dataList != null) {

if (dataList.HeaderTemplate != null)

RenderHeader(writer, dataList);

if (dataList.ItemTemplate != null ||

dataList.AlternatingItemTemplate != null) {

RenderItem(writer, dataList);

if (dataList.RepeatDirection == RepeatDirection.Horizontal) return;

}

if ((dataList.Items.Count % RepeatColumns) != 0) {

writer.Indent--;

writer.WriteLine();

writer.WriteEndTag("div");

} }

writer.Indent--;

writer.WriteLine();

if (dataList.FooterTemplate != null)

RenderFooter(writer, dataList);

} VB:

Protected Overloads Overrides Sub RenderContents(

ByVal writer As HtmlTextWriter) Dim dataList As DataList = TryCast(Control, DataList) If dataList IsNot Nothing Then

If Not dataList.HeaderTemplate Is Nothing Then

RenderHeader(writer, dataList) End If

If Not dataList.ItemTemplate Is Nothing OrElse

Not dataList.AlternatingItemTemplate Is Nothing Then RenderItem(writer, dataList) If dataList.RepeatDirection = RepeatDirection.Horizontal Then Exit Sub

End If End If

Listing 7.4 Code that generates the markup structure

Render HeaderTemplate

Add items

Add final div

Render FooterTemplate

Render HeaderTemplate

Add items

If (dataList.Items.Count Mod RepeatColumns) <> 0 Then writer.Indent -= 1

writer.WriteLine() writer.WriteEndTag("div") End If

End If

writer.Indent -= 1 writer.WriteLine()

If dataList.FooterTemplate IsNot Nothing Then RenderFooter(writer, dataList) End If

End Sub

The complex part is encapsulated in the RenderItem method. You have to address the fact that, depending on the RepeatDirection value, you need to display a specific index. Take a look at the next listing.

C#:

private void RenderItem(HtmlTextWriter writer, DataList dataList) {

DataListItemCollection items = dataList.Items;

writer.WriteLine();

DataListItem currentItem;

int itemsPerColumn = (int)Math.Ceiling(

((Double)dataList.Items.Count) / ((Double)RepeatColumns));

int rowIndex, columnIndex, currentIndex = 0;

for (int index = 0; index < dataList.Items.Count; index++) {

rowIndex = index / RepeatColumns;

columnIndex = index % RepeatColumns;

currentIndex = index;

if (dataList.RepeatDirection == RepeatDirection.Vertical) currentIndex = (columnIndex * itemsPerColumn) + rowIndex;

currentItem = items[currentIndex];

VB:

Private Sub RenderItem(ByVal writer As HtmlTextWriter, ByVal dataList As DataList) Dim items As DataListItemCollection = dataList.Items writer.WriteLine()

Dim currentItem As DataListItem Dim itemsPerColumn As Integer = Convert.ToInt32(Math.Ceiling(

Convert.ToDouble(dataList.Items.Count) /

Convert.ToDouble(RepeatColumns))) Dim rowIndex As Integer, columnIndex As Integer,

currentIndex As Integer = 0

Listing 7.5 The DataList adapter renders templates and content

Add final div

Render FooterTemplate

Calculate page size

B

Get row and column index

C

Calculate index

D

Get

E item

Calculate page size

B

173 TECHNIQUE 43 Build a table-less control adapter for the DataList

For index As Integer = 0 To dataList.Items.Count - 1 rowIndex = Convert.ToInt32(Math.Floor(

Convert.ToDouble(index) /

Convert.ToDouble(RepeatColumns))) columnIndex = index Mod RepeatColumns currentIndex = index

If dataList.RepeatDirection = RepeatDirection.Vertical Then currentIndex = (columnIndex * itemsPerColumn) + rowIndex currentItem = items(currentIndex)

In the first part of this listing, you’ll see the formula that calculates the index to dis- play B before getting the row and column index C. Next, based on the items to be displayed per row, a new width property is added (via CSS) to every element, wrapped inside a div. Finally, ItemTemplate (or AlternatingItemTemplate) is instantiated in the container to display the template specified in the page, using the markup. After we have the current item index D, we need to get the item E.

At this point, we’re ready to produce the output for the first row:

C#:

if ((index % RepeatColumns) == 0) {

writer.WriteLine();

writer.WriteBeginTag("div");

writer.WriteAttribute("style", "clear:both");

writer.Write(HtmlTextWriter.TagRightChar);

writer.Indent++;

} VB:

If (index Mod RepeatColumns) = 0 Then writer.WriteLine()

writer.WriteBeginTag("div")

writer.WriteAttribute("style", "clear:both") writer.Write(HtmlTextWriter.TagRightChar) writer.Indent += 1

End If

The next step is to write the current element in the markup:

C#:

writer.WriteBeginTag("div");

TableItemStyle style = (currentItem.ItemType == ListItemType.Item) ? dataList.ItemStyle :

(dataList.AlternatingItemStyle == null ? dataList.ItemStyle :

dataList.AlternatingItemStyle);

style.Width = new Unit((int)Math.Abs((double)100 / RepeatColumns), UnitType.Percentage);

CssStyleCollection finalStyle = GetStyleFromTemplate(dataList, style);

Get row and column index

C

Calculate index

D

Get item

E

Một phần của tài liệu ASP.NET 4.0 in Practice phần 4 pps (Trang 39 - 50)

Tải bản đầy đủ (PDF)

(50 trang)