Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống
1
/ 26 trang
THÔNG TIN TÀI LIỆU
Thông tin cơ bản
Định dạng
Số trang
26
Dung lượng
606,19 KB
Nội dung
Scripting There are many ways you can make a program extensible at run-time. For example, Chapter 22, “Reflection,” shows how you can provide add-in functionality by loading compiled DLLs at run-time. One of the most flexible methods for extending an application at run-time is scripting. By allowing the program to execute new code at run-time, you can enable it to do just about anything that you could do had you written the code ahead of time, at least in theory. This chapter describes several techniques that you can use to add scripting to your applications. It explains how a program can execute SQL statements or Visual Basic .NET code, and how a pro- gram can parse and evaluate arithmetic expressions at run-time. Scripting Safely Scripting is an extremely flexible and powerful way to add functionality to an application. However, like many other powerful and flexible tools, scripting comes with a certain degree of danger. If you give users the ability to destroy the application or its data, you may very well need to deal with them doing exactly that. Sooner or later you’ll get a call from a customer who has dropped a key table from the application’s database, used a script to set foreground and back- ground colors to black, or removed key objects from the application’s object model, and wants you to make everything better. Even worse, a disgruntled or dishonest user might modify the data to damage the application or for fraudulent reasons, and you may need to sort out the mess. To reduce the frequency of these types of calls, you should consider the users’ needs and program- ming skill level. Then you should carefully select the scripting capabilities that you provide to match. 14_053416 ch09.qxd 1/2/07 6:32 PM Page 237 For example, if the users only need to make configuration changes such as setting colors and options, you can provide configuration dialogs to make that easier and safer. Provide a “Reset to Default” button to let them undo any really bad changes. In this example, scripting isn’t really necessary. Many users understand or can easily learn the basics of SQL. Querying the database is generally safe, so many of the applications I’ve written allow the users to execute ad hoc SQL queries. This gives the user an easy way to find particular records and to generate quick one-off reports without requiring the devel- opers to build new reports into the system. Most users don’t need to be able to perform more dangerous SQL statements that would let them drop tables, add or delete records, and remove indexes. Even with queries, however, there are occasions when an application cannot allow the users unlimited freedom. If the application’s data is sensitive, perhaps containing financial or medical information, all of the users may not be allowed to see all of the data. For example, in a medical application, you might want to allow receptionists to see only appointment and insurance information, and not medical records. In that case, you could not allow the users to write their own SQL queries. Actually, if the database provides security at a sufficiently fine-grained level, you might be able to let users write their own queries. Some databases let you define column-level permissions for particular users. Then, if a user tries to select data that he or she is not allowed to see, the database will raise an error, and the script will fail safely. You can make SQL scripting easier for the users if you provide some means to store and execute prede- fined queries. In some applications I’ve written, supervisors and more experienced users wrote queries for other users to execute. If you only allow the users to execute predefined queries stored in a database or a shared directory that only the supervisors have permission to edit, then you have pretty good con- trol over what the users can do. Another great place to store predefined queries is as stored procedures. That gives you a lot of control over who can create, modify, and view the queries. Stored procedures are saved in the database, so you can modify them without recompiling the application. Only developers who have the proper tools and permissions can view or modify stored procedures, so they are relatively protected. Often, databases can give better performance when executing stored procedures than they can when performing queries com- posed by the application’s code. All in all, stored procedures are a great place to store database tools for the users to execute. If the users are less sophisticated, you may want to build tools that help the user compose queries. The section “Generating Queries” later in this chapter shows one such tool. Users rarely have the skills needed to modify the database safely, so you probably shouldn’t give them the ability to execute non-query SQL commands such as DROP TABLE. That doesn’t mean there’s no point in executing these scripts, however. Sometimes it is useful to give supervisors or application administra- tors tools for performing database maintenance tasks. To protect the database, you may want to allow the users to execute only predefined scripts created by the developers. Stored procedures are a great place to store these maintenance scripts, too. These sorts of database modification tasks can also be extremely useful for developers. Many applica- tions that I’ve worked on modify their data so that you cannot easily perform the same operations repeatedly without resetting the database. A script that inserts test data into the database can make development and testing much easier. 238 Part II: Meta-Development 14_053416 ch09.qxd 1/2/07 6:32 PM Page 238 Scripts that manipulate the application’s object model are usually a bit easier to safeguard than SQL scripts. If you expose an object model that only allows users to do what they can do by using the user interface, you don’t need to worry as much about the user’s permissions. This approach does have some design consequences, however. It means that you cannot rely solely on the user interface to control the user’s access to application features. For example, suppose supervisors should be able to modify work assignments, but clerks should only be able to view them. When a clerk starts the application, you can hide the menu items that allow the user to edit work assignments, but if the object model contains functions to do this, the clerk may be able to use a script to change assign- ments anyway. To prevent this sort of flanking maneuver, the object model’s sensitive methods must always verify the current user’s privileges, even though they might normally be protected by the user interface. If the users don’t have the skills to write their own scripts, or if letting them write scripts is just too dan- gerous, you can still use scripts behind the scenes to provide new functionality. You can store scripts written by supervisors or developers in a database or protected directory, and then allow the program to execute them without the user ever seeing the details. You might provide a Scripts menu that lists the names of the scripts and then executes whichever scripts the user selects. If you store the scripts in a database, you have good control over who can look at them. If you store the scripts in text files, you can encrypt them if you really don’t want the users peeking. As a final word of caution, consider how users might inadvertently or intentionally subvert a script, even if your code actually writes the code. For example, suppose an application stores user names and passwords in the Users table. Many programs use code similar to the following to build this query and verify the user’s password: ‘ Verify the user name and password. Private Sub btnVerify_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles btnVerify.Click ‘ Compose the query. Dim user_name As String = txtUserName.Text Dim password As String = txtPassword.Text Dim query As String = _ “SELECT COUNT(*) FROM Users “ & _ “WHERE UserName = ‘“ & user_name & “‘“ & _ “ AND Password = ‘“ & password & “‘“ Debug.WriteLine(query) ‘ Execute the query. Try m_Connection.Open() Dim cmd As New OleDbCommand(query, m_Connection) Dim result As Integer = CInt(cmd.ExecuteScalar()) m_Connection.Close() If result > 0 Then MessageBox.Show(“User verified”) Else MessageBox.Show(“Invalid user”) End If 239 Chapter 9: Scripting 14_053416 ch09.qxd 1/2/07 6:32 PM Page 239 Catch ex As Exception MessageBox.Show(ex.Message, “Query Error”, _ MessageBoxButtons.OK, MessageBoxIcon.Exclamation) Finally If m_Connection.State = ConnectionState.Open Then m_Connection.Close() End Try End Sub This code creates and executes a query similar to the following: SELECT COUNT(*) FROM Users WHERE UserName = ‘Rod’ AND Password = ‘VbGeek’ If this query returns a value greater than 0, the program assumes the user has a valid password. Now, consider what happens if the user enters the name Rod and the bizarre password shown in the fol- lowing code: ‘ OR True OR Password = ‘ When the code runs, it produces the following enigmatic SQL statement: SELECT COUNT(*) FROM Users WHERE UserName = ‘Rod’ AND Password = ‘’ OR True OR Password = ‘’ This WHERE clause always evaluates to True, so the select statement always returns 1 (assuming there is one record with the user name Rod) and the program accepts the user name even though the password is incorrect. Breaking into a database in this way is called a “SQL injection attack,” and is a fairly common exploit on Web sites. There are even cases where a user can innocently wreak havoc in the SQL query. If the user’s name is O’Toole, for example, the code produces the following query: SELECT COUNT(*) FROM Users WHERE UserName = ‘O’Toole’ AND Password = ‘1337’ Here the extra apostrophe inside the user name confuses the database and the query fails. One solution is to change the code to replace each apostrophe in the input strings with two apostrophes: Dim user_name As String = txtUserName.Text.Replace(“‘“, “‘’”) Dim password As String = txtPassword.Text.Replace(“‘“, “‘’”) Now, the SQL injection attack creates the following SQL query: SELECT COUNT(*) FROM Users WHERE UserName = ‘Rod’ AND Password = ‘’’ OR True OR Password = ‘’’ 240 Part II: Meta-Development 14_053416 ch09.qxd 1/2/07 6:32 PM Page 240 Now, the database correctly reads the weird password entered by the user and is not fooled. The program now creates the following query for user O’Toole: SELECT COUNT(*) FROM Users WHERE UserName = ‘O’’Toole’ AND Password = ‘1337’ The database correctly places an apostrophe inside the user name and there’s no problem. Example program ValidateUser demonstrates both the safe and unsafe version of this code. Scripting is a powerful technique, but it’s not completely without risk. Carefully consider the users’ needs, skill levels, and permissions. Then, you can pick a scripting strategy that gives the most addi- tional flexibility with a reasonable level of risk. Executing SQL Statements Structured Query Language (SQL) is a relatively easy-to-learn language for interacting with databases. It includes commands for creating, reading, editing, and deleting data. SQL also includes commands that manipulate the database itself. Its commands let you create and drop tables, add and remove indexes, and so forth. Visual Studio provides objects that interact with databases by executing SQL statements, so providing scripting capabilities is fairly easy. The following section explains how a program can execute queries written by the user. The section after that shows how a program can provide a tool that makes building queries easier and safer. The final sec- tion dealing with SQL scripts shows how a program can execute more general SQL statements to modify and delete data, and to alter the database’s structure. Note that these sections provide only brief coverage of database programming as it applies to scripting in Visual Basic, and they omit lots of details. For more in-depth coverage of database programming, see a book about database programming in Visual Basic .NET, such as my book Visual Basic .NET Database Programming (Indianapolis: Que, 2002). Running Queries Executing a SQL query is fairly easy in Visual Basic. The following code shows a minimalist approach for executing a query and displaying the results in a DataGridView control: ‘ Open the connection. m_Connection.Open() ‘ Select the data. Dim data_table As New DataTable(“Books”) Dim da As New OleDbDataAdapter(query, m_Connection) ‘ Get the data. 241 Chapter 9: Scripting 14_053416 ch09.qxd 1/2/07 6:32 PM Page 241 da.Fill(data_table) ‘ Display the result. dgvBooks.DataSource = data_table ‘ Close the connection. m_Connection.Close() The code starts by opening the connection object named m_Connection. It creates a DataTable object to hold the selected data, and makes an OleDbDataAdapter to execute the query on the connection. It then uses the adapter to fill the DataTable. The code finishes by setting the DataGridView control’s DataSource property to the DataTable and closing the database connection. Example program UserSqlQuery uses similar code to execute ad hoc queries. It provides some addi- tional error-handling code, and does some extra work to format the DataGridView’s columns (for example, it right-justifies numbers and dates). Download the example to see how the code works. Figure 9-1 shows the program in action. Figure 9-1: Program UserSqlQuery lets the user execute ad hoc SQL queries. The program uses a combo box to let the user enter a query, or select from a list of previously defined queries. When the code successfully executes a query, the program saves it in the combo box’s list and in the Registry so that it will be available to the user later. Program UserSqlQuery also allows the user to select the fields displayed by the DataGridView control. When you select the Data menu’s Select Fields command, the program displays the dialog shown in Figure 9-2. The dialog lists the fields returned by the query, and lets the user select the ones that should be visible in the grid. 242 Part II: Meta-Development 14_053416 ch09.qxd 1/2/07 6:32 PM Page 242 Figure 9-2: Program UserSqlQuery lets the user select the fields it displays in its grid. The following code shows how the Select Fields dialog works: Imports System.Data.OleDb Public Class dlgSelectFields ‘ The DataGridView on the main form. Public TheDataGridView As DataGridView = Nothing ‘ Load the list of database fields. Private Sub dlgSelectFields_Load(ByVal sender As Object, _ ByVal e As System.EventArgs) Handles Me.Load ‘ Make sure TheDataGridView has been initialized. Debug.Assert(TheDataGridView IsNot Nothing, “TheDataGridView is Nothing”) ‘ Set properties. (Done here so it’s easier to find.) clbFields.CheckOnClick = True ‘ Fill the checked list box with the fields. clbFields.Items.Clear() For i As Integer = 0 To TheDataGridView.Columns.Count - 1 clbFields.Items.Add(TheDataGridView.Columns(i).HeaderText) Dim checked As Boolean = TheDataGridView.Columns(i).Visible clbFields.SetItemChecked(i, checked) Next i End Sub ‘ Apply the user’s selections to the DataGridView. Private Sub btnOk_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles btnOk.Click For i As Integer = 0 To TheDataGridView.Columns.Count - 1 TheDataGridView.Columns(i).Visible = clbFields.GetItemChecked(i) Next i End Sub End Class 243 Chapter 9: Scripting 14_053416 ch09.qxd 1/2/07 6:32 PM Page 243 The main program sets the form’s TheDataGridView variable to a reference to the DataGridView con- trol before displaying the form. When the dialog loads, the code loops through the grid’s Columns collection, adding each column’s header text to the dialog’s CheckedListBox control clbFields. It checks an item if the corresponding grid column is currently visible. If the user clicks the OK button, the dialog again loops through grid’s columns, this time setting a col- umn’s Visible property to True if the corresponding item is checked in the dialog’s list box. (You can download this example at www.vb-helper.com/one_on_one.htm.) Generating Queries Ad hoc queries are great for users who know their way around SQL. For many users, however, even simple queries can be intimidating. Example program SelectCriteria uses the dialog shown in Figure 9-3 to let the user specify selection criteria in a simpler manner. The user selects a database field in the left column, picks an operator ( >, >= , IS NULL, LIKE, and so forth) in the middle column, and enters a value string in the right column. Figure 9-3: Program SelectCriteria uses this dialog to build SQL queries. When the user clicks OK, the program uses the selections on the dialog to build a SQL query. The selec- tions shown in Figure 9-3 generate the following SQL statement: SELECT * FROM Books WHERE Title LIKE ‘%Visual Basic’ AND Pages >= 200 AND Rating >= 4 ORDER BY Title The program also saves the criteria in the Registry so that the program can use them the next time it starts. The following code shows how the Set Criteria dialog works: Imports System.Data.OleDb Public Class dlgCriteria ‘ The DataGridView on the main form. 244 Part II: Meta-Development 14_053416 ch09.qxd 1/2/07 6:32 PM Page 244 Public TheDataGridView As DataGridView = Nothing ‘ The collection of criteria. Public TheCriteria As Collection = Nothing ‘ Delimiters for the field choices. Private m_Delimiters As Collection ‘ Load the list of database fields. Private Sub dlgSelectFields_Load(ByVal sender As Object, _ ByVal e As System.EventArgs) Handles Me.Load ‘ Make sure TheDataGridView and TheCriteria have been initialized. Debug.Assert(TheDataGridView IsNot Nothing, “TheDataGridView is Nothing”) Debug.Assert(TheCriteria IsNot Nothing, “TheCriteria is Nothing”) ‘ Get the cell’s template object. Dim dgv_cell As DataGridViewCell = dgvCriteria.Columns(0).CellTemplate Dim combo_cell As DataGridViewComboBoxCell = _ DirectCast(dgv_cell, DataGridViewComboBoxCell) ‘ Make a list of the fields. combo_cell.Items.Clear() m_Delimiters = New Collection For i As Integer = 0 To TheDataGridView.Columns.Count - 1 ‘ Add the name to the combo cell. Dim field_name As String = TheDataGridView.Columns(i).Name combo_cell.Items.Add(field_name) ‘ Get an appropriate delimiter for the data type. Dim delimiter As String = “” If TheDataGridView.Columns(i).ValueType Is GetType(String) Then delimiter = “‘“ ElseIf TheDataGridView.Columns(i).ValueType Is GetType(Date) Then ‘ Note that you need to handle dates differently in SQL Server. delimiter = “#” End If m_Delimiters.Add(delimiter, field_name) Next i ‘ Display current criteria. dgvCriteria.RowCount = TheCriteria.Count + 1 For r As Integer = 0 To TheCriteria.Count - 1 Dim condition As Criterion = DirectCast(TheCriteria(r + 1), Criterion) dgvCriteria.Rows(r).Cells(0).Value = condition.FieldName dgvCriteria.Rows(r).Cells(1).Value = condition.Op dgvCriteria.Rows(r).Cells(2).Value = condition.Value Next r End Sub ‘ Create the new Criteria collection. Private Sub btnOk_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles btnOk.Click ‘ Verify that each row has a field and operator. For r As Integer = 0 To dgvCriteria.RowCount - 2 245 Chapter 9: Scripting 14_053416 ch09.qxd 1/2/07 6:32 PM Page 245 If dgvCriteria.Rows(r).Cells(0).Value Is Nothing Then MessageBox.Show( _ “You must select a field for every row”, _ “Missing Field”, _ MessageBoxButtons.OK, _ MessageBoxIcon.Exclamation) dgvCriteria.CurrentCell = dgvCriteria.Rows(r).Cells(0) dgvCriteria.Select() Me.DialogResult = Windows.Forms.DialogResult.None Exit Sub End If If dgvCriteria.Rows(r).Cells(1).Value Is Nothing Then MessageBox.Show( _ “You must select an operator for every row”, _ “Missing Field”, _ MessageBoxButtons.OK, _ MessageBoxIcon.Exclamation) dgvCriteria.CurrentCell = dgvCriteria.Rows(r).Cells(1) dgvCriteria.Select() Me.DialogResult = Windows.Forms.DialogResult.None Exit Sub End If Next r ‘ Make the new criteria collection. TheCriteria = New Collection For r As Integer = 0 To dgvCriteria.RowCount - 2 Dim field_name As String = _ DirectCast(dgvCriteria.Rows(r).Cells(0).Value, String) Dim delimiter As String = _ DirectCast(m_Delimiters(field_name), String) Dim op As String = _ DirectCast(dgvCriteria.Rows(r).Cells(1).Value, String) Dim value As String = _ DirectCast(dgvCriteria.Rows(r).Cells(2).Value, String) TheCriteria.Add(New Criterion(field_name, op, value, _ delimiter)) Next r End Sub End Class When the dialog loads, it gets the template cell for the dialog’s first grid column. This cell acts as a tem- plate to define other cells in the column so that when the program defines the field names in its drop- down list, it defines the values for every drop-down in this column. The program loops through the main program’s DataGridView columns, adding each column’s name to the drop-down list. It saves a corresponding delimiter (apostrophe for strings, # for dates, an empty string for other data types) for the column in the m_Delimiters collection. The allowed operators ( <, <=, =, >=, >, LIKE, IS NULL, and IS NOT NULL) were set for the second col- umn at design time. 246 Part II: Meta-Development 14_053416 ch09.qxd 1/2/07 6:32 PM Page 246 [...]... script into semi-colon delimited commands Dim commands() As String = Split(txtScript.Text, “;”) ‘ Execute each command Dim results As String = “” For i As Integer = 0 To commands.Length - 1 ‘ Clean up the command to see if it’s non-blank Dim cmd As String = _ commands(i).Replace(vbCr, “ “).Replace(vbLf, “ “).Trim() ‘ Execute only non-blank commands If cmd.Length > 0 Then Debug.WriteLine(commands(i)) ‘ Display... 247 14_053416 ch 09. qxd 1/2/07 6:32 PM Page 248 Part II: Meta -Development The ExecuteNonQuery function shown in the following code executes a SQL command and returns a success or failure message It simply creates an OleDbCommand object associated with the command and the database connection and then executes it ‘ Execute a non-query command and return a success or failure string Public Function ExecuteNonQuery(ByVal... carriage returns and line feeds, and decides whether the command is blank If the command is not blank, the code determines whether the command begins with the SELECT keyword and calls function ExecuteNonQuery or ExecuteQuery as appropriate As it executes each command, it displays the command and its results in an output text box so that the user can see the results as work progresses Figure 9- 4 shows the... previous examples show how an application can execute Visual Basic script code Unfortunately, over the years, Visual Basic has grown less and less basic Although many users have at least some knowledge of Visual Basic for Applications (VBA), few know more than a smattering of Visual Basic NET Many will be immediately lost when they see the Imports, Namespace, and Class statements that the script needs You... causes a divide-by-zero error in the script, and the ExecuteClassCode example program displays the message shown in Figure 9- 5 Figure 9- 5 : If the script throws an error at run-time, the exception’s InnerException property provides useful details Exposing an Object Model The example script in the previous section is fairly simplistic in that it only uses objects provided by Visual Studio and the NET Framework... Scripting Executing Visual Basic Code Programs such as Microsoft’s Word, Excel, and Visual Studio applications allow the user to record, edit, and run macros to automate repetitive tasks Similarly, you can add some scripting support to your applications One approach to this kind of scripting is to use objects in the System.CodeDom and System.Reflection namespaces to load, compile, and run Visual Basic NET script... connection 2 49 14_053416 ch 09. qxd 1/2/07 6:32 PM Page 250 Part II: Meta -Development m_Connection.Close() results &= “Done” & vbCrLf txtResults.Text = results txtResults.Select(results.Length - 1, 10) txtResults.ScrollToCaret() End Sub The code starts by opening a database connection It reads the script in the txtScript text box and splits it into semicolon-delimited commands For each command, the program... several records, and then selects the records The bottom text box shows the results Figure 9- 4 : Program ExecuteSqlScript lets you execute SQL scripts (You can download this example at www.vb-helper.com/one_on_one.htm.) You may never want to give this sort of functionality to users, but you may find it useful during development and testing 250 14_053416 ch 09. qxd 1/2/07 6:32 PM Page 251 Chapter 9: Scripting... String Try ‘ Make and execute the command Dim cmd As New OleDbCommand(txt, conn) cmd.ExecuteNonQuery() Return “> Ok” Catch ex As Exception Return “*** Error executing command ***” & vbCrLf & ex.Message End Try End Function The following code shows a function that executes a query It creates an OleDbCommand object for the query and calls its ExecuteReader command to run the query and get a data reader... this example at www.vb-helper.com/one_on_one.htm.) Running Commands Though executing ad hoc queries is handy for users, developers need more powerful tools Often, it’s handy to execute more general SQL commands that add, modify, or delete data, or that modify the database’s structure Fortunately, Visual Basic s database tools make this relatively straightforward 247 14_053416 ch 09. qxd 1/2/07 6:32 PM . text box and splits it into semicolon-delimited commands. For each command, the program removes carriage returns and line feeds, and decides whether the com- mand is blank. If the command is not. during devel- opment and testing. 250 Part II: Meta -Development 14_053416 ch 09. qxd 1/2/07 6:32 PM Page 250 Executing Visual Basic Code Programs such as Microsoft’s Word, Excel, and Visual Studio. scripting in Visual Basic, and they omit lots of details. For more in-depth coverage of database programming, see a book about database programming in Visual Basic .NET, such as my book Visual Basic