Using ADO.NET to Access Custom Data Sources


Out-of-the-box ADO.NET is great, and when used properly it is easy to use and powerful. However, you can get even more out of this technology if you’re prepared to put in some work. One possibility is to expose all sorts of data, not just data stored in databases, via ADO.NET. You can do this by creating your own data provider and exposing whatever data you want in a standardized ADO.NET way.

Custom Providers

We’ll look at an example of a custom ADO.NET data provider that exposes files and directory information on your local network (in the FileSystemDataProvider project in the chapter’s sample code) and allows files to be downloaded as byte arrays. To keep things simple, we’ll implement only read functionality and look only at the names of files and directories, but the principles you’ll learn along the way will also apply when you have bigger needs. More important, you’ll see how to create a custom data provider for any data source.

FileSystemDataProvider

To create an assembly that can be called a basic data provider, you must implement certain interfaces, all found in the System.Data namespace. Table 9-1 shows these interfaces, along with the classes that implement them in the FileSystemDataProvider library.

Table 9-1: FileSystemDataProvider Implementation of Data-Provider Interfaces

Interface

Description

Implementing Class in FileSystemDataProvider

IDbConnection

Data source connection

FSConnection

IDbCommand

Command or query for manipulating your data source

FSCommand

IDataReader

Forward-only, read access to data

FSDataReader

IDataAdapter

The link between data and a DataSet object

FSDataAdapter

In addition, if you want to use parameterized commands, you must implement the interfaces listed in Table 9-2.

Table 9-2: Data-Provider Interfaces for Parameterized-Command Support

Interface

Description

Implementing Class in FileSystemDataProvider

IDataParameter

Single parameter

N/A

IDataParameterCollection

Collection of parameters

N/A

The two interfaces listed in Table 9-2 aren’t implemented in FileSystemDataProvider, although it wouldn’t be much of a chore to add them. We’ve omitted them in the sample code purely for space considerations.

In addition to the four classes listed in Table 9-1, FileSystemDataProvider also includes the types listed in Table 9-3.

Table 9-3: FileSystemDataProvider Types

Type

Description

FSException

Provider-specific exception class that inherits from DataException. Functions as a standard exception class, with no custom properties.

FSInfo

Intermediate class for storing file or directory information. Has Name and Type properties. Name is a string property, and Type is a FSInfoType property. (See below.)

FSInfoType

Enumeration of possible FSInfo types: FSInfoType.File and FSInfoType.Directory.

We won’t look at the code for these types in this chapter because they’re so simple.

You can write data providers in a way that allows data sources to be queried using standard SQL syntax, although that might mean writing an awful lot of code. To keep things simple, FileSystemDataProvider understands the commands listed in Table 9-4.

Table 9-4: FileSystemDataProvider Commands

Command

Description

GetDirectory [path]

Obtains the contents (files and directories) of the connection- configured root directory or a subdirectory indicated by the optional path parameter. To restrict the portion of the directory that can be examined, this command doesn’t recognize .. in path.

GetFile path

Obtains information about one or more files matching path; wildcards are permitted.

In addition, the FSDataReader class allows the current file to be obtained in byte form.

FSConnection

The FSConnection class, as noted earlier, is responsible for maintaining a connection to a data source, in this case the file system. This is achieved via an instance of the System.IO.DirectoryInfo class.

In ADO.NET, connections are usually initialized with a connection string; we’ll keep this concept. The connection string in this case is a path to a directory, which is what the DirectoryInfo instance will point to. Also, connections can be either open or closed; data access is permitted only when the connection is open. Although the DirectoryInfo class has no analogue to the “open” and “closed” states, we’ll implement our own open/closed states for the purposes of consistency.

We’ll need to implement plenty of members of the IDbConnection interface, but several of them relate to functionality we don’t need, including transactional support (two versions of BeginTransaction), changing databases and getting database names (ChangeDatabase and the Database property), and using a timeout period (ConnectionTimeout). We’ll set these members to do nothing or raise a NotSupportedException, since they don’t really apply in the context of this example or the technology we are using to achieve our results.

We’ll walk through the rest of the code, starting with the private members for holding state information, and two constructors:

