9.4. Working on the Client SideThe Sum web part performs its calculations on the server, but that's not really necessary or efficient. For high-volume applications, it's a good idea to do as much work on the client side as possible. For example, the following HTML creates an equivalent client-side web part that doesn't require a roundtrip to the server to perform the calculations: <html> <head> <script id="clientEventHandlersJS" language="javascript"> function _btn_onclick() { var arr = new Array(""); // Use getElementById, direct control refs don't work var _txt = document.getElementById("_txt"); var _div = document.getElementById("_div"); var total = 0, s = _txt.value; arr = s.split("\n"); for (var i in arr) { total += parseFloat(arr[i]); } _div.innerText = "Total: " + total; return; } </script> </head> <body> <form id="_frm"> <div id="_div">Total: </div> <TEXTAREA id="_txt" name="_txt" rows="10" cols="35"> </TEXTAREA> <br> <INPUT id="_btn" type="button" value="Sum" onclick="return _btn_onclick()"> </form> </body> </html> This code is stored as an HTM file in a resource, then loaded and rendered by the following line: ' See "Creating Web Part Appearance" for GetHtml() code. output.Write(GetHtml("clientSum.htm") At runtime this web part is visually identical to the server-side web part in Figure 9-8, but the calculation is done on the client computer via JavaScript. The result is much better performance and less network traffic, since the page isn't sent back to the server every time the member clicks the Sum button. 9.4.1. Using Scripts with Web ControlsThe ClientSum web part is efficient, but you can't easily get values from the contained HTML controls once the page returns to the server. To see this limitation, enter some values in ClientSum and refresh with F5: the values you entered are cleared. That happens because the HTML controls don't automatically preserve their state the way that ASP.NET web controls do. To solve this problem, use ASP.NET web controls rather than HTML controls. In other words, create a hybrid web part that uses both server-side controls and client-side scripts. Using client-side scripts with web controls requires these special steps:
When SharePoint renders a web part, it includes a lot of special code to preserve the state of web controls in the Controls collection. To make sure that code gets the right values, SharePoint pre-appends a unique identifier to each name and id element in the generated HTML, as shown here: <textarea name="FullPage:g_03d3b969_e9c0_4846_9cf5_b14b5e7f6aa7:_txt" id="FullPage_g_03d3b969_e9c0_4846_9cf5_b14b5e7f6aa7_ _txt" title="Enter a series of numbers to add." style="height:150px;width:300px;"> </textarea> If you don't add an ID property to the control (step 2), SharePoint generates a name attribute and omits id . If you don't add the web controls to the Controls collection (step 3), SharePoint doesn't preserve the state of the control and consequently doesn't pre-append UniqueID . The following code shows these steps implemented for the Sum control: ' .NET code running on the server. ' Declare child controls. Dim _txt As New TextBox Dim _br1 As New Literal Dim _br2 As New Literal ' Added literal Dim WithEvents _btn As New Button Dim _lbl As New TextBox ' 1) Changed to textbox Protected Overrides Sub CreateChildControls() ' Create utility object for dimensions. Dim u As Unit ' Set child control properties. With _txt .ID = "_txt" ' 2) Added ID for scripts . .Width = u.Pixel(300) .Height = u.Pixel(150) .TextMode = TextBoxMode.MultiLine .ToolTip = "Enter a series of numbers to add." End With _br1.Text = "<br>" _br2.Text = "<br>" _btn.Text = "Postback" ' Changed. With _lbl .ID = "_lbl" ' 2) Added ID for scripts . .Text = "Total: " .ReadOnly = True .BorderStyle = BorderStyle.None End With ' 3) Add the controls in the order to display them Controls.Add(_lbl) Controls.Add(_br1) Controls.Add(_txt) Controls.Add(_br2) End Sub ' Display web part. Protected Overrides Sub RenderWebPart _ (ByVal output As System.Web.UI.HtmlTextWriter) ' Load client-side script. output.Write(GetHtml("sumFinal.js")) ' Write controls to output stream. RenderChildren(output) ' 4) Add button to run script. output.Write("<INPUT id='_btn' type='button' " & _ "value='Sum' onclick='return _btn_onclick(""" & _ Me.UniqueID() & """)' name='_btn'>") End Sub Step 4 above is a little tricky. I embedded the HTML button control in my code (despite telling you to avoid this) because it's only one control and because it makes it easier to include Me.UniqueID as an argument to _btn_onclick . SharePoint also provides a ReplaceTokens method to replace embedded tokens with SharePoint values. For example, it replaces _WPID_ with the web part's unique ID: ' Alternate approach -- use ReplaceTokens output.Write( ReplaceTokens ("<INPUT id='_btn' type='button' value='Sum'" & _ " onclick='return _btn_onclick(""FullPage: _WPID_ "")' name='_btn'>")) ReplaceTokens can replace the following literals. (See the SharePoint SDK for additional details.)
The client-side script uses the passed-in UniqueID to get references to web controls through getElementsByName , as shown here: // JavaScript running on the client (sumFinal.js). <script id="clientEventHandlersJS" language="javascript"> // Pass in unique page ID generated by SharePoint. function _btn_onclick(uID) { var arr = new Array(""); // Get elements using passed-in unique ID. var _txt = document.getElementsByName(uID + ":_txt")[0]; var _lbl = document.getElementsByName(uID + ":_lbl")[0]; var total = 0, s = _txt.value; arr = s.split("\n"); for (var i in arr) total += parseFloat(arr[i]); _lbl.innerText = "Total: " + total; return; } </script> This completed control will now perform its calculation on the client side, and preserve its settings. Also, the values of the _lbl and _txt web controls are now available to the server. To test, add this code, set a breakpoint, and click the Postback button to see the values displayed in the Visual Studio Debug window: Private Sub _btn_Click(ByVal sender As Object, _ ByVal e As System.EventArgs) Handles _btn.Click ' Display values from controls Debug.Write(_txt.Text) Debug.WriteLine(_lbl.Text) End Sub Figure 9-9 shows the completed control at runtime in Debug mode. animal 9-9. Using debug mode to make sure control values are available on the server9.4.2. Importing Script BlocksI explained how to store HTML as a resource because it is a better way to build tables and other display elements of a web part. And I just used that same technique to insert a client-side script into a web part. That's OK for small scripts, but it means you have to rebuild the assembly each time you change the script. You can import externally stored scripts directly into a web part using the RegisterClientScriptBlock method. That method imports scripts from a server folder at runtime so you can modify and debug scripts without rebuilding the assembly. Instead, you can simply refresh the page (F5) to get the changes. To import script blocks into a web part:
Where you store the scripts depends on how the web part is installed. For web parts installed in a \ bin folder, create a subfolder named after the web part assembly in the C:\InetPub\ wwwroot \wpresouces folder. For example: C:\InetPub\wwwroot\wpresouces\Ch09Samples For web parts installed in the global assembly cache (GAC), create the assembly folder in C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\wpresources ; then create a sub folder in the new folder using the Version , Culture , and PublickKeyToken attributes from the web part's SafeControl element in Web.config . The name has the following form: version_culture_token Omit culture if the assembly culture is neutral. For example: C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\wpresources\ Ch09Samples\ 1.0.0.0_ _fb6919fe58e4ba63 When you copy the script files to the new folder, remember to remove surrounding <script> tags from the file or you will get syntax errors when you import the script. Scripts should only be loaded once per page, so you need to create a unique key for each script file. Then determine whether the file has already been imported by checking IsClientScriptBlockRegistered before calling RegisterClientScriptBlock , as shown here: Private Sub ImportScripts_PreRender(ByVal sender As Object, _ ByVal e As System.EventArgs) Handles MyBase.PreRender Const sKey As String = "sumFinal.js" Dim sFile As String = Me.ClassResourcePath & "/sumFinal.js" Dim sBlock As String = "<script language='javascript' src='" & sFile & "'/>" ' Load client-side script. If (Not Me.Page.IsClientScriptBlockRegistered(sKey)) Then _ Me.Page.RegisterClientScriptBlock(sKey, sBlock) End Sub In the preceding code, ClassResourcePath returns the location of the folder you created for script storage. The RegisterClientScriptBlock method inserts the script tag sBlock on the page and registers the script as sKey so it won't be loaded again. At runtime, SharePoint renders this output: <script language='javascript' src='http://localhost/wpresources/Ch09Samples/sumFinal.js'/> |