ptg 914 CHAPTER 19 Building Data Access Components with ADO.NET private string _title; private string _director; public string Title { get { return _title; } set { _title = value; } } public string Director { get { return _director; } set { _director = value; } } } } Using Asynchronous ASP.NET Pages When you take advantage of asynchronous ADO.NET methods, you must also enable asynchronous ASP.NET page execution. You enable an asynchronous ASP.NET page by adding the following two attributes to a page directive: <%@ Page Async=”true” AsyncTimeout=”8” %> The first attribute enables asynchronous page execution. The second attribute specifies a timeout value in seconds. The timeout value specifies the amount of time that the page gives a set of asynchronous tasks to complete before the page continues execution. After you enable asynchronous page execution, you must set up the asynchronous tasks and register the tasks with the page. You represent each asynchronous task with an instance of the PageAsyncTask object. You register an asynchronous task for a page by calling the Page.RegisterAsyncTask() method. For example, the page in Listing 19.35 displays the records from the Movies database table in a GridView control. The database records are retrieved asynchronously from the AsyncDataLayer component created in the previous section. LISTING 19.35 ShowPageAsyncTask.aspx <%@ Page Language=”C#” Async=”true” AsyncTimeout=”5” Trace=”true” %> <%@ Import Namespace=”System.Threading” %> <!DOCTYPE html PUBLIC “-//W3C//DTD XHTML 1.0 Transitional//EN” “http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd”> <script runat=”server”> From the Library of Wow! eBook ptg 915 Executing Asynchronous Database Commands 19 private AsyncDataLayer dataLayer = new AsyncDataLayer(); void Page_Load() { // Setup asynchronous data execution PageAsyncTask task = new PageAsyncTask(BeginGetData, EndGetData, TimeoutData, null, true); Page.RegisterAsyncTask(task); // Fire off asynchronous tasks Page.ExecuteRegisteredAsyncTasks(); } IAsyncResult BeginGetData(object sender, EventArgs e, AsyncCallback callback, object state) { // Show Page Thread ID Trace.Warn(“BeginGetData: “ + Thread.CurrentThread.GetHashCode()); // Execute asynchronous command return dataLayer.BeginGetMovies(callback, state); } void EndGetData(IAsyncResult ar) { // Show Page Thread ID Trace.Warn(“EndGetDate: “ + Thread.CurrentThread.GetHashCode()); // Bind results grdMovies.DataSource = dataLayer.EndGetMovies(ar); grdMovies.DataBind(); } void TimeoutData(IAsyncResult ar) { // Display error message lblError.Text = “Could not retrieve data!”; } </script> <html xmlns=”http://www.w3.org/1999/xhtml” > <head id=”Head1” runat=”server”> <title>Show Page AsyncTask</title> </head> <body> <form id=”form1” runat=”server”> <div> From the Library of Wow! eBook ptg 916 CHAPTER 19 Building Data Access Components with ADO.NET <asp:Label id=”lblError” Runat=”server” /> <asp:GridView id=”grdMovies” Runat=”server” /> </div> </form> </body> </html> The page in Listing 19.35 creates an instance of the PageAsyncTask object that represents the asynchronous task. Next, the PageAsyncTask object is registered for the page with the Page.RegisterAsyncTask() method. Finally, a call to the Page.ExecuteRegisteredAsyncTasks() method executes the task. (If you don’t call this method, any asynchronous tasks registered for the page are executed during the PreRender event automatically.) The constructor for the PageAsyncTask object accepts the following parameters: . beginHandler—The method that executes when the asynchronous task begins. . endHandler—The method that executes when the asynchronous task ends. . timoutHandler—The method that executes when the asynchronous task runs out of time according to the Page directive’s AsyncTimeout attribute. . state—An arbitrary object that represents state information. . executeInParallel—A Boolean value that indicates whether multiple asynchro- nous tasks should execute at the same time or execute in sequence. You can create multiple PageAsyncTask objects and register them for the same page. When you call the ExecuteRegisteredAsyncTasks() method, all the registered tasks are executed. If an asynchronous task does not complete within the time allotted by the AsyncTimeout attribute, the timoutHandler method executes. For example, the page in Listing 19.35 gives the asynchronous tasks 5 seconds to execute. If the database SELECT command does not return a record within the 5 seconds, the TimeoutData() method executes. It is important to understand that the asynchronous task continues to execute even when the task executes longer than the interval of time specified by the AsyncTimeout attribute. The AsyncTimeout attribute specifies the amount of time that a page is willing to wait before continuing execution. An asynchronous task is not canceled if takes too long. From the Library of Wow! eBook ptg 917 Executing Asynchronous Database Commands 19 The page in Listing 19.35 has tracing enabled and is sprinkled liberally with calls to Trace.Warn() so that you can see when different events happen. The Trace.Warn() state- ments writes out the ID of the current Page thread. The Page thread ID can change between the BeginGetData() and EndGetData() methods (see Figure 19.19). FIGURE 19.19 Trace information fo r a page executed asynchrono usly. You can force the asynchronous task in Listing 19.35 to time out by adding a delay to the database command executed by the AsyncDataLayer.BeginGetMovies() method. For example, the following SELECT statement waits 15 seconds before returning results: WAITFOR DELAY ‘0:0:15’;SELECT Title,Director FROM Movies If you use this modified SELECT statement, the asynchronous task times out and the TimeoutData() method executes. The TimeoutData() method simply displays a message in a Label control. NOTE As an alternative to using the Page.RegisterAsyncTask() method to register an asyn- chronous task, you can use the Page.AddOnPreRenderCompleteAsync() method. However, this latter method does not provide you with as many options. From the Library of Wow! eBook ptg 918 CHAPTER 19 Building Data Access Components with ADO.NET Building Database Objects with the .NET Framework Microsoft SQL Server 2005 and 2008 (including Microsoft SQL Server Express) supports building database objects with .NET Framework. For example, you can create user-defined types, stored procedures, user-defined functions, and triggers written with the Visual Basic .NET or C# programming language. The SQL language is optimized for retrieving database records. However, it is a crazy language that doesn’t look like any other computer language on earth. Doing basic string parsing with SQL, for example, is a painful experience. Doing complex logic in a stored procedure is next to impossible (although many people do it). When you work in the .NET Framework, on the other hand, you have access to thousands of classes. You can perform complex string matching and manipulation by using the Regular expression classes. You can implement business logic, no matter how complex. By taking advantage of the .NET Framework when writing database objects, you no longer have to struggle with the SQL language when implementing your business logic. In this section, you learn how to build both user-defined types and stored procedures by using .NET Framework. Enabling CLR Integration By default, support for building database objects with .NET Framework is disabled. You must enable CLR integration by executing the following SQL Server command: sp_configure ‘clr enabled’, 1 RECONFIGURE When using SQL Express, you can execute these two commands by right-clicking a data- base in the Database Explorer window and selecting the New Query menu option. Enter the following string: sp_configure ‘clr enabled’, 1; RECONFIGURE Select Query Designer, Execute SQL to execute the commands (see Figure 19.20). You receive warnings that the query can’t be parsed, which you can safely ignore. From the Library of Wow! eBook ptg 919 Building Database Objects with the .NET Framework 19 Creating User-Defined Types with .NET Framework You can create a new user-defined type by creating either a .NET class or .NET structure. After you create a user-defined type, you can use it in exactly the same way as the built-in SQL types such as the Int, NVarChar, or Decimal types. For example, you can create a new type and use the type to define a column in a database table. To create a user-defined type with the .NET Framework, you must complete each of the following steps: 1. Create an assembly that contains the new type. 2. Register the assembly with SQL Server. 3. Create a type based on the assembly. We go through each of these steps and walk through the process of creating a new user- defined type. We create a new user-defined type named DBMovie. The DBMovie type repre- sents information about a particular movie. The type includes properties for the Title, Director, and BoxOfficeTotals for the movie. After we create the DBMovie type, we can use the new type to define a column in a data- base table. Next, we write ADO.NET code that inserts and retrieves DBMovie objects from the database. FIGURE 19.20 Executing a database query in Visual Web Developer. From the Library of Wow! eBook ptg 920 CHAPTER 19 Building Data Access Components with ADO.NET Creating the User-Defined Type Assembly You can create a new user-defined type by creating either a class or a structure. We create the DBMovie type by creating a new .NET class. When creating a class that will be used as a user-defined type, you must meet certain requirements: . The class must be decorated with a SqlUserDefinedType attribute. . The class must be able to equal NULL. . The class must be serializable to/from a byte array. . The class must be serializable to/from a string. If you plan to use a class as a user-defined type, you must add the SqlUserDefinedType attribute to the class. This attribute supports the following properties: . Format—Enables you to specify how a user-defined type is serialized in SQL Server. Possible values are Native and UserDefined. . IsByteOrdered—Enables you to cause the user-defined type to be ordered in the same way as its byte representation. . IsFixedLength—Enables you to specify that all instances of this type have the same length. . MaxByteSize—Enables you to specify the maximum size of the user-defined type in bytes. . Name—Enables you to specify a name for the user-defined type. . ValidationMethodName—Enables you to specify the name of a method that is called to verify whether a user-defined type is valid (useful when retrieving a user-defined type from an untrusted source). The most important of these properties is the Format property. You use this property to specify how the user-defined type is serialized. The easiest option is to pick Native. In that case, SQL Server handles all the serialization issues, and you don’t need to perform any additional work. Unfortunately, you can take advantage of native serialization only for simple classes. If your class exposes a nonvalue type property such as a String, you can’t use native serialization. Because the DBMovie class includes a Title and Director property, it’s necessary to use UserDefined serialization. This means that it’s also necessary to implement the IBinarySerialize interface to specify how the class gets serialized. The DBMovie class is contained in Listing 19.36. LISTING 19.36 DBMovie.cs using System; using System.Text; using Microsoft.SqlServer.Server; using System.Data.SqlTypes; From the Library of Wow! eBook ptg 921 Building Database Objects with the .NET Framework 19 using System.Runtime.InteropServices; using System.IO; [SqlUserDefinedType(Format.UserDefined, MaxByteSize = 512, IsByteOrdered = true)] public class DBMovie : INullable, IBinarySerialize { private bool _isNull; private string _title; private string _director; private decimal _boxOfficeTotals; public bool IsNull { get { return _isNull; } } public static DBMovie Null { get { DBMovie movie = new DBMovie(); movie._isNull = true; return movie; } } public string Title { get { return _title; } set { _title = value; } } public string Director { get { return _director; } set { _director = value; } } [SqlFacet(Precision = 38, Scale = 2)] public decimal BoxOfficeTotals { get { return _boxOfficeTotals; } set { _boxOfficeTotals = value; } } From the Library of Wow! eBook ptg 922 CHAPTER 19 Building Data Access Components with ADO.NET [SqlMethod(OnNullCall = false)] public static DBMovie Parse(SqlString s) { if (s.IsNull) return Null; DBMovie movie = new DBMovie(); string[] parts = s.Value.Split(new char[] { ‘,’ }); movie.Title = parts[0]; movie.Director = parts[1]; movie.BoxOfficeTotals = decimal.Parse(parts[2]); return movie; } public override string ToString() { if (this.IsNull) return “NULL”; StringBuilder builder = new StringBuilder(); builder.Append(_title); builder.Append(“,”); builder.Append(_director); builder.Append(“,”); builder.Append(_boxOfficeTotals.ToString()); return builder.ToString(); } public void Write(BinaryWriter w) { w.Write(_title); w.Write(_director); w.Write(_boxOfficeTotals); } public void Read(BinaryReader r) { _title = r.ReadString(); _director = r.ReadString(); _boxOfficeTotals = r.ReadDecimal(); } public DBMovie() { } } From the Library of Wow! eBook ptg 923 Building Database Objects with the .NET Framework 19 The class in Listing 19.36 exposes three properties: the movie Title, Director, and BoxOfficeTotals properties. The BoxOfficeTotals property is decorated with a SqlFacet attribute that indicates the precision and scale of the property value. You must include this attribute if you want to perform SQL queries that use comparison operators with this property. The class in Listing 19.36 also includes both an IsNull and Null property. SQL Server uses a three-valued logic (True,False,Null). All SQL Server types must be nullable. The DBMovie class also includes both a Parse() and a ToString() method. These methods are required for converting the DBMovie class back and forth to a string representation. Finally, the DBMovie class includes both a Write() and Read() method. These methods are required by the IBinarySerialize interface. The Write() method serializes the class. The Read() method deserializes the class. These methods must be implemented because the class uses UserDefined serialization. You need to compile the DBMovie class into a separate assembly (.dll file). After you create (and debug) the class, move the class from your App_Code folder to another folder in your application, such as the root folder. Next, open the Visual Studio 2010 Command prompt and execute the following command: csc /t:library DBMovie.cs This command uses the C# command-line compiler to compile the DBMovie class into an assembly. Registering the User-Defined Type Assembly with SQL Server After you create the assembly that contains your user-defined type, you must register the assembly in SQL Server. You can register the DBMovie assembly by executing the following command in a query window: CREATE ASSEMBLY DBMovie FROM ‘C:\DBMovie.dll’ You need to provide the right path for the DBMovie.dll file on your hard drive. After you complete this step, the assembly is added to Microsoft SQL Server. When using Visual Web Developer, you can see the assembly by expanding the Assemblies folder in the Database Explorer window. Alternatively, you can view a list of all the assemblies installed on SQL Server by executing the following query: SELECT * FROM sys.assemblies You can drop any assembly by executing the DROP Assembly command. For example, the following command removes the DBMovie assembly from SQL Server: DROP Assembly DBMovie From the Library of Wow! eBook . Asynchronous ASP. NET Pages When you take advantage of asynchronous ADO .NET methods, you must also enable asynchronous ASP. NET page execution. You enable an asynchronous ASP. NET page by adding. the process of creating a new user- defined type. We create a new user-defined type named DBMovie. The DBMovie type repre- sents information about a particular movie. The type includes properties. retrieving a user-defined type from an untrusted source). The most important of these properties is the Format property. You use this property to specify how the user-defined type is serialized.