Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Pro ASP.NET 2.0 In CSharp 2005 (2005) [eng]

.pdf
Скачиваний:
106
Добавлен:
16.08.2013
Размер:
29.8 Mб
Скачать

978 C H A P T E R 2 9 J AVA S C R I P T

The OnClientClick Property

Usually, you need to insert JavaScript by adding attributes to a control. However, one exception exists. If you want to handle button clicks with JavaScript code, you can use the OnClientClick property. Here’s an example that gets confirmation before a page is posted back:

<asp:button id="btnClick" runat="server" OnClientClick="return confirm('Post back to the server?');" text="Click Me"/>

The button click still posts back the page and raises server-side events. The difference is that the OnClientClick client-side logic fires first and then the postback is performed.

Tip You can use the OnClientClick attribute to cancel a postback. The basic pattern is to call a JavaScript method. If this method returns false, the postback is canceled.

Script Blocks

It’s impractical to place a large amount of JavaScript code in an attribute, particularly if you need to use the same code for several controls. A more common approach is to place a JavaScript function in a <script> block and then call that function using an event attribute.

The <script> tag can appear anywhere in the header or the body of an HTML document, and a single document can have any number of <script> tags in it. However, everything in the document is processed in the order in which it appears in the file, from top to bottom. In other words, if you need to call a function, that function must be defined in a <script> block before the event attribute that calls it.

The <script> tag takes a language attribute that specifies the language and version. Browsers will ignore <script> blocks for languages or language versions they don’t support (although you’ll see certain quirks).

A typical inline script looks like this:

<script language="JavaScript"> <!--

window.alert('This windows displayed through JavaScript.'); // -->

</script>