public class FSConnection : IDbConnection {     private ConnectionState connectionState;     private String connectionString;     private DirectoryInfo dirInfo;     public FSConnection() : this("")     {     }     public FSConnection(string newConnectionString)     {         connectionState = ConnectionState.Closed;         connectionString = newConnectionString;     }

The connectionState and connectionString members are accessible via public properties; the ConnectionState property is read only, and the ConnectionString property can be modified only if the connection is closed:

   public System.Data.ConnectionState State    {        get        {            return connectionState;        }    }    public string ConnectionString    {        get        {            return connectionString;        }        set        {            if (connectionState == ConnectionState.Closed)            {                connectionString = value;            }            else            {                throw new FSException("Cannot set the connection string unless"                    + " the connection is closed.");            }        }    }

The private member, dirInfo, isn’t publicly accessible, but it must be available to other classes in the assembly, so we’ll make it internal:

    internal DirectoryInfo DirInfo     {         get         {             return dirInfo;         }     }

Next we have Open and Close methods for controlling the connection; Open instantiates the dirInfo member, and Close clears it:

    public void Open()     {         try         {             dirInfo = new DirectoryInfo(connectionString);             connectionState = ConnectionState.Open;         }         catch         {             throw new FSException("Cannot open connection.");         }     }     public void Close()     {         dirInfo = null;         connectionState = ConnectionState.Closed;     }

Once a connection is “open,” other classes in the assembly can use the DirInfo property to access files and use ChangeDirectory to change the private dirInfo member:

    internal void ChangeDirectory(string newDir)     {         if (newDir.IndexOf("..") != -1)         {             throw new FSException("Cannot use '..'.");          }         if (connectionState == ConnectionState.Closed)         {             throw new FSException("Connection is closed.");         }         dirInfo = new DirectoryInfo(newDir);      }

Finally, we have two methods for creating commands, both of which return FSCommand instances:

    public IDbCommand CreateCommand()     {         FSCommand newCommand = new FSCommand();         newCommand.Connection = this;         return newCommand;     }     public FSCommand CreateCommand(String commandText)     {         FSCommand newCommand = new FSCommand(commandText);         newCommand.Connection = this;         return newCommand;     }     ...other members omitted... }

FSCommand

Next we come to the class responsible for executing commands against the connection, FSCommand.

Once again, there are several IDbCommand members that we won’t implement. They include the Transaction property, the Cancel method, and the Prepare method (for transactional support); the ExecuteScalar and ExecuteNonQuery methods and the CommandTimeout property; and the CreateParameter method (for parameter support). There are also two members that return fixed values and don’t allow other values to be set: CommandType, which is always CommandType.Text, and UpdateRowSource, which determines mapping between returned parameters and the DataSet object and is always UpdateRowSource.None.

The code for this class starts with the private state members we require and two simple constructors:

public class FSCommand : IDbCommand {     private FSConnection connection;     private String commandText;     public FSCommand(): this("GetDirectory")     {     }     public FSCommand(String newCommandText)     {         commandText = newCommandText;     }

We also have two simple property accessors, Connection and CommandText, to access the private members, although there is no real need to show these here.

The real work of this class is done in the ExecuteReader method. The code for this method starts by ensuring that the command is in a position to execute by checking for an open connection:

public IDataReader ExecuteReader(System.Data.CommandBehavior behavior) {     if (connection == null)     {         throw new FSException ("No connection associated with this command.");     }     if (connection.State != ConnectionState.Open)     {         throw new FSException("Connection not open.");     }

Next we analyze the command string by using a regular expression to split the command (GetDirectory or GetFile) from the parameter used (if any). We throw an exception if no command exists, and we get the command and parameter if it does. We also declare an FSDataReader variable for returning the reader result of the command:

    Regex pattern =         .new Regex("\\s*(?<Command>\\S*)\\s*(?<Parameter>\\S*)\\s*",             RegexOptions.IgnoreCase);     Match splitString = pattern.Match(commandText);     if (!splitString.Success)     {         throw new FSException("Must supply a command.");     }     string cmdCommand = splitString.Groups["Command"].Value;     string cmdParameter = splitString.Groups["Parameter"].Value;     FSDataReader reader = null;

Next we check the command type and take action accordingly. If the command is GetDirectory, we change the directory using FSConnection.ChangeDirectory if necessary, and then we get the directory information using DirectoryInfo.GetFileSystemInfos, passing the result to the Data property of FSDataReader. (See the next section.)

    switch (cmdCommand.ToLower())     {         case "getdirectory" :             try             {                 if (cmdParameter != "")                 {                     connection.ChangeDirectory(                         connection.DirInfo.FullName + cmdParameter);                 }                 reader = new FSDataReader(connection);                 reader.Data = connection.DirInfo.GetFileSystemInfos();             }             catch (System.IO.IOException)             {                 throw new FSException("No such directory.");             }             catch (System.ArgumentException)             {                 throw new FSException("Bad directory name.");             }             break;

The GetFile command requires a parameter (filename), so we throw an exception if nothing is supplied. If we do have a parameter, we split the path info from the filename, change the directory to the correct location, and execute the command, getting a reader in the same way as for GetDirectory:

        case "getfile" :             try             {                 if (cmdParameter == "")                 {                     throw new FSException("Must specify a filename.");                 }                 if (cmdParameter.IndexOf('\\') != -1)                 {                     connection.ChangeDirectory(                         connection.DirInfo.FullName                              + cmdParameter.Substring(0,                                  cmdParameter.LastIndexOf('\\') + 1));                 }                 reader = new FSDataReader(connection);                 reader.Data = connection.DirInfo.GetFiles(                     cmdParameter.Substring(                         cmdParameter.LastIndexOf('\\') + 1));                 if (reader.RecordsAffected == 0)                 {                     throw new FSException("No files found.");                 }             }             catch (System.IO.IOException)             {                 throw new FSException("No such directory.");             }             catch (System.ArgumentException)             {                 throw new FSException("Bad directory name.");             }             break;         default:             throw new FSException("Unknown command.");     }     return reader; }

There is also another version of ExecuteReader that works without needing a value for the CommandBehavior parameter (which we don’t use anyway, so we just pass a CommandBehavior.Default value to the version of this method shown above), and that completes the code for this class:

