- The Concept of Managed Code Execution
- The Common Language Runtime
- COM+ Component Services
- Using VB.NET to Develop Managed Components
- Serviced Components
- Building VB.NET Serviced Components
- Summary
5.6 Building VB.NET Serviced Components
Now it's time for you to use serviced components to build the supermarket ordering and inventory system outlined in the previous section! You'll work through each step in the process, from designing to coding and then testing the components in an ASP.NET application.
LAB 5-1 An Ordering and Inventory System Made with Serviced Components
STEP 1. Create a new VB.NET class library project.
Open VS.NET and select File'New'Project.
Highlight Class Library and give the new project the name "SupermarketSystem".
STEP 2. Design the components.
Consider the design of your system. First, establish the users of the system and their roles:
Supervisors (adding new products, receiving orders, updating inventory)
Receiving Clerks (receiving orders)
Suppliers (shipping goods to the supermarket and supplying order information)
Organize these functions into a series of classes for your class library. Four classes will handle the business logic of the different duties that each user will perform: Product, ReceiveOrder, UpdateInventory, and CreateOrder. Lab Figure 5-1 shows the classes and the methods of each class.
Lab Figure 5-1 The classes and methods of the Supermarket program
Adding a new product to the system, updating inventory, receiving an order, and creating an order all involve operations on the database tier of the application. For some of these operations, database updates can occur in multiple tables. It's important to keep data integrity across these tables. You also want to implement security features so the users perform only the functions that their roles designate. You'll see this functionality develop as you code the components.
STEP 3. Write code for component functionality.
In the process of implementing the components, we'll discuss these topics:
How to use the System.EnterpriseServices namespace and one particular class: the ServicedComponent class
How to use class attributes to specify the type of COM+ support you want your components to have
How to specify an interface from which to call the components
How to control the transaction inside the components
Create the classes outlined in the specifications in Step 2: Product, ReceiveOrder, UpdateInventory, and CreateOrder. For each of these classes, right-click the project in the Solution Explorer and select Add Class...from the menu. Name the classes as listed above. VS.NET will "stub out" an empty class definition for each class.
Building serviced components requires support from COM+ Component Services. The .NET Framework implements this support through classes in the System.EnterpriseServices namespace. VS.NET doesn't include this reference by default, so you'll need to add it yourself. Select Project'Add Reference from the menu and select System.EnterpriseServices from the list. Click the Select button and then click OK.
Now you're ready to add component functionality. Start by adding the following code for the Product class (Product.vb).
Lab Code Sample 5-1
Imports System Imports System.Reflection Imports System.EnterpriseServices Imports System.Data Imports System.Data.SqlClient <Assembly: ApplicationName("Supermarket")> <Assembly: ApplicationActivation(ActivationOption.Library)> <Assembly: AssemblyKeyFile("KeyFile.snk")> Namespace Supermarket Public Interface IProduct Function Create(ByVal SKU As String, _ ByVal Description As String, _ ByVal UnitPrice As Decimal, _ ByVal StockingBinNumber As String) _ As Boolean End Interface <ConstructionEnabled( _ [Default]:="Default Construction String"), _ Transaction(TransactionOption.Required)> _ Public Class Product Inherits ServicedComponent Implements Supermarket.IProduct Public Sub New() End Sub Protected Overrides Sub Construct( _ ByVal constructString As String) End Sub Function Create(ByVal SKU As String, _ ByVal Description As String, _ ByVal UnitPrice As Decimal, _ ByVal StockingBinNumber As String) _ As Boolean _ Implements Supermarket.IProduct.Create Dim objCnn As SqlConnection Dim objCmd As SqlCommand Dim objParam As SqlParameter Dim intRowsReturned As Integer Try objCnn = New SqlConnection() objCnn.ConnectionString = _ "Initial Catalog=Supermarket;Data Source=localhost;uid=sa;pwd=" objCnn.Open() objCmd = objCnn.CreateCommand() objCmd.CommandText = _ "INSERT INTO Product " & _ "( SKU, Description, UnitPrice, StockingBinNumber ) " & _ "VALUES ( @sku, @description, @unitprice, @stockingbinnumber )" objParam = New SqlParameter() With objParam .ParameterName = "@sku" .SqlDbType = SqlDbType.VarChar .Direction = ParameterDirection.Input .Value = SKU End With objCmd.Parameters.Add(objParam) objParam = New SqlParameter() With objParam .ParameterName = "@description" .SqlDbType = SqlDbType.VarChar .Direction = ParameterDirection.Input .Value = Description End With objCmd.Parameters.Add(objParam) objParam = New SqlParameter() With objParam .ParameterName = "@unitprice" .SqlDbType = SqlDbType.Decimal .Direction = ParameterDirection.Input .Value = UnitPrice End With objCmd.Parameters.Add(objParam) objParam = New SqlParameter() With objParam .ParameterName = "@stockingbinnumber" .SqlDbType = SqlDbType.VarChar .Direction = ParameterDirection.Input .Value = StockingBinNumber End With objCmd.Parameters.Add(objParam) intRowsReturned = _ objCmd.ExecuteNonQuery() Create = True ContextUtil.SetComplete() Catch E As Exception Create = False ContextUtil.SetAbort() Finally objCnn.Close() End Try End Function End Class End Namespace
This code implements a serviced component. As mentioned before, a serviced component is a .NET class that uses COM+ Component Services. The class becomes a serviced component when it derives from the System.EnterpriseServices.ServicedComponent class. Before we talk more about the ServicedComponent class, let's first investigate the beginning of the code where some assembly-level attributes are declared.
Making a serviced component requires you to provide information to COM+ Component Services about the component's configuration. First you designate to which package, or COM+ application, the component will belong. That designation is made with the ApplicationName assembly-level attribute shown in line . The name for the order and inventory application is "Supermarket". COM+ applications are listed in the Component Services Console.
COM+ Component Services provides a runtime environment for assembly components. You can also control where the components are activatedin the same process as the creator of the object (IIS) or in a separate system process (dllhost.exe). You can control application activation by specifying the ApplicationActivation assembly attribute shown in line . This code specifies ActivationOption.Library, which causes components to be activated in the creator's process. So, if you were running these components inside ASP.NET Web Forms, the creating process would be the Web server, IIS. The ActivationOption.Server option provides faster performance; this option, which runs the component in a system process, provides more isolation, so the component's execution won't adversely affect the execution of IIS. One advantage to using ActivationOption.Library is to make debugging easier.
The final assembly-level attribute, AssemblyKeyFile, specifies a shared name for the assembly (see line ). The shared name is sometimes referred to as a strong name. A shared name ensures that a name assigned to a component is unique. This is accomplished by using a public/private cryptographic key pair to sign each shared component in the assembly. The public key is then published with the assembly. Besides specifying this attribute in the code, you'll need to actually generate the key file specified for the assembly-level attribute. To do this, use sn.exe, the Strong Name Utility. Lab Figure 5-2 shows how this is done.
Lab Figure 5-2 Specifying a strong name for the assembly using sn.exe
The usage of sn.exe, as shown in Lab Figure 5-2, outputs a .snk file. This is the file name that you specify in the AssemblyKeyFile attribute in the code (change the path to the location of your generated key file). You must complete this step before you compile your project. These assembly-level attributes appear once in the project's code. The class file in which they appear is irrelevant, but they must be declared once and only once in the assembly code.
Warning
Never let anybody else have access to your .snk file. It contains a private key that is for your use only.
Let's move on to the main part of the component code. Line defines the Supermarket namespace, which will contain all the components for the Supermarket application. Line declares an interface you'll use in the client application (the ASP.NET Web Form) to call the component. The interface has one function, Create(), which you'll use to set up a new product in the Product database table.
The component's class definition comes next. The code beginning in line shows two class-level attributes for the Product class. The first one, ConstructionEnabled, specifies that the class will be able to use COM+ constructor strings. A constructor string is a string passed into the activation procedure of the component. This string can be specified by a system administrator inside the Component Services Console. A constructor string can contain any information, but typically you'll use it for passing initialization information to the component. If no constructor string is given for the component in the Component Services Console (see Lab Figure 5-3, which shows the Component Services Console dialog box for setting a constructor string), a default constructor string is used by specifying it in the attribute as shown in the continuation of line . The other class-level attribute specifies how the class will participate in transactions. Use the Transaction attribute to specify the participation level. In this code, Transaction uses TransactionOption.Required. This indicates that the Product component should participate in an existing transaction if one already exists. If no transaction exists, a new transaction will begin and the Product component will execute within the boundaries of that transaction.
Lab Figure 5-3 Specifying a constructor string in Component Services
The class definition continues with the specification of an inherited class and an Implements statement. Since this will be a serviced component, it needs to derive from the ServicedComponent class, as shown in line . In line the class implements the IProduct interface specified earlier.
Line provides implementation support for constructor strings. Since the code specified that the component will have support for constructor strings with the class-level attribute ConstructionEnabled, here there is an override method for the Construct() sub. This method will be called upon object construction, and the constructor string assigned to the component will be available in the constructString variable.
Now follows the implementation of the Create() method, which accepts a new product's SKU number, product description, unit price, and stocking bin number. The Create() method then executes the appropriate ADO.NET code to insert a new row into the Product database table. (ADO.NET code will be discussed in Chapter 7. For now, just be aware that the Product component contains this method that will be callable from an ASP.NET Web Form.)
Although the discussion of the ADO.NET code details is deferred, it's important to point out two details in the Create() method. These are two methods of the ContextUtil object, SetComplete() in line and SetAbort() in line . These methods cast a vote in the current transaction. Typically, when you have verified that all of the code inside a particular component method has executed successfully, a call to SetComplete() is made. This tells COM+ Component Services that a unit of work has succeeded and that database integrity and consistency is assured for updates made by the unit of work. It's a signal that the transaction can continue running. Conversely, if a failure occurs (an exception or other user-defined error or condition), the program needs to cast a "fail" vote for the transaction by calling SetAbort(). This will cause COM+ Component Services to stop the transaction and roll back any changes made to the database by previous steps in the transaction.
Now that you understand the basic pieces of the Product class code, add the code for the remaining classes (CreateOrder, UpdateInventory, and ReceiveOrder). The implementations are all different, of course, but they follow pretty much the same conventions as the Product component.
Lab Code Sample 5-2
Imports System Imports System.Reflection Imports System.EnterpriseServices Imports System.Data Imports System.Data.SqlClient Namespace Supermarket Public Interface ICreateOrder Function Create(ByVal OrderNumber As String, _ ByVal SupplierName As String) _ As Boolean Function AddItems(ByVal OrderNumber As String, _ ByVal SKU As String, _ ByVal Quantity As Integer) _ As Boolean End Interface <ConstructionEnabled( _ [Default]:="Default Construction String"), _ Transaction(TransactionOption.Required)> _ Public Class CreateOrder Inherits ServicedComponent Implements ICreateOrder Public Sub New() End Sub Public Function Create(ByVal OrderNumber As String, _ ByVal SupplierName As String) _ As Boolean _ Implements ICreateOrder.Create Dim objCnn As SqlConnection Dim objCmd As SqlCommand Dim objParam As SqlParameter Dim intRowsAffected As Integer Try objCnn = New SqlConnection() objCnn.ConnectionString = _ "Initial Catalog=Supermarket;Data Source=localhost;uid=sa;pwd=" objCnn.Open() objCmd = objCnn.CreateCommand() objCmd.CommandText = _ "INSERT INTO Orders " & _ "( OrderNumber, SupplierName, OrderReceived ) " & _ "VALUES ( @OrderNumber, @SupplierName, @OrderReceived )" objParam = New SqlParameter() With objParam .ParameterName = "@OrderNumber" .SqlDbType = SqlDbType.VarChar .Direction = ParameterDirection.Input .Value = OrderNumber End With objCmd.Parameters.Add(objParam) objParam = New SqlParameter() With objParam .ParameterName = "@SupplierName" .SqlDbType = SqlDbType.VarChar .Direction = ParameterDirection.Input .Value = SupplierName End With objCmd.Parameters.Add(objParam) objParam = New SqlParameter() With objParam .ParameterName = "@OrderReceived" .SqlDbType = SqlDbType.Bit .Direction = ParameterDirection.Input .Value = False End With objCmd.Parameters.Add(objParam) intRowsAffected = objCmd.ExecuteNonQuery() Create = True ContextUtil.SetComplete() Catch E As Exception Create = False ContextUtil.SetAbort() End Try End Function Public Function AddItems(ByVal OrderNumber As String, _ ByVal SKU As String, _ ByVal Quantity As Integer) _ As Boolean _ Implements ICreateOrder.AddItems Dim objCnn As SqlConnection Dim objCmd As SqlCommand Dim objParam As SqlParameter Dim intMaxLineNumber As Integer Dim intRowsAffected As Integer Dim objTemp As Object Try objCnn = New SqlConnection() objCnn.ConnectionString = _ "Initial Catalog=Supermarket;Data Source=localhost;uid=sa;pwd=" objCnn.Open() objCmd = objCnn.CreateCommand() objCmd.CommandText = _ "SELECT MAX( LineItemNumber ) " & _ "FROM OrderDetails " & _ "WHERE OrderNumber = @OrderNumber" objParam = New SqlParameter() With objParam .ParameterName = "@OrderNumber" .SqlDbType = SqlDbType.VarChar .Direction = ParameterDirection.Input .Value = OrderNumber End With objCmd.Parameters.Add(objParam) objTemp = objCmd.ExecuteScalar() If TypeOf objTemp Is DBNull Then intMaxLineNumber = 1 Else intMaxLineNumber = CType(objTemp, Integer) intMaxLineNumber += 1 End If objCmd = objCnn.CreateCommand() objCmd.CommandText = _ "INSERT INTO OrderDetails " & _ "( OrderNumber, LineItemNumber, SKU, " & _ "QuantityReceived, Quantity ) VALUES " & _ "( @OrderNumber, @LineNumber, @SKU, " & _ "@QuantityReceived, @Quantity )" objParam = New SqlParameter() With objParam .ParameterName = "@OrderNumber" .SqlDbType = SqlDbType.VarChar .Direction = ParameterDirection.Input .Value = OrderNumber End With objCmd.Parameters.Add(objParam) objParam = New SqlParameter() With objParam .ParameterName = "@LineNumber" .SqlDbType = SqlDbType.Int .Direction = ParameterDirection.Input .Value = intMaxLineNumber End With objCmd.Parameters.Add(objParam) objParam = New SqlParameter() With objParam .ParameterName = "@SKU" .SqlDbType = SqlDbType.VarChar .Direction = ParameterDirection.Input .Value = SKU End With objCmd.Parameters.Add(objParam) objParam = New SqlParameter() With objParam .ParameterName = "@QuantityReceived" .SqlDbType = SqlDbType.Int .Direction = ParameterDirection.Input .Value = 0 End With objCmd.Parameters.Add(objParam) objParam = New SqlParameter() With objParam .ParameterName = "@Quantity" .SqlDbType = SqlDbType.Int .Direction = ParameterDirection.Input .Value = Quantity End With objCmd.Parameters.Add(objParam) intRowsAffected = objCmd.ExecuteNonQuery() AddItems = True ContextUtil.SetComplete() Catch E As Exception AddItems = False ContextUtil.SetAbort() Finally objCnn.Close() End Try End Function End Class End Namespace
Lab Code Sample 5-3
Imports System Imports System.Reflection Imports System.EnterpriseServices Imports System.Data Imports System.Data.SqlClient Namespace Supermarket Public Interface IUpdateInventory Function Update(ByVal BinNumber As String, _ ByVal SKU As String, _ ByVal Quantity As Integer) _ As Boolean Function GetStockingLocation(ByVal SKU As String) _ As String End Interface <ConstructionEnabled( _ [Default]:="Default Construction String"), _ Transaction(TransactionOption.Required)> _ Public Class UpdateInventory Inherits ServicedComponent Implements IUpdateInventory Public Sub New() End Sub Public Function GetStockingLocation( _ ByVal SKU As String) As String _ Implements IUpdateInventory.GetStockingLocation Dim objCnn As SqlConnection Dim objCmd As SqlCommand Dim objParam As SqlParameter Dim objTemp As Object Try objCnn = New SqlConnection() objCnn.ConnectionString = _ "Initial Catalog=Supermarket;Data Source=localhost;uid=sa;pwd=" objCnn.Open() objCmd = objCnn.CreateCommand() objCmd.CommandText = _ "SELECT StockingBinNumber " & _ "FROM Product WHERE SKU = @SKU" objParam = New SqlParameter() With objParam .ParameterName = "@SKU" .SqlDbType = SqlDbType.VarChar .Direction = ParameterDirection.Input .Value = SKU End With objCmd.Parameters.Add(objParam) objTemp = objCmd.ExecuteScalar() If TypeOf objTemp Is DBNull Then GetStockingLocation = "" Else GetStockingLocation = _ CType(objCmd.ExecuteScalar(), String) End If ContextUtil.SetComplete() Catch E As Exception ContextUtil.SetAbort() GetStockingLocation = "" Finally objCnn.Close() End Try End Function Private Function InventoryRecExists( _ ByVal SKU As String, _ ByVal StockingBinNumber As String) _ As Boolean Dim objCnn As SqlConnection Dim objCmd As SqlCommand Dim objParam As SqlParameter Dim intRowCount As Integer Dim objTemp As Object Try objCnn = New SqlConnection() objCnn.ConnectionString = _ "Initial Catalog=Supermarket;Data Source=localhost;uid=sa;pwd=" objCnn.Open() objCmd = objCnn.CreateCommand() objCmd.CommandText = _ "SELECT COUNT(*) FROM Inventory " & _ "WHERE SKU = @SKU AND " & _ "BinNumber = @StockingBinNumber" objParam = New SqlParameter() With objParam .ParameterName = "@SKU" .SqlDbType = SqlDbType.VarChar .Direction = ParameterDirection.Input .Value = SKU End With objCmd.Parameters.Add(objParam) objParam = New SqlParameter() With objParam .ParameterName = "@StockingBinNumber" .SqlDbType = SqlDbType.VarChar .Direction = ParameterDirection.Input .Value = StockingBinNumber End With objCmd.Parameters.Add(objParam) objTemp = objCmd.ExecuteScalar() If TypeOf objTemp Is DBNull Then intRowCount = 0 Else intRowCount = CType(objTemp, Integer) End If If intRowCount > 0 Then InventoryRecExists = True Else InventoryRecExists = False End If ContextUtil.SetComplete() Catch E As Exception InventoryRecExists = False ContextUtil.SetAbort() Finally objCnn.Close() End Try End Function Private Sub UpdateInventoryRecord( _ ByVal BinNumber As String, _ ByVal SKU As String, _ ByVal Quantity As Integer) Dim objCnn As SqlConnection Dim objCmd As SqlCommand Dim objParam As SqlParameter Dim intRowCount As Integer Try objCnn = New SqlConnection() objCnn.ConnectionString = _ "Initial Catalog=Supermarket;Data Source=localhost;uid=sa;pwd=" objCnn.Open() objCmd = objCnn.CreateCommand() objCmd.CommandText = "UPDATE Inventory " & _ "SET Quantity = Quantity + @Quantity " & _ "WHERE BinNumber = @BinNumber AND SKU = @SKU" objParam = New SqlParameter() With objParam .ParameterName = "@Quantity" .SqlDbType = SqlDbType.Int .Direction = ParameterDirection.Input .Value = Quantity End With objCmd.Parameters.Add(objParam) objParam = New SqlParameter() With objParam .ParameterName = "@BinNumber" .SqlDbType = SqlDbType.VarChar .Direction = ParameterDirection.Input .Value = BinNumber End With objCmd.Parameters.Add(objParam) objParam = New SqlParameter() With objParam .ParameterName = "@SKU" .SqlDbType = SqlDbType.VarChar .Direction = ParameterDirection.Input .Value = SKU End With objCmd.Parameters.Add(objParam) intRowCount = objCmd.ExecuteNonQuery() ContextUtil.SetComplete() Catch E As Exception ContextUtil.SetAbort() Finally objCnn.Close() End Try End Sub Private Sub InsertInventoryRecord( _ ByVal BinNumber As String, _ ByVal SKU As String, _ ByVal Quantity As Integer) Dim objCnn As SqlConnection Dim objCmd As SqlCommand Dim objParam As SqlParameter Dim intRowCount As Integer Try objCnn = New SqlConnection() objCnn.ConnectionString = _ "Initial Catalog=Supermarket;Data Source=localhost;uid=sa;pwd=" objCnn.Open() objCmd = objCnn.CreateCommand() objCmd.CommandText = _ "INSERT INTO Inventory " & _ "( BinNumber, SKU, Quantity ) VALUES " & _ "( @BinNumber, @SKU, @Quantity )" objParam = New SqlParameter() With objParam .ParameterName = "@BinNumber" .SqlDbType = SqlDbType.VarChar .Direction = ParameterDirection.Input .Value = BinNumber End With objCmd.Parameters.Add(objParam) objParam = New SqlParameter() With objParam .ParameterName = "@SKU" .SqlDbType = SqlDbType.VarChar .Direction = ParameterDirection.Input .Value = SKU End With objCmd.Parameters.Add(objParam) objParam = New SqlParameter() With objParam .ParameterName = "@Quantity" .SqlDbType = SqlDbType.Int .Direction = ParameterDirection.Input .Value = Quantity End With objCmd.Parameters.Add(objParam) intRowCount = objCmd.ExecuteNonQuery() ContextUtil.SetComplete() Catch E As Exception ContextUtil.SetAbort() Finally objCnn.Close() End Try End Sub Public Function Update(ByVal BinNumber As String, _ ByVal SKU As String, _ ByVal Quantity As Integer) _ As Boolean _ Implements IUpdateInventory.Update Dim objCnn As SqlConnection Dim objCmd As SqlCommand Dim objParam As SqlParameter Dim strStockingLocation As String Dim intRowsAffected As Integer Try If InventoryRecExists(SKU, BinNumber) Then UpdateInventoryRecord( _ BinNumber, _ SKU, _ Quantity) Else InsertInventoryRecord( _ BinNumber, _ SKU, _ Quantity) End If Update = True ContextUtil.SetComplete() Catch E As Exception Update = False ContextUtil.SetAbort() End Try End Function End Class End Namespace
Lab Code Sample 5-4
Imports System Imports System.Reflection Imports System.EnterpriseServices Imports System.Data Imports System.Data.SqlClient Namespace Supermarket Public Interface IReceiveOrder Function GetNextLineItem(ByVal OrderNumber As String, _ ByRef SKU As String) As Integer Function Receive(ByVal OrderNumber As String, _ ByVal SKU As String, _ ByVal LineNumber As Integer, _ ByVal QuantityReceived As Integer) _ As Boolean End Interface <ConstructionEnabled( _ [Default]:="Default Construction String"), _ Transaction(TransactionOption.Required)> _ Public Class ReceiveOrder Inherits ServicedComponent Implements IReceiveOrder Public Sub New() End Sub Private Sub UpdateOrderDeatils( _ ByVal OrderNumber As String, _ ByVal LineNumber As Integer, _ ByVal QuantityReceived As Integer) Dim objCnn As SqlConnection Dim objCmd As SqlCommand Dim objParam As SqlParameter Dim objSQLDr As SqlDataReader Dim intRowsAffected As Integer Try objCnn = New SqlConnection() objCnn.ConnectionString = _ "Initial Catalog=Supermarket;Data Source=localhost;uid=sa;pwd=" objCnn.Open() objCmd = objCnn.CreateCommand() objCmd.CommandText = _ "UPDATE OrderDetails " & _ "SET QuantityReceived = " & _ "QuantityReceived + @QuantityReceived " & _ "WHERE OrderNumber = " & _ "@OrderNumber AND LineItemNumber = @LineNumber" objParam = New SqlParameter() With objParam .ParameterName = "@QuantityReceived" .SqlDbType = SqlDbType.Int .Direction = ParameterDirection.Input .Value = QuantityReceived End With objCmd.Parameters.Add(objParam) objParam = New SqlParameter() With objParam .ParameterName = "@OrderNumber" .SqlDbType = SqlDbType.VarChar .Direction = ParameterDirection.Input .Value = OrderNumber End With objCmd.Parameters.Add(objParam) objParam = New SqlParameter() With objParam .ParameterName = "@LineNumber" .SqlDbType = SqlDbType.Int .Direction = ParameterDirection.Input .Value = LineNumber End With objCmd.Parameters.Add(objParam) intRowsAffected = objCmd.ExecuteNonQuery() ContextUtil.SetComplete() Catch E As Exception ContextUtil.SetAbort() Finally objCnn.Close() End Try End Sub Public Function GetNextLineItem( _ ByVal OrderNumber As String, _ ByRef SKU As String) _ As Integer Implements _ IReceiveOrder.GetNextLineItem Dim objCnn As SqlConnection Dim objCmd As SqlCommand Dim objParam As SqlParameter Dim objSQLDr As SqlDataReader Try objCnn = New SqlConnection() objCnn.ConnectionString = _ "Initial Catalog=Supermarket;Data Source=localhost;uid=sa;pwd=" objCnn.Open() objCmd = objCnn.CreateCommand() objCmd.CommandText = _ "SELECT MAX(LineItemNumber), " & _ "SKU FROM OrderDetails od, Orders o " & _ "WHERE od.OrderNumber = @OrderNumber AND " & _ "o.OrderReceived = 0 AND " & _ "o.OrderNumber = od.OrderNumber GROUP BY SKU" objParam = New SqlParameter() With objParam .ParameterName = "@OrderNumber" .SqlDbType = SqlDbType.VarChar .Direction = ParameterDirection.Input .Value = OrderNumber End With objCmd.Parameters.Add(objParam) objSQLDr = objCmd.ExecuteReader() objSQLDr.Read() If Not objSQLDr.IsDBNull(0) Then GetNextLineItem = objSQLDr.GetInt32(0) SKU = objSQLDr.GetString(1) Else GetNextLineItem = -1 SKU = "" End If ContextUtil.SetComplete() objSQLDr.Close() Catch E As Exception GetNextLineItem = -1 SKU = "" ContextUtil.SetAbort() Finally objCnn.Close() End Try End Function Public Function Receive(ByVal OrderNumber As String, _ ByVal SKU As String, _ ByVal LineNumber As Integer, _ ByVal QuantityReceived As Integer) _ As Boolean _ Implements IReceiveOrder.Receive Dim objCnn As SqlConnection Dim objCmd As SqlCommand Dim objParam As SqlParameter Dim objInvUpdate As IUpdateInventory Dim strBinNumber As String Try UpdateOrderDeatils(OrderNumber, _ LineNumber, _ QuantityReceived) objInvUpdate = New UpdateInventory() strBinNumber = _ objInvUpdate.GetStockingLocation(SKU) If objInvUpdate.Update(strBinNumber, _ SKU, QuantityReceived) Then Receive = True ContextUtil.SetComplete() Else Receive = False ContextUtil.SetAbort() End If Catch E As Exception Receive = False ContextUtil.SetAbort() End Try End Function End Class End Namespace
Now that you've entered the main code for the Supermarket COM+ application, it's time to compile it. Choose Build'Build Solution from the menu to create the assembly file. In the next step, you'll create a project that references that assembly.
STEP 4. Create the ASP.NET application.
Create a new VB ASP.NET project called "SupermarketWeb".
Add a reference to System.EnterpriseServices to the project by right-clicking the project icon and selecting Add Reference... from the pop-up menu.
Add four new Web Forms (.aspx files) and their code-behind files (.vb files) to the project, named as follows:
Lcs5-5.aspx, Lcs5-5.aspx.vb Lcs5-6.aspx, Lcs5-6.aspx.vb Lcs5-7.aspx, Lcs5-7.aspx.vb Lcs5-8.aspx, Lcs5-8.aspx.vb
These Web Forms will test the different components and their functionality.
Product Component Test Web Form
The HTML for the Lcs5-5.aspx file appears below.
Lab Code Sample 5-5
<%@ Page Language="vb" AutoEventWireup="false" src="Lcs5-05.aspx.vb" Inherits="WebForm1" Transaction="RequiresNew"%> <html> <head> <title>Create New Product</title> </head> <body> <form id="Form1" method="post" runat="server"> <p>SKU: <asp:TextBox id=txtSKU runat="server"> </asp:TextBox></p> <p>Description: <asp:TextBox id=txtDescription runat="server"> </asp:TextBox></p> <p>Unit Price: <asp:TextBox id=txtUnitPrice runat="server"> </asp:TextBox></p> <p>Stocking Location: <asp:TextBox id=txtStockLoc runat="server"> </asp:TextBox></p> <p> <asp:Button id=cmdAddProduct runat="server" Text="Add Product"> </asp:Button> <asp:CompareValidator id=CompareValidator1 runat="server" ErrorMessage="You must enter a price (number)" Type="Double" ControlToValidate="txtUnitPrice" Operator="DataTypeCheck"> </asp:CompareValidator></p> </form> </body> </html>
ASP.NET Web Forms can run inside the context of a COM+ Component Services transaction. In this ASP.NET Web application, the Web Forms call the serviced components in response to events raised by Web Controls (buttons clicked and so on). Since the ASP.NET Web Form is the initiator of calls made into a COM+ application, the Web Form is considered the root of the transaction. It has the "final vote" as to whether or not the transaction succeeds or fails.
In order to designate that the Web Form will participate in a transaction, you need to set the Transaction page attribute (highlighted in bold above). This code specifies the level as RequiresNew. This means that the page will always begin a new transaction for any units of work executed during the lifetime of the page.
Here is the code-behind file, Lcs5-5.aspx.vb, for the preceding Web Form.
Imports SupermarketSystem.Supermarket Imports System.EnterpriseServices Imports System.Reflection Imports System.ComponentModel Public Class WebForm1 Inherits System.Web.UI.Page Protected WithEvents cmdAddProduct As _ System.Web.UI.WebControls.Button Protected WithEvents txtSKU As _ System.Web.UI.WebControls.TextBox Protected WithEvents txtDescription As _ System.Web.UI.WebControls.TextBox Protected WithEvents txtUnitPrice As _ System.Web.UI.WebControls.TextBox Protected WithEvents txtStockLoc As _ System.Web.UI.WebControls.TextBox Protected WithEvents CompareValidator1 As_ System.Web.UI.WebControls.CompareValidator Private Sub Page_Load(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles MyBase.Load End Sub Private Sub cmdAddProduct_Click( _ ByVal sender As System.Object, _ ByVal e As System.EventArgs) _ Handles cmdAddProduct.Click Dim objProduct As IProduct Dim bln As Boolean objProduct = New Product() bln = objProduct.Create(txtSKU.Text, _ txtDescription.Text, _ CDec(txtUnitPrice.Text), _ txtStockLoc.Text) If bln Then ContextUtil.SetComplete() Else ContextUtil.SetAbort() End If End Sub End Class
The Click event for the cmdAddProduct button calls the Product component to add a new product to the database. The code creates a new Product object and obtains a reference to the IProduct interface. It then calls the Create() method. If the call was successful (returned True), SetComplete() is called to indicate to COM+ that this unit of work in the transaction was successful. If not, SetAbort()stops the transaction immediately.
CreateOrder Component Test Web Form
The code for the Lcs5-6.aspx file follows below. Note that again the Transaction page attribute is specified and set to RequiresNew.
Lab Code Sample 5-6
<%@ Page Language="vb" AutoEventWireup="false" src="Lcs5-06.aspx.vb" Inherits="SupermarketCreateOrder" Transaction="RequiresNew"%> <html> <head> <title>Create New Order</title> </head> <body> <form id="Form1" method="post" runat="server"> <p>Order number: <asp:TextBox id=txtOrderNumber runat="server"> </asp:TextBox></p> <p>Supplier Name: <asp:TextBox id=txtSupplierName runat="server"> </asp:TextBox></p> <p> <asp:Button id=cmdCreateOrder runat="server" Text="Add"> </asp:Button></p> </form> </body> </html>
The code-behind file, Lcs5-6.aspx.vb, contains the following code.
Imports System.EnterpriseServices Imports System.Reflection Imports SupermarketSystem.Supermarket Public Class SupermarketCreateOrder Inherits System.Web.UI.Page Protected WithEvents txtOrderNumber As _ System.Web.UI.WebControls.TextBox Protected WithEvents txtSupplierName As _ System.Web.UI.WebControls.TextBox Protected WithEvents cmdCreateOrder As _ System.Web.UI.WebControls.Button Private Sub Page_Load(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles MyBase.Load End Sub Private Sub cmdCreateOrder_Click( _ ByVal sender As System.Object, _ ByVal e As System.EventArgs) _ Handles cmdCreateOrder.Click Dim objCreateOrder As ICreateOrder objCreateOrder = New CreateOrder() If objCreateOrder.Create(txtOrderNumber.Text, _ txtSupplierName.Text) Then ContextUtil.SetComplete() Else ContextUtil.SetAbort() End If End Sub End Class
Similar to the test Web Form for the Product component, this code creates a CreateOrder object using New. The program calls the Create() method and then calls SetComplete() or SetAbort() upon success or failure, respectively.
AddToOrder Test Web Form
The HTML code for the AddToOrder Web Form (Lcs5-7.aspx) appears below.
Lab Code Sample 5-7
<%@ Page Language="vb" AutoEventWireup="false" Codebehind="SupermarketAddToOrder.aspx.vb" Inherits="SupermarketWeb.SupermarketAddToOrder" Transaction="RequiresNew"%> <html> <head> <title>Add To Order</title> </head> <body> <form id="Form1" method="post" runat="server"> <p>Order Number: <asp:TextBox id=txtOrderNumber runat="server"> </asp:TextBox></p> <p>SKU: <asp:TextBox id=txtSKU runat="server"> </asp:TextBox></p> <p>Quantity: <asp:TextBox id=txtQuantity runat="server"> </asp:TextBox></p> <p> <asp:CompareValidator id=CompareValidator1 runat="server" ErrorMessage="Quantity must be a whole number!" ControlToValidate="txtQuantity" Type="Integer" Operator="DataTypeCheck"> </asp:CompareValidator></p> <p> <asp:Button id=cmdAddToOrder runat="server" Text="Add To Order"> </asp:Button></p> </form> </body> </html>
Here is the associated code-behind file (Lcs5-7.aspx.vb). AddItems() is a method of the CreateOrder component, so the code is very similar to the CreateOrder Web Form.
Imports System.EnterpriseServices Imports System.Reflection Imports SupermarketSystem.Supermarket Public Class SupermarketAddToOrder Inherits System.Web.UI.Page Protected WithEvents txtOrderNumber As _ System.Web.UI.WebControls.TextBox Protected WithEvents txtSKU As _ System.Web.UI.WebControls.TextBox Protected WithEvents txtQuantity As _ System.Web.UI.WebControls.TextBox Protected WithEvents CompareValidator1 As _ System.Web.UI.WebControls.CompareValidator Protected WithEvents cmdAddToOrder As _ System.Web.UI.WebControls.Button Private Sub Page_Load(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles MyBase.Load End Sub Private Sub cmdAddToOrder_Click( _ ByVal sender As System.Object, _ ByVal e As System.EventArgs) _ Handles cmdAddToOrder.Click Dim objCreateOrder As ICreateOrder objCreateOrder = New CreateOrder() If objCreateOrder.AddItems(txtOrderNumber.Text, _ txtSKU.Text, CInt(txtQuantity.Text)) Then ContextUtil.SetComplete() Else ContextUtil.SetAbort() End If End Sub End Class
ReceiveOrder Component Test Web Form
Finally, here is the code (Lcs5-8.aspx) for the Web Form that will receive items for an order.
Lab Code Sample 5-8
<%@ Page Language="vb" AutoEventWireup="false" src="SupermarketReceiveOrder.aspx.vb" Inherits="SupermarketReceiveOrder" Transaction="RequiresNew"%> <html> <head> <title>Receive Order</title> </head> <body> <form id="Form1" method="post" runat="server"> <p>Order Number to Receive: <asp:TextBox id=txtOrderToReceive runat="server"> </asp:TextBox> <asp:Button id=cmdGetOrder runat="server" Text="Get Order"> </asp:Button></p> <p> <asp:Panel id=Panel1 runat="server" Width="399px" Height="144px" Enabled="False"> <p> <asp:Label id=lblOrderNumber runat="server" Width="184px" Height="19px"> </asp:Label></p> <p></p><p> <asp:Label id=lblReceiveSKU runat="server" Width="183px" Height="19px"> </asp:Label></p> <p> <asp:Label id=lblLineNumberReceive runat="server" Width="188px" Height="19px"> </asp:Label></p> <p> <asp:Label id=Label1 runat="server" Width="128px" Height="19px"> Quantity To Receive: </asp:Label> <asp:TextBox id=txtQuantityToReceive runat="server"> </asp:TextBox> <asp:Button id=cmdReceive runat="server" Text="Receive"> </asp:Button> </asp:Panel></p> </form> </body> </html>
This Web Form wraps page elements in a Panel Web Control. The panel is initially disabled to avoid displaying or enabling the order information until a valid order number is keyed into the Web Form. Here's the code-behind file (Lcs5-8.aspx.vb).
Imports System.EnterpriseServices Imports System.Reflection Imports SupermarketSystem.Supermarket Public Class SupermarketReceiveOrder Inherits System.Web.UI.Page Protected WithEvents cmdGetOrder As _ System.Web.UI.WebControls.Button Protected WithEvents Panel1 As _ System.Web.UI.WebControls.Panel Protected WithEvents lblOrderNumber As _ System.Web.UI.WebControls.Label Protected WithEvents Label1 As _ System.Web.UI.WebControls.Label Protected WithEvents txtOrderToReceive As _ System.Web.UI.WebControls.TextBox Protected WithEvents cmdReceive As _ System.Web.UI.WebControls.Button Protected WithEvents lblReceiveSKU As _ System.Web.UI.WebControls.Label Protected WithEvents lblLineNumberReceive As _ System.Web.UI.WebControls.Label Protected WithEvents txtQuantityToReceive As _ System.Web.UI.WebControls.TextBox Private Sub Page_Load(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles MyBase.Load End Sub Private Sub cmdGetOrder_Click( _ ByVal sender As System.Object, _ ByVal e As System.EventArgs) _ Handles cmdGetOrder.Click Dim objReceiveOrder As IReceiveOrder Dim intLineNumber As Integer Dim strSKUToReceive As String objReceiveOrder = New ReceiveOrder() intLineNumber = _ objReceiveOrder.GetNextLineItem( _ txtOrderToReceive.Text, _ strSKUToReceive) If intLineNumber <> -1 Then ViewState("OrderToReceive") = txtOrderToReceive.Text ViewState("SKUToReceive") = strSKUToReceive ViewState("LineNumber") = intLineNumber Panel1.Enabled = True lblLineNumberReceive.Text = _ "Line Number: " & intLineNumber lblOrderNumber.Text = _ "Order Number: " & txtOrderToReceive.Text lblReceiveSKU.Text = "SKU: " & strSKUToReceive Else Panel1.Enabled = False End If End Sub Private Sub cmdReceive_Click( _ ByVal sender As System.Object, _ ByVal e As System.EventArgs) _ Handles cmdReceive.Click Dim objReceiveOrder As IReceiveOrder objReceiveOrder = New ReceiveOrder() If objReceiveOrder.Receive( _ ViewState("OrderToReceive"), _ ViewState("SKUToReceive"), _ ViewState("LineNumber"), _ CInt(txtQuantityToReceive.Text)) Then ContextUtil.SetComplete() Else ContextUtil.SetAbort() End If End Sub End Class
This form uses two Button Web Controls, cmdGetOrder and cmdReceive. This makes a two-step process for receiving items for an order. First, the event handler for cmdGetOrder calls GetNextLineItem(), taking as input the order number the user entered. If there is a line item to receive for the order, the program displays the line-item information in the Label Web Control contained within the Panel Web Control. The Panel Web Control is then enabled, making the information visible to the user. The line-item information is also copied to the ViewState StateBag because the program will need this information on the subsequent post-back that will occur when items are received.
The event handler for cmdReceive calls the Receive() method. The Receive() method updates the OrderDetails table as well as the Inventory table. Using a transaction in this situation helps point out any discrepancies between quantities in inventory and quantities ordered. The Receive() method returns True on success and False on failure, and the program makes an appropriate call to either SetComplete() or SetAbort() as a result.
Now you need to add a reference to the assembly DLL for the components. Right-click the References folder in the Solution Explorer, select Add Reference, browse to the compiled DLL, and select it. Click OK to add the reference to the selected assembly.
STEP 5. Run the application.
Now you're ready to build the application. Select Build'Build Solution from the menu. Select a start page for the application (like Lcs5-5.aspx) by right-clicking on a Web Form in the Solution Explorer and selecting Set As Start Page from the menu.
Run the application by selecting Debug'Start Without Debugging.
Test the application by entering some products. Then create a new order, add some items to the order, and run an order-receive process. This should complete a full test.
Something important happened when you first called a component's method inside the assembly. The system performed what is known as a lazy registration. A lazy registration automatically places the components in the assembly into a new COM+ application in the COM+ Component Services Console. It does this based on the assembly-level attributes specified in the component assembly code. Lab Figure 54 shows the Supermarket COM+ application in the COM+ Component Services Console.
Lab Figure 5-4 The Supermarket application within the COM+ Component Services Console
STEP 6. Add role-based security.
One of the requirements for the application is that only certain categories of users should be allowed to run certain components. To enable role-based security for the Supermarket COM+ application, right-click on the Supermarket COM+ application icon and select Properties. Click the Security tab. Check the boxes and radio buttons as shown in Lab Figure 5-5.
Lab Figure 5-5 Enabling role-based security for the Supermarket application
Now you can set up the user roles (Receiving Clerks, Supervisors, and Suppliers) inside the COM+ Component Services Console. For each role, right-click the Roles folder under the Supermarket application (see Lab Figure 5-6), select New'Role from the menu, and assign a name for the role.
Lab Figure 5-6 Adding roles for the Supermarket application
Assign users to each role by right-clicking the User folder under the role and selecting New'User from the menu. Pick a Windows account name(s) or group(s) to assign to the role. Lab Figure 5-7 shows users assigned to the various roles of the Supermarket application.
Lab Figure 5-7 Assigning users to roles
Now you need to assign each role to a component. Right-click a component in the COM+ application and select Properties from the menu. Click the Security tab. Check Enforce component level access checks, and then check the roles you wish to assign to the component, as shown in Lab Figure 5-8.
Lab Figure 5-8 Assigning roles to components
STEP 7. Test the role-based security.
A convenient way to test the role-based security is to call the components from a console application and run the console application in the security context of a specific user. Normally, when you run an application from the console, Windows uses the security context of the currently logged-on user. By using the runas command, you can run an executable program using any account name (provided, of course, you have the password of that account!). Here's a simple console application you can run to test role-based security.
Imports System Imports SupermarketSystem.Supermarket Imports System.EnterpriseServices Module Module1 Sub Main() Dim objCreateOrder As ICreateOrder objCreateOrder = New CreateOrder() objCreateOrder.Create("834957239-1", "My Supplier") End Sub End Module
Compile this small testing console application and run it using the runas command below:
runas /user:rmdevbiz01\matt roletester.exe
Given the role configuration of the CreateOrder component, the username "matt" should be the only user allowed to run the application. Assuming that the user account "matt" has appropriate Windows permissions set to run the program, it should execute without errors.
You can also perform a negative test by rerunning the program with an account name not included in the Supplier role.