In this case, the HTML comment markers (<!-- and -->) hide the content from browsers that don’t understand script. Additionally, the closing HTML comment marker (-->) is preceded by a JavaScript comment (//). This is because extremely old versions of Netscape will throw a JavaScript parsing exception when encountering the closing HTML comment marker. Modern browsers don’t suffer from these problems, and most browsers now recognize the <script> tag (even if they don’t support JavaScript).

You can also use the src attribute to reference an external file containing JavaScript, as shown here:

<script language="JavaScript" src="ExternalJavaScript.js"> </script>

This technique is often used with complex JavaScript routines. You can also embed a JavaScript resource in a DLL assembly when you build a custom control using the WebResource attribute (discussed in Chapter 28).

C H A P T E R 2 9 J AVA S C R I P T

979

Note Placing JavaScript in a separate file or even embedding it in an assembly doesn’t prevent users from retrieving it and examining it (and even modifying their local copy of the web page to use a tampered version of the script file). Therefore, you should never include any secret algorithms or information in your JavaScript code. You should also make sure you repeat any JavaScript validation steps on the server, because the user can circumvent them.

Creating a JavaScript Page Processor

How many times have you clicked a web page just to watch the Internet Explorer globe spin for what seems like an eternity? Did your Internet connection go down? Was there any error connecting to a back-end system? Or is the system just that slow? These issues often complicate the deployment of new web-based solutions, particularly if you’re moving from an internal forms-based system that may appear to be more responsive. In this situation, the easiest way to help get the users back on your side is to provide them with messages along the way to let them know the system is currently working on their request.

One common way to give a status message is to use JavaScript to create a standard page processor. When the user navigates to a page that takes a long time to process, the page processor appears immediately and shows a standard message (perhaps with scrolling text). At the same time, the requested page is downloaded in the background. Once the results are available, the page processor message is replaced by the requested page.

You can’t solve the processing delay problem by adding JavaScript code to the target page, because this code won’t be processed until the page has finished processing and the rendered HTML is returned to the user. However, you can create a generic page processor that can handle requests for any other page.

To create a page processor, you need to react to the onLoad and onUnload events. Here’s a page that defines a table with the message text “Loading Page - Please Wait.” The <body> element is wired up to two functions, which aren’t shown here.

<html>

<head>

<title>LoadPage</title>

<script> <!-- JavaScript functions go here. --> </script>

</head>

<body onLoad="javascript:BeginPageLoad();" onUnload="javascript:EndPageLoad();">

<form id="frmPageLoader" method="post" runat="server"> <table border="0" cellpadding="0" cellspacing="0" width="99%" height="99%" align="center" vAlign="middle">

<tr>

<td align="center" vAlign="center">

<span id="MessageText">Loading Page - Please Wait</span> <span id="ProgressMeter" style="width:25px;text-align:left;"></span>

</td>

</tr>

</table>

</form>

</body>

</html>

980 C H A P T E R 2 9 J AVA S C R I P T

This page is saved as PageProcessor.aspx. To use the page processor, you request this page and pass the desired page as a query string argument. For example, if you want to load TimeConsumingPage.aspx in the background, you would use this query string:

PageProcessor.aspx?Page=TimeConsumingPage.aspx

The page processor needs very little server-side code. In fact, all it does is retrieve the originally requested page from the query string and store it in a protected page class variable. You can then access this variable by data binding expressions in the .aspx file. Here’s the complete page code:

public partial class PageProcessor : System.Web.UI.Page

{

protected string PageToLoad;

protected void Page_Load(object sender, System.EventArgs e)

{

PageToLoad = Request.QueryString["Page"];

}

}

The page is then rendered and sent to the client. The rest of the work is performed with client-side JavaScript. When the page processor first loads, the onLoad event fires, which calls the client-side BeginPageLoad() function. The BeginPageLoad() function keeps the current window open and begins retrieving the page that the user requested. To accomplish this, it uses the window.setInterval() method, which sets a timer that calls the custom UpdateProgressMeter() function periodically.

Here’s the code for the BeginPageLoad() JavaScript function:

var iLoopCounter = 1; var iMaxLoop = 6; var iIntervalId;

function BeginPageLoad()

{

/* Redirect the browser to another page while keeping focus */ location.href = "<%=PageToLoad %>";

/* Update progress meter every 1/2 second */ iIntervalId = window.setInterval

("iLoopCounter=UpdateProgressMeter(iLoopCounter,iMaxLoop)", 500);

}

Notice that the page you want to download isn’t hard-coded in the JavaScript code. Instead, it’s extracted using the data binding expression <%=PageToLoad %>. When the page is rendered on the server, ASP.NET automatically inserts the actual name of the page from the PageToLoad variable.

The BeginPageLoad() function is only part of the solution. You also need another custom JavaScript function in your web page, which is named UpdateProgressMeter(). The UpdateProgressMeter() function is called periodically on a timer. When it’s triggered, it simply adds periods to the status message to make it look more like an animated progress meter. It cycles repeatedly from 0 to 5 periods.

Here’s the JavaScript code for the UpdateProgressMeter() function:

function UpdateProgressMeter(iCurrentLoopCounter, iMaximumLoops)

{

iCurrentLoopCounter += 1; if(iCurrentLoopCounter <= iMaximumLoops)

{

ProgressMeter.innerText += "."; return iCurrentLoopCounter

C H A P T E R 2 9 J AVA S C R I P T

981

}

else

{

ProgressMeter.innerText = ""; return 1;

}

}

Finally, when the page is fully loaded, the client-side onUnload event fires, which triggers the EndPageLoad() JavaScript function. This function clears the progress message and sets a temporary transfer message that disappears as soon as the new page is rendered in the browser. Here’s the code:

function EndPageLoad()

{

window.clearInterval(iIntervalId);

ProgressMeter.innerText = "Page Loaded - Now Transferring";

}

No postbacks are made through the whole process. The end result is a progress message (see Figure 29-2) that remains until the target page is fully processed and loaded.

Figure 29-2. An automated progress meter

To test the page processor, you simply need to use a target page that takes a long time to execute on the server (because of the work performed by the code) or to be downloaded in the client (because of the size of the page). You can simulate a slow page by placing the following time delay code in the target page:

protected void Page_Load(object sender, System.EventArgs e)

{

// Simulate a slow page loading (wait ten seconds). System.Threading.Thread.Sleep(10000);

}

982 C H A P T E R 2 9 J AVA S C R I P T

As you can see, with just a small amount of client-side JavaScript code, you can keep the user informed that a page is processing. By keeping users informed, the level of perceived performance increases.

Using JavaScript to Download Images Asynchronously

The previous example demonstrated how JavaScript can help you create a more responsive interface. This advantage isn’t limited to page processors. You can also use JavaScript to download time-consuming portions of a page in the background. Often, this requires a little more work, but it can provide a much better user experience.

For example, consider a case where you’re displaying a list of records in a GridView. One of the fields displays a small image. This technique, which was demonstrated in Chapter 10, requires a dedicated page to retrieve the image, and, depending on your design, it may require a separate trip to the file system or database for each record. In many cases, you can optimize this design (for example, by preloading images in the cache before you bind the grid), but this isn’t possible if the images are retrieved from a third-party source. This is the case in the next example, which displays a list of books and retrieves the associated images from the Amazon website.

Rendering the full table can take a significant amount of time, especially if it has a large number of records. You can deal with this situation more effectively by using placeholder images that appear immediately. The actual images can be retrieved in the background and displayed once they’re available. The time required to display the complete grid with all its pictures won’t change, but the user will be able to start reading and scrolling through the data before the images have been downloaded, which makes the slowdown easier to bear.

The first step in this example is to create the page that displays the GridView. For the purposes of this example, the code fills a DataSet with a static list of books from an XML file.

protected void Page_Load(object sender, System.EventArgs e)

{

if (!Page.IsPostBack)

{

// Get data.

DataSet ds = new DataSet(); ds.ReadXml(Server.MapPath("Books.xml")); GridView1.DataSource = ds.Tables["Book"]; GridView1.DataBind();

}

}

Here’s the content of the XML file:

<?xml version="1.0" encoding="utf-8" ?> <Books>

<Book Title="Expert C# Business Objects" isbn="1590593448" Publisher="Apress"></Book>

<Book Title="C# and the .NET Platform" isbn="1590590554" Publisher="Apress"></Book>

<Book Title="Beginning XSLT" isbn="1590592603" Publisher="Apress"></Book>

<Book Title="SQL Server Security Distilled" isbn="1590592190" Publisher="Apress"></Book>

</Books>

As you can see, the XML data doesn’t include any picture information. Instead, these details need to be retrieved from the Amazon website. The GridView binds directly to the columns that are available (Title, isbn, and Publisher) and then uses another page (named GetBookImage.aspx) to find the corresponding image for this ISBN.

C H A P T E R 2 9 J AVA S C R I P T

983

Here’s the GridView control tag without the style information:

<asp:GridView id="GridView1" runat="server" AutoGenerateColumns="False"> <Columns>

<asp:BoundField DataField="Title" HeaderText="Title"/> <asp:BoundField DataField="isbn" HeaderText="ISBN"/> <asp:BoundField DataField="Publisher" HeaderText="Publisher"/> <asp:TemplateField>

<HeaderTemplate> Book Cover </HeaderTemplate>

<ItemTemplate>

<img src="UnknownBook.gif" onerror="javascript:this.src='Unknownbook.gif'" onload=

"javascript:this.src='GetBookImage.aspx?isbn= <%# Eval("isbn") %>';">

</ItemTemplate>

</asp:TemplateField>

</Columns>

</asp:GridView>

The innovative part is the last column, which contains an <img> tag. Rather than pointing this tag directly to GetBookImage.aspx, the src attribute is set to a local image file (UnknownBook.gif), which can be quickly downloaded and displayed. Then the onLoad event (which occurs as soon as the UnknownBook.gif image is first displayed) begins downloading in the background the real image from the GetBookImage.aspx page. When the real image is retrieved, it’s displayed, unless an error occurs during the download process. The onError event is handled in order to ensure that if an error occurs, the UnknownBook.gif image remains (rather than the red X error icon).

The GetBookImage.aspx performs the time-consuming task of retrieving the image, which can involve contacting a web service or connecting to a database. In this case, it simply hands the work off to a dedicated class named FindBook. Once the URL is retrieved, it redirects the page:

protected void Page_Load(object sender, System.EventArgs e)

{

FindBook findBook = new FindBook();

string imageUrl = findBook.GetImageUrl(Request.QueryString["isbn"]); Response.Redirect(imageUrl);

}

The FindBook class is more complex. It uses screen scraping to find the <img> tag for the picture on the Amazon website. Unfortunately, Amazon’s image thumbnails don’t have a clear naming convention that would allow you to retrieve the URL directly. However, based on the ISBN you can find the book detail page, and you can look through the HTML of the book detail page to find the image URL. That’s the task the FindBook class performs.

Two methods are at work in the FindBook class. The GetWebPageAsString() method requests a URL, retrieves the HTML content, and converts it to a string, as shown here:

public string GetWebPageAsString(string url)

{

// Create the request.

WebRequest requestHtml = WebRequest.Create(url);

// Get the response.

WebResponse responseHtml = requestHtml.GetResponse();

984 C H A P T E R 2 9 J AVA S C R I P T

// Read the response stream.

StreamReader r = new StreamReader(responseHtml.GetResponseStream()); string htmlContent = r.ReadToEnd();

r.Close();

return htmlContent;

}

The GetImageUrl() method uses GetWebPageAsString() and a little regular expression wizardry. Amazon image URLs take the form shown here:

http://images.amazon.com/images/P/" + [ISBN] + [some character sequence]

Using the regular expression, the code matches the full URL (with the ending character sequence) and returns it. Here’s the complete code:

public string GetImageUrl(string isbn)

{

try

{

//Find the pointer to the book cover image.

//Amazon.com has the most cover images,

//so go there to look for it.

//Start with the book details page.

isbn = isbn.Replace("-", "");

string bookUrl = "http://www.amazon.com/exec/obidos/ASIN/" + isbn;

//Now retrieve the HTML content of the book details page. string bookHtml = GetWebPageAsString(bookUrl);

//Search the page for an image tag that has the requested ISBN. string imgTagPattern =

"<img src=\"(http://images.amazon.com/images/P/" + isbn + "[^\"]+)\"";

Match imgTagMatch = Regex.Match(bookHtml, imgTagPattern); return imgTagMatch.Groups[1].Value;

}

catch

{

return "";

}

}

Note Using the dedicated Amazon web service would obviously be a more flexible and robust approach, although it wouldn’t change this example, which demonstrates the performance enhancements of a little JavaScript. Web services are dealt with in Part 6, and you can get information about Amazon’s offerings at http://www.amazon.com/gp/aws/landing.html.

The end result is a page that initially loads with default images, as shown in Figure 29-3.

C H A P T E R 2 9 J AVA S C R I P T

985

Figure 29-3. The initial view of the page

After a short delay, the images will begin to appear, as shown in Figure 29-4.

Figure 29-4. The page with image thumbnails

986 C H A P T E R 2 9 J AVA S C R I P T

Once loaded, the real book images will load in the background, but the user can begin using the page immediately.

Tip This technique becomes particularly powerful when combined with a little DHTML know-how. For example, you could download a portion of a page in the background and insert it when it’s available. Creating this effect is more work (and is notoriously difficult to support on different browsers), but it is an interesting technique.

Rendering Script Blocks

So far the examples you’ve seen have used static <script> blocks that are inserted directly in the .aspx portion of your page. However, it’s often more flexible to render the script using the Page.ClientScript property, which exposes a ClientScriptManager object that provides several useful methods for managing script blocks. Two of the most useful are as follows:

RegisterClientScriptBlock(): Writes a script block at the beginning of the web form, right after the <form runat="server"> tag

RegisterStartupScript(): Writes a script block at the beginning of the web form, right before the closing </form> tag

These two methods perform the same task—they take a string input with the <script> block and add it to the rendered HTML. RegisterClientScriptBlock() is designed for functions that are called in response to JavaScript events. You can place these <script> blocks anywhere in the HTML document. Placing them at the beginning of the web form is just a matter of convention and makes them easy to find. The RegisterStartupScript() is meant to add JavaScript code that will be executed immediately when the page loads. This code might manipulate other controls on the page, so to be safe you should place it at the end of the web form. Otherwise, it might try to manipulate elements that haven’t been created yet.

When you use RegisterClientScriptBlock() and RegisterStartupScript(), you also specify a key name for the script block. For example, if your function opens a pop-up window, you might use the key name ShowPopUp. The actual key name isn’t important as long as it’s unique. The purpose is to ensure that ASP.NET doesn’t add the same script function more than once. This scenario is most important when dealing with server controls that render JavaScript. For example, consider the ASP.NET validation controls. Every validation control requires the use of certain validation functions, but it doesn’t make sense for each control to add a duplication <script> block. But because each control uses the same key name when it calls RegisterClientScriptBlock(), ASP.NET realizes they are duplicate definitions, and it renders only a single copy.

For example, the following code registers a JavaScript function named confirmSubmit(). This function displays a confirmation box and, depending on whether the user clicks OK or Cancel, either posts back the page or does nothing. This function is then attached to the form through the onSubmit attribute.

protected void Page_Load(object sender, System.EventArgs e)

{

string script = @"<script language='JavaScript'> function confirmSubmit() {

var msg = 'Are you sure you want to submit this data?'; return confirm(msg);

}

</script>";

C H A P T E R 2 9 J AVA S C R I P T

987

Page.ClientScript.RegisterClientScriptBlock(this.GetType(), "Confirm", script); form1.Attributes.Add("onSubmit", "return confirmSubmit();");

}

Note To make it easier to define a JavaScript function over multiple lines, you can precede the string with the @ symbol. That way, all the characters are treated as string literals, and you can span multiple lines.

Figure 29-5 shows the result.

Figure 29-5. Using a JavaScript confirmation message

Later in this example, you’ll see a control that uses the RegisterStartupScript() to show a pop-up window.

Note In previous versions of ASP.NET, developers often used startup scripts to set the control that should get the focus when the page first displays. However, ASP.NET 2.0 encapsulates this functionality with the Control.Focus() method, so you don’t have to code your own solution any longer.

Script Injection Attacks

Often, developers aren’t aware of the security vulnerabilities they introduce in a page. That’s because many common dangers—including script injection and SQL injection—are surprisingly easy to stumble into. To minimize these risks, technology vendors such as Microsoft strive to find ways to integrate safety checks into the programming framework itself, thereby insulating application programmers.

One attack to which web pages are commonly vulnerable is a script injection attack. A script injection attack occurs when malicious tags or script code are submitted by a user (usually through a simple control such as a TextBox control) and then rendered into an HTML page later. Although this rendering process is intended to display the user-supplied data, it actually executes the script. A script injection attack can have any of a number of different effects from trivial to significant. If the user-supplied data is stored in a database and inserted later into pages used by other people, the attack may affect the operation of the website for all users.