    IDataReader System.Data.IDbCommand.ExecuteReader()     {         return ExecuteReader(CommandBehavior.Default);     }     ...other members omitted... }

FSDataReader

Now we come to the class that allows us read access to the data returned by FSCommand execution, FSDataReader. As usual, the code starts with private state-holding members and two constructors (both internal because this class cannot be created independently):

public class FSDataReader : IDataReader {     private FileSystemInfo[] data;     private FileSystemInfo currentData;     private FSInfo currentDataInfo;     private int index;     private FSConnection connection;     private bool isClosed;     private bool readMethodCalled;     internal FSDataReader()     {         isClosed = true;         readMethodCalled = false;         index = 0;     }     internal FSDataReader(FSConnection connectionRef)     {         connection = connectionRef;         isClosed = false;         readMethodCalled = false;         index = 0;     }

The state members are

  • data A FileSystemInfo array containing directory members, initialized by FSCommand via the internal property Data

  • currentData The FileSystemInfo object representing the current file or directory

  • currentDataInfo An FSInfo representation of currentData

  • index The index of the current record within the data

  • connection A reference to the associated connection

  • isClosed A Boolean value indicating whether the reader is closed

  • readMethodCalled A Boolean value indicating whether Read has been called, which is necessary to initialize data

The most important method in this class is Read (from IDataReader), which advances the current data through the FileSystemInfo array containing all the file and directory objects returned by FSCommand. First we check whether the reader is open, which it will be if it was initialized with a connection; if there is any data to examine, we read the data. We assign the current FileSystemInfo object to currentData. Then we check to see whether the record being examined is a directory by checking the type of the FileSystemInfo object to see if it is actually a DirectoryInfo instance. We then initialize the currentDataInfo member accordingly. (FSInfo requires this information.)

    public bool Read()     {         if (!isClosed)         {             if (data != null)             {                 if (index < data.Length)                 {                     currentData = data[index];                     if (currentData.GetType() == typeof(DirectoryInfo))                     {                         currentDataInfo = new FSInfo(currentData.Name,                                                      FSInfoType.Directory);                     }                     else                     {                         currentDataInfo = new FSInfo(currentData.Name,                                                      FSInfoType.File);                     }

Next we advance the index so it’s ready for the next time Read is called, set readMethodCalled to true, and return a true result. Alternatively, if no data is found, a false value is returned so the client knows to stop reading data.

                    index++;                     readMethodCalled = true;                     return true;                 }                 else                 {                     return false;                 }             }

Finally, we have exception-throwing code in case the reader isn’t properly initialized:

            else             {                 throw new FSException("No data loaded.");             }         }         else         {             throw new FSException("Reader is closed.");         }     }

The rest of the IDataReader members (and the IDataRecord members, which must be implemented if we implement IDataReader) are concerned with getting information about the current record or are concerned with the data in general, and none of them concern us that much. Some have been implemented, and others haven’t, based on whether they are applicable to FSInfo data. (Most of them aren’t, such as GetGuid.)

The data can be examined using members of these interfaces that are implemented, but we would expect clients of this class to use the custom methods GetFSInfo (to access the current file info) and GetFile (which returns a byte of the current file). GetFSInfo simply returns currentDataInfo as appropriate (that is, if the reader is properly initialized):

    public FSInfo GetFSInfo()     {         if (!isClosed)         {             if (readMethodCalled)             {                 return currentDataInfo;             }             else             {                 throw new FSException("Must call Read() to initialize data.");             }         }         else         {             throw new FSException("Reader is closed.");         }     }

GetFile loads the current file into a byte array using file stream classes and returns it as follows:

    public byte[] GetFile()     {         if (!isClosed)         {             if (readMethodCalled && currentDataInfo.Type == FSInfoType.File)             {                 FileInfo currentFile = currentData as FileInfo;                 FileStream stream = currentFile.OpenRead();                 BinaryReader reader = new BinaryReader(stream);                 byte[] fileData = new byte[stream.Length];                 for (long currentByte = 0; currentByte < stream.Length;                     currentByte++)                 {                     fileData[currentByte] = reader.ReadByte();                 }                 reader.Close();                 stream.Close();                 return fileData;             }             else             {                 throw new FSException("Must call Read() to initialize data.");             }         }         else         {             throw new FSException("Reader is closed.");         }     }     ...other members omitted... }

The code so far is all we need to access our data source. However, to make it a proper data provider, we must provide a way to obtain data in DataSet form, which requires FSDataAdapter.

FSDataAdapter

FSDataAdapter has only one private state member (and an associated public property accessor, which we won’t look at here):

public class FSDataAdapter : IDataAdapter {     private FSCommand fillCommand;

This member holds the command used to fill a DataSet with results. Typically, DataAdapter classes also have commands for data modification, but we’re creating a read-only provider here, so we don’t need to worry about them.

Next we have some constructors, which require varying numbers of parameters and supply the usual .NET data-provider versatility in how classes are used:

    public FSDataAdapter() : this(new FSCommand())     {     }     public FSDataAdapter(FSCommand newFillCommand)     {         fillCommand = newFillCommand;     }     public FSDataAdapter(FSCommand newFillCommand,          FSConnection newConnection)     {         fillCommand = newFillCommand;         fillCommand.Connection = newConnection;     }

Next we come to the IDataAdapter members. Most of these aren’t required for this data provider. TableMappings, which determines what happens if unmapped data raises errors, simply returns null. MissingSchemaAction, which determines whether schema information is added to the DataSet even if no data is present, is always MissingSchemaAction.Add (which means that schema information is added). MissingMapping action is always MissingMappingAction.Passthrough, which means that unmapped data (if any) is added to the DataSet without complaint. Update, which is used by the adapter to make changes to a data source based on DiffGram information, isn’t required, and it throws a NotSupportedException.

None of this, though, affects the real meat of this class, which is the FillSchema and Fill methods. They are responsible for setting the DataSet object’s schema information and filling the DataSet object with data, respectively.

FillSchema sets the schema information by creating a new table, setting column information (the filename is used as a primary key—filenames are unique within a directory by definition, after all), and adding the table to the DataSet object as a new table named FSInfo. Note that we always use this table name for adding to the DataSet, so clients never have to supply one when Fill is called.

    public DataTable[] FillSchema(DataSet dataSet,                                   System.Data.SchemaType schemaType)     {         DataTable[] tables = new DataTable[1];         tables[0] = new DataTable("FSInfo");         tables[0].Columns.Add("Name", typeof(String));         tables[0].Columns.Add("Type", typeof(String));         DataColumn[] primaryKeys = new DataColumn[1];         primaryKeys[0] = tables[0].Columns["Name"];         tables[0].PrimaryKey = primaryKeys;         if (dataSet.Tables.Contains("FSInfo"))         {             dataSet.Tables.Remove("FSInfo");         }         dataSet.Tables.Add(tables[0]);         return tables;     }

Fill simply uses the stored fillCommand command to read data and stores it into the FSInfo table initialized in FillSchema:

    public int Fill(DataSet dataSet)     {         FillSchema(dataSet, SchemaType.Mapped);         DataTable FSInfoTable = dataSet.Tables["FSInfo"];         FSDataReader reader = fillCommand.ExecuteReader(             CommandBehavior.Default) as FSDataReader;         FSInfo inf = null;         while (reader.Read())         {             inf = reader.GetFSInfo();             DataRow newRow = FSInfoTable.NewRow();             newRow["Name"] = inf.Name;             newRow["Type"] = inf.Type.ToString();             FSInfoTable.Rows.Add(newRow);         }         dataSet.AcceptChanges();         return 0;     }     ...other members omitted... }

And that completes our data provider.

A Windows Client for FileSystemDataProvider

Before exposing file system data via a Web service, we should check that everything is working by using a simple Windows application. This is good practice and makes the code easier to debug—plus, we can make the application use a Web service rather than using the provider directly with relatively few modifications.

We won’t look at how to create this application because it uses standard Windows Forms techniques; you can view the code at your leisure in the files that come with this book. We will, however, examine the data access–specific parts and discuss the basic concepts behind the application. Figure 9-2 shows the application in action.

click to expand
Figure 9-2: A Windows Forms client application for FileSystemDataProvider

The user can choose to execute a GetDirectory or a GetFile command and optionally supply a parameter. The DataGrid display will show the files using a DataSet object obtained using an FSDataAdapter object configured with the command entered.

In addition to allowing you to enter commands manually, the application allows you to select and download files by using the Download Selected button. This button uses a GetFile command and then calls the GetFile method of the FSDataReader class to get the file data, and the data can then be saved by using a SaveFileDialog object.

Note

Because this is a local windows application, you aren’t really downloading files at all, you’re just copying them from one place to another. However, we’ll be modifying this application to use a Web service shortly, where the Download File button makes more sense.

Feedback is also supplied in the Current Directory and Status labels, which show which directory is being examined (information that is also used when files are downloaded) and what operations the application carries out, respectively.

Note that the root directory of the data is hardcoded into the application, in the constructor. Although this isn’t strictly necessary, it fits in with the design methodology for the data provider. The idea is to expose a selection of files and directories below a common root and not allow .. to be used, to avoid exposing other data. When we tie the provider into a Web service shortly, we will hardcode this root directory into the Web service, but for now (because this information isn’t appropriate inside the provider itself) we’ll set it in the client application.

In addition to the rootPath private state member, the application has a private state variable called results, which stores the DataSet filled by FSDataAdapter. The rest of the code in the application is contained in the event handlers for the two buttons.

Let’s first look at executeButton_click, which is executed when the user clicks Execute Command. We start by initializing the connection:

    private void executeButton_Click(object sender, System.EventArgs e)     {         try         {             FSConnection conn = new FSConnection(rootPath);             FSCommand cmd = null;

We then check for the command type selected and use this to construct a FSCommand instance:

            if (getDirectoryButton.Checked)             {                 cmd = conn.CreateCommand("GetDirectory "                            + parameterTextBox.Text);             }             else             {                 cmd = conn.CreateCommand("GetFile "                            + parameterTextBox.Text);             }

Next we use the command to create an FSDataAdapter, use this adapter to fill results, bind the data to the DataGrid, and set the status labels:

            FSDataAdapter da = new FSDataAdapter(cmd, conn);             results = new DataSet();             conn.Open();             da.Fill(results);             conn.Close();             resultGrid.DataSource = results;             resultGrid.DataMember = "FSInfo";             statusLabel.Text = "Command Executed OK.";             if (getDirectoryButton.Checked)             {                 directoryLabel.Text = parameterTextBox.Text;             }             else             {                 directoryLabel.Text = parameterTextBox.Text.Substring(0,                     parameterTextBox.Text.LastIndexOf('\\') + 1);             }         }

If anything goes wrong in the code, we’ll receive an FSException (never a NotSupportedException because we aren’t using the unimplemented members). If this occurs, we can use the text to set the status and abort:

        catch (FSException ex)         {             results = null;             resultGrid.DataSource = null;             statusLabel.Text = ex.Message;         }     }

Next we have downloadButton_Click, which is called when the user clicks the Download File button. This code checks for results and for a selected file (not directory) before creating and executing a command to initialize an FSDataReader. Note also that the current directory information is taken from the directoryLabel label, which is written to when GetDirectory is executed:

    private void downloadButton_Click(object sender, System.EventArgs e)     {         try         {             if (results != null && resultGrid.CurrentRowIndex != -1)             {                 DataRow selectedRow =                 results.Tables["FSInfo"].Rows[resultGrid.CurrentRowIndex];                 String rowName = selectedRow.ItemArray[0] as String;                 String rowType = selectedRow.ItemArray[1] as String;                 if (rowType == "Directory")                 {                     statusLabel.Text = "Cannot download whole directories.";                     return;                 }                 FSConnection conn = new FSConnection(rootPath);                 FSCommand cmd = conn.CreateCommand("GetFile "                      + directoryLabel.Text                     + "\\" + rowName);                 conn.Open();                 FSDataReader reader = cmd.ExecuteReader(                     CommandBehavior.Default) as FSDataReader;

Downloading the file involves checking for the presence of the file using FSDataReader.Read (in case the directory listing has changed for some reason), prompting the user for a file path to save to, and then saving the file using standard streaming code:

            if (reader.Read())             {                 byte[] fileData = reader.GetFile();                 saveFileDialog1.FileName = rowName;                 if (saveFileDialog1.ShowDialog() == DialogResult.OK)                 {                     FileInfo newFile = new FileInfo(                         saveFileDialog1.FileName);                     FileStream fs = newFile.Create();                     BinaryWriter writer = new BinaryWriter(fs);                     writer.Write(fileData);                     writer.Flush();                     writer.Close();                     statusLabel.Text = "File downloaded successfully.";                 }             }             else             {                 statusLabel.Text = "File not available.";             }

Finally, we have code to close the connection and we have exception- handling code—which is very much like the exception-handling code in executeButton_click:

            conn.Close();         }     }     catch (FSException ex)     {         statusLabel.Text = ex.Message;     } }

To use the application, you simply set the directory in the constructor to a directory on your computer (or on another computer with a drive letter configured) and play away. Some sample data is included in the code for this chapter. Figure 9-3 shows a download in action.

click to expand
Figure 9-3: The FileSystemDataProvider client application downloading a file

Using FileSystemDataProvider via a Web Service

Now that we’ve done most of the legwork, it’s time to put some of the concepts we’ve covered into practice and look at a Web service for accessing FileSystemDataProvider. We’ll do this by modifying the Windows client from the previous section to use a Web service called FSDPWebService.asmx.

We’re using a “fat” client, which understands DataSet objects, so we’ll pass the data as a DataSet object. Furthermore, because we can and because it illustrates an earlier point, we’ll pass the data as a typed DataSet object. Now, you might wonder why we didn’t use a typed DataSet object directly in FileSystemDataProvider. There are three reasons for this. First, we had to conform to the interface signature for IDataAdapter.Fill, which uses a regular DataSet object. (We could have added a custom method for this, however, so it doesn’t really matter.) Second, DataAdapter objects are supposed to work with any DataSet object. We can include FSInfo data in any DataSet object, even one that stores other types of data at the same time. Again, though, we could have provided an alternative. Third, and perhaps most important, we wanted our typed DataSet object to be used with user interface components, and the data provider sits firmly in the business layer. Making the typed DataSet type visible to a business layer isn’t a problem, but making it visible through the business layer to user interface components can lead into tricky waters.

Instead, we introduced the typed DataSet object in the business layer, making it visible to the user interface layer. This doesn’t cause a problem when it comes to accessing the data layer because, as noted earlier, DataReader objects work with any DataSet objects—including, of course, objects that inherit from DataSet, which is exactly what a typed DataSet object is.

Let’s start, then, by looking at this typed DataSet class, called FSDataSet, which is in the FSDPWebServices project in the sample code for this book. Unfortunately, because this isn’t a standard data connection, we can’t drag and drop a table as we did before. Instead, we have to build the schema manually. The result is shown in Figure 9-4.


Figure 9-4: The schema for FSDataSet shown in the Visual Studio .NET Schema view

This schema consists of one element, <FSInfo>, which contains <Name> and <Type> elements, where <Name> is a string and <Type> is the SimpleType FSInfoType. FSInfoType has two constraints set, which restrict the possible string values to Directory and Type. In XML, this schema is as follows:

<?xml version="1.0" encoding="utf-8" ?> <xs:schema     targetNamespace="http://www.notashop.com/wscr/FSDataSet.xsd"   elementFormDefault="qualified" attributeFormDefault="qualified"   xmlns="http://www.notashop.com/wscr/FSDataSet.xsd"   xmlns:mstnshttp://www.notashop.com/wscr/FSDataSet.xsd"   xmlns:xs="http://www.w3.org/2001/XMLSchema"   xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">   <xs:element name="FSDataSet" msdata:IsDataSet="true">     <xs:complexType>       <xs:choice maxOccurs="unbounded">         <xs:element name="FSInfo">           <xs:complexType>             <xs:sequence>               <xs:element name="Name" type="xs:string" minOccurs="0" />               <xs:element name="Type" type="FSInfoType" minOccurs="0" />             </xs:sequence>           </xs:complexType>         </xs:element>       </xs:choice>     </xs:complexType>   </xs:element>   <xs:simpleType name="FSInfoType">     <xs:restriction base="xs:string">       <xs:enumeration value="Directory" />       <xs:enumeration value="Type" />     </xs:restriction>   </xs:simpleType> </xs:schema>

This code defines the type FSDataSet, which we will use in the code for both the Web service and the new Windows client.

Now let’s look at the Web service code, FSDPWebService.asmx.cs. We start by putting the service in our standard namespace:

[WebService(Namespace="http://www.notashop.cm/wscr")] public class FSDPWebService : System.Web.Services.WebService {

As mentioned earlier, we hardcode the root directory for file transfer in the Web service, initialized in the constructor:

    private string rootDirectory;     public FSDPWebService()     {         //CODEGEN: This call is required by          //the ASP.NET Web Services Designer         InitializeComponent();         rootDirectory = "C:\\WSCR\\CDDBInfo\\";     }

Next we have some more standard Web service code, then our three Web methods: GetDirectory for using the GetDirectory command, GetFile for using the GetFile command, and DownloadFile for getting files in byte form. Here’s the GetDirectory Web method:

    [WebMethod]     public FSDataSet GetDirectory(string directory)     {         FSConnection conn = new FSConnection(rootDirectory);         FSCommand cmd = conn.CreateCommand("GetDirectory " + directory);         FSDataAdapter adapter = new FSDataAdapter(cmd, conn);         FSDataSet results = new FSDataSet();         conn.Open();         adapter.Fill(results);         conn.Close();         return results;     }

This follows the standard code we used earlier to use our custom data provider. The only new thing here is that we use an FSDataSet object rather than a plain DataSet object. Note that once we’ve defined this type we can use an object of the type as if it were a plain DataSet object. We don’t have to add any extra code.

Then we have GetFile:

    [WebMethod]     public FSDataSet GetFile(string searchString)     {         FSConnection conn = new FSConnection(rootDirectory);         FSCommand cmd = conn.CreateCommand("GetFile " + searchString);         FSDataAdapter adapter = new FSDataAdapter(cmd, conn);         FSDataSet results = new FSDataSet();         conn.Open();         adapter.Fill(results);         conn.Close();         return results;     }

Again, this is standard code that now uses an FSDataSet.

Finally, here’s the DownloadFile method:

    [WebMethod]     public byte[] DownloadFile(string filename)     {         FSConnection conn = new FSConnection(rootDirectory);         FSCommand cmd = conn.CreateCommand("GetFile " + filename);         conn.Open();         FSDataReader reader = cmd.ExecuteReader(CommandBehavior.Default)           as FSDataReader;         if (!reader.Read())         {             return null;         }         byte[] returnVal = reader.GetFile();         conn.Close();         return returnVal;     } }

Once more, we see code that’s almost identical to that in our earlier Windows client. We get the file into a byte array and return it. Simple.

Next we turn to the client for this Web service. As mentioned earlier, we’re basing the client on FSDPWinFormClient but hooking up a Web reference to the new Web service and using that rather than referencing FileSystemDataProvider.

The first code change, other than the namespace referenced in the using statement that gives the client access to our file system data, is simply to remove the root directory configured in the client because this is now handled by the Web service. Next we have changes to our two handlers. First, the executeButton_Click handler:

    private void executeButton_Click(object sender, System.EventArgs e)     {         try         {             FSDPWebService service = new FSDPWebService();             if (getDirectoryButton.Checked)             {                 results = service.GetDirectory(parameterTextBox.Text);             }             else             {                 results = service.GetFile(parameterTextBox.Text);             }             resultGrid.DataSource = results;             resultGrid.DataMember = "FSInfo";             statusLabel.Text = "Command Executed OK.";             if (getDirectoryButton.Checked)             {                 directoryLabel.Text = parameterTextBox.Text;             }             else             {                 directoryLabel.Text = parameterTextBox.Text.Substring(0,                    parameterTextBox.Text.LastIndexOf('\\') + 1);             }         }         catch (SoapException ex)         {             results = null;             resultGrid.DataSource = null;             Regex pattern =                 new Regex("\\S*FSException: (?<Message>[a-zA-Z0-9 ]*)",                 RegexOptions.Multiline);             Match splitString = pattern.Match(ex.Message);             if (!splitString.Success)             {                 statusLabel.Text = ex.Message;             }             else             {                 statusLabel.Text =  splitString.Groups["Message"].Value;             }         }     }

Here we change the data access classes used to use the Web service, and the exception-handling code to catch exceptions of type SoapException. To extract just the message from the FSException that is the precursor to the SoapException message, we use another simple RegEx expression. To see why this is required, try setting statusLabel.Text to ex.Message for all exceptions. You’ll see a lot of text that isn’t very useful!

Next, we have the downloadButton_Click button handler:

    private void downloadButton_Click(object sender, System.EventArgs e)     {         try         {             if (results != null && resultGrid.CurrentRowIndex != -1)             {                 DataRow selectedRow = results.Tables["FSInfo"]                     .Rows[resultGrid.CurrentRowIndex];                 String rowName = selectedRow.ItemArray[0] as String;                 String rowType = selectedRow.ItemArray[1] as String;                 if (rowType == "Directory")                 {                     statusLabel.Text = "Cannot download whole directories.";                     return;                 }                 FSDPWebService service = new FSDPWebService();                 byte[] fileData = service.DownloadFile(directoryLabel.Text                     + "\\" + rowName);                 saveFileDialog1.FileName = rowName;                 if (saveFileDialog1.ShowDialog() == DialogResult.OK)                 {                     FileInfo newFile = new FileInfo(                         saveFileDialog1.FileName);                     FileStream fs = newFile.Create();                     BinaryWriter writer = new BinaryWriter(fs);                     writer.Write(fileData);                     writer.Flush();                     writer.Close();                     statusLabel.Text = "File downloaded successfully.";                 }             }         }         catch (SoapException ex)         {             Regex pattern =                 new Regex(@"\S*FSException: (?<Message>[a-zA-Z0-9 ]*)",                     RegexOptions.Multiline);             Match splitString = pattern.Match(ex.Message);             if (!splitString.Success)             {                 statusLabel.Text = ex.Message;             }             else             {                 statusLabel.Text =  splitString.Groups["Message"].Value;             }         }     }

Much more was removed from this method than put in. We now have much simpler syntax because the byte array from the source file is obtained by the Web service. We just have to save it to disk, using the same code as before. The only other change to the code is to modify the exception handling as before.

And there we have it. A fully functional custom ADO.NET provider accessed via a Web service, using typed DataSet objects suitable for a .NET Windows client. This enables the client to make use of the full DataSet functionality to manipulate file system data, which is great in situations like this, where we can use a DataGrid control with ease.

A Final Note

Before moving on, it’s worth noting one important thing, which we’ll examine in more depth in Chapter 11. The Web service is responsible for accessing the directory containing the downloadable files. This means that security plays a part—the Web service itself must have access to these files. Now, you might have noticed that in our example we used the directory C:\WSCR\ CDDBInfo\—that is, not a directory below wwwroot, but one elsewhere on the file system. And we didn’t receive a security error. In fact—and this is the key thing we’ll be looking at later—ASP.NET (including ASP.NET Web services) is given a high degree of security privilege by default. But this can be a dangerous thing. Admittedly, the data provider used here is read only, but people can still get access to private data if you aren’t careful. What we really need to do is to impose a greater level of control over what ASP.NET can and can’t do, and you’ll see how to do that in Chapter 11.




Programming Microsoft. NET XML Web Services
Programming MicrosoftВ® .NET XML Web Services (Pro-Developer)
ISBN: 0735619123
EAN: 2147483647
Year: 2005
Pages: 172

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