Exposing Proprietary Data

Writing a Simple Data Provider

Functionally, a simple .NET managed data provider is nearly identical to the DirectoryListing class we examined earlier. In terms of the internal structure, though, a significant gap exists between the two. A generic class such as DirectoryListing has no particular requirements, which can make it the major strength or the weakest point of your solution. In contrast, the class that outfits a simple managed data provider must implement the IDataAdapter interface and, subsequently, must expose a precise number of methods and properties.

Let s consider extending the DirectoryListing class so that it works as a data provider. The class will need at least two constructors. The default constructor takes no argument and initializes the internal state of the object in a default way. A second constructor takes one or more arguments and serves as the command text that runs against the data source.

public class FileSystemDataAdapter : IDataAdapter { String m_strPath; DirectoryInfo m_di; // Constructors public FileSystemDataAdapter() { m_strPath = "c:\\"; } public FileSystemDataAdapter(String strPath) { m_strPath = strPath; }  }

The directory path of the second constructor represents the command text. The private member of the DirectoryInfo type represents the connection with the file system through which information is retrieved.

The Table Mapping Mechanism

As mentioned earlier, the IDataAdapter interface comprises the three properties listed in Table 10-2.

Table 10-2 Properties of the IDataAdapter Interface

Property

Description

MissingMappingAction

Indicates how the class should behave when a column has no entry in the specified table of mappings.

MissingSchemaAction

Indicates how the class should behave when the in-memory DataSet object lacks a source table, column, or relationship.

TableMappings

Returns a collection object (which is empty by default) that contains mappings between the column names used by the data source and the column names used by the DataSet object.

As described in Table 10-2, the MissingMappingAction property determines the action that occurs when a column name being selected by the adapter does not have a corresponding entry in the TableMappings collection. After the command terminates, the resultant set of rows and tables must be passed back to the caller packed in a DataSet object. The TableMappings collection determines how each column and table in the freshly obtained result set should be mapped in the DataSet object that the caller will actually get. By default each table in the result set originates a DataTable object with a prefixed name in the DataSet object. The first table takes the name that the caller specified (Table by default), and all other tables are named by adding a progressive index to the name of the first table (by default, Table1, Table2, and so on.) Likewise, each column in each result set originates a column in the table. By specifying a table mapping, you can give each table a name and control which columns are added to the DataSet object. This is helpful if your application needs to manage different user profiles. In this case, you can always run the same query but use different mappings for different categories of users. The MissingMappingAction enumeration defines three feasible actions: an exception is thrown, the column is ignored, or, as happens by default, the column is processed using the original data source name.

The MissingSchemaAction specifies the action to take when a client tries to add data to the DataSet object when the required DataTable or DataColumn object is missing. In this case, by choosing one of the values in the MissingSchemaAction enumeration, you can decide to raise an error, ignore the missing tables and columns, add the necessary tables and columns, or even complete the schema with important information.

Filling the DataSet Object

The IDataAdapter interface comprises the methods listed in Table 10-3.

Table 10-3 Methods of the IDataAdapter Interface

Method

Description

Fill

Adds or refreshes rows in the DataSet object. In your implementation, define the name of the tables used and created.

FillSchema

Adds a DataTable object to a DataSet object and configures the matching schema in the data source.

GetFillParameters

Retrieves the parameters that the user embedded in the command text.

Update

Executes the operations necessary to update the data source.

For simple data formats, you can implement all the methods in Table 10-3, except for Fill, to simply raise the NotSupportedException exception. You also don t need table mapping. The following code is a typical null implementation of a feature you don t provide. Raising an exception is preferable to a no-op implementation.

public int Update(DataSet ds) { throw new NotSupportedException(); }

The implementation of a simple .NET data provider results in a class that has a handful of empty functions and just one working method Fill. The source code for Fill closely resembles the core code of the DirectoryListing class. The IDataAdapter interface requires Fill to have at least one version that returns an integer and takes a DataSet parameter. Of course, you are free to define as many overloads as you need.

public int Fill(DataSet ds, String strTableName) { if (!Directory.Exists(m_strPath)) return -1; m_di = new DirectoryInfo(m_strPath); ds.DataSetName = "DirectoryListing"; DataTable dt = new DataTable(strTableName); ds.Tables.Add(dt); Internal_CreateTable(dt); int nRows = Internal_FillTable(dt); dt.AcceptChanges(); return nRows; } public int Fill(DataSet ds) { return Fill(ds, "FolderItem"); }

The two internal functions used in this listing, Internal_CreateTable and Internal_FillTable, respectively, prepare the structure of the table and loop through subdirectories and files to add rows. Note that you should call AcceptChanges on the DataTable object prior to returning the rows. Doing so does not affect the actual data being returned but it does mark all rows in the table as unchanged. If you do not call AcceptChanges, each row is given a state of Added, which has a serious impact on the subsequent process of detecting changes for the update.

Using the Simple Data Provider

Let s see how to use this data provider. The following code is an excerpt from an ASP.NET page that reads a path from a text box and populates a DataGrid control with the results:

public void OnLoadData(Object sender, EventArgs e) { FileSystemDataAdapter da = new FileSystemDataAdapter(txtCommand.Text); DataSet ds = new DataSet(); int nItems = da.Fill(ds, "MyTable"); if (nItems <0) status.Text = "Directory not found."; else status.Text = String.Format("{0} item(s) found.", nItems); // Display the data grid.DataSource = ds.Tables["MyTable"]; grid.DataBind(); }

You use the FileSystemDataAdapter class in the same way that you take advantage of the SQL Server data adapter in any ADO.NET application. You create a new instance of the class, specifying the arguments required by the adapter s constructor. For simple providers, you allow users to indicate the command text by using the constructor rather than by setting up nonstandard properties or methods. After the adapter is up and running, you call its Fill method and pass a DataSet object to be filled. The Fill method creates and adds a new table that you can then use as any other .NET collection class. Figure 10-9 shows the new data adapter in action.

Figure 10-9

A directory listing retrieved using a simple data provider.

Updating the Data Source

For the FileSystemDataAdapter class, the underlying data source is the file system, in particular the originating folder for any information retrieved. An update has a different meaning depending on the data source. When the data source is a database, the update is inserting or deleting rows from a table. When the data source is the file system, updating might mean changing some attributes of a certain file or directory and even creating new directories or files.

Let s suppose that your data provider supports editing the user comment associated with a particular folder. To simplify our example, let s treat all the other information as read-only. A data adapter object updates the underlying data source by using the Update method. Although you can use a custom method with an ad-hoc signature, for consistency you should use the Update method or, at least, overload it. You should also add extra properties to the adapter unless you can perform the update using a simpler overload.

Detecting the Updates

Although no strict guidelines dictate the behavior of the Update method, you can easily figure the role it plays. The method must loop the whole table and check the state of each row. When a modified row is found, the new information is read and persisted to disk. The Update method is defined in the IDataAdapter interface as follows:

public int Update(DataSet ds)

This prototype is not optimal for our purposes, but changing it causes a compile error. A DataSet object can contain multiple tables, and typically the Update method processes the default table, which has a name of Table. We can manage the method and make Update default to the first table in the DataSet object or to the table with a fixed name you determine. The following code demonstrates the structure of the Update method:

public int Update(DataSet ds) { // Determine the table to work on DataTable dt = ds.Tables[0]; // Initialize the update counter int nUpdatedRows=0; // Loop through the rows in the table foreach(DataRow dr in dt.Rows) { if (dr.RowState == DataRowState.Modified) {  } } return nUpdatedRows; }

Earlier in this chapter, I remarked about the importance of calling AcceptChanges at the end of the Fill method. If you omit that call, during the update process the check against a Modified row state can repeatedly fail even though the row has been effectively updated. This happens because the Added state the state of the row when a row is first added to the table has a higher priority than Modified.

Implementing the Update

The goal of the Update method is to modify the contents of the desktop.ini file in the folder pointed to by the row. The row, however, contains only the name of the folder, not the full path. You solve this problem at the root by changing the implementation of the Fill method so that it returns the full path in the Name column. If you do not change the implementation, the prototype of the Update method lacks an important piece of information the full path to the parent folder, which contains the directories and files listed in the table.

The full path can be made available by using a custom property such as parentFolder, which you set prior to calling Update. To avoid adding new properties, which keeps the overall interface of the adapter more consistent, you can overload the Update method, adding a String argument to denote the name of the parent folder. The code looks like the following:

public int Update(DataSet ds) { // Default to the parent folder set to fill the table if (m_strPath != "") return Update(ds, m_strPath); return 0; } public int Update(DataSet ds, String parentFolder){ // Determine the table to work on DataTable dt = ds.Tables[0]; // Initialize the update counter int nUpdatedRows=0; // Loop through the rows in the table foreach(DataRow dr in dt.Rows) { if (dr.RowState == DataRowState.Modified) { // Files and folders listed in the table // are contained in parentFolder  } } return nUpdatedRows; }

When no parent folder is specified, the adapter defaults to the path used to fill the DataSet object. If this information is unavailable, the Update method fails. If the modified row points to a file instead of a folder, the update doesn t happen. The name of the folder in the table is combined with the parent folder name to get the fully qualified path to desktop.ini. At this point, the update can take place.

String fullDirName = Path.Combine(parentFolder, dr["Name"].ToString()); _WriteDirComment(fullDirName, dr["comment"].ToString());

If the desktop.ini file does not exist, it is created with the following content:

[.ShellClassInfo] InfoTip=...

The code that does this follows:

StreamWriter sw = new StreamWriter(strDesktopIni); sw.WriteLine("[.ShellClassInfo]"); sw.WriteLine("InfoTip=" + newText); sw.Close();

If the desktop.ini file already exists, the _WriteDirComment method first reads its contents and then modifies the string in memory. The file is then replaced with a new one that has the new content. Note that in Windows XP, the desktop.ini file must also be marked as a system file.

A User Interface to Update

To demonstrate the update feature of the file system data adapter object, I reworked one of the sample ASP.NET pages developed in Chapter 4. The new page provides an editable DataGrid control, as shown in Figure 10-10.

Figure 10-10

A user interface for editing the folder user comment.

In the sample page, only the Comment column is editable. After the text is entered, the DataTable object used to populate the grid is updated and the changes are made persistent by the adapter. The following code demonstrates the UpdateCommand event handler invoked by the DataGrid Web control when you save the changes on an editable row:

public void UpdateCommand(Object sender, DataGridCommandEventArgs e) { // Get the new text in the comment textbox (column index is 3) int nColComment = 3; TextBox txtComment = (TextBox) e.Item.Cells[nColComment].Controls[0]; // Get the underlying DataTable FileSystemDataAdapter da = new FileSystemDataAdapter(txtPath.Text); DataSet ds = new DataSet(); da.Fill(ds, "MyTable"); DataTable dt = ds.Tables["MyTable"]; // Select the row being edited (only one element in the array) DataRow[] rg; rg = dt.Select("name='" + grid.DataKeys[grid.EditItemIndex] + "'"); // Perform the update rg[0]["comment"] = txtComment.Text; da.Update(ds, txtPath.Text); }

For the update to take place, you need to retrieve the DataRow object behind the DataGrid row being edited. You can easily do this by using the same techniques we analyzed in Chapter 4, which are based on primary key values. Although the contents of this table (a directory listing) does not come from a database table, you can still use the DataTable object s Select method to locate a range of DataRow objects based on a condition. Likewise, you can use the DataKeyField and DataKeys properties on the DataGrid control to associate each grid row with key values for the underlying table.

In the displayed table of files and directories, the Name column represents a primary key. You cannot have two items (files or folders) with the same name in the same folder. After the correct DataRow object is retrieved and its Comment field updated, you call the data adapter s Update method to persist changes. In Figure 10-11, you can see the grid after the update.

da.Update(ds, txtPath.Text);

Figure 10-11

A user interface displays the updated user comment.

Figure 10-12 shows that the changes were effective because Windows Explorer now reflects them. The full source code for the FileSystemDataAdapter object is in the FileSystemDataAdapter.cs and files available from the companion CD.

Figure 10-12

Changes reflected in Windows Explorer.



Building Web Solutions with ASP. NET and ADO. NET
Building Web Solutions with ASP.Net and ADO.NET
ISBN: 0735615780
EAN: 2147483647
Year: 2002
Pages: 75
Authors: Dino Esposito

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net