Welcome to MSDN Blogs Sign in | Join | Help

Writing a DatabaseProvider

This blog post is about writing a DatabaseProvider for the IIS Manager's Database Manager module. The module can be found here. The source for this article is available here.

This article will help walk you through the steps to create a database provider using an XML DataSet provider as an example.

Implementing DatabaseProvider

We’ll started by making a new project that implements DatabaseProvider.

  1. Start Visual Studio and create a new C# “Class Library” project
  2. Add a reference to “Microsoft.Web.Managment.DBManager”
  3. Create a class that will be your database provider class. Have it implement “Microsoft.Web.Management.DBManager.DatabaseProvider”.  Tell Visual Studio to implement the abstract class. It will fill out all the methods with “throw new NotImplementException()” which is fine for now.
  4. We’ll need to strongly type our assembly so go into the project properties and then the “Signing” tab. Click the “Sign the assembly” checkbox and create a new strong name key file. Give the key file a name and uncheck “Protect my key file with a password”.
  5. Build the assembly.

Now we’ve got the skeleton of a database provider done. We’ll register our database provider with DB Manager and then start implementing functions.

Registering the DatabaseProvider

Once you’ve written a database provider for DB Manager you’ll have to register it before DB Manager will start using it.

  1. Install your database provider assembly in the GAC. This can be done using “gacutil”.
  2. Open the “%windir%\system32\inetsrv\config\administration.config” file.
  3. Find the <system.webServer/management/dbManager/DBProviders> collection.
  4. Create a new entry for your provider. You will have to specify two values.

The “type” will be the fully qualified type which will include the assembly name specification, a namespace specification and a type name. 

The “name” for your provider should match the “providerName” attribute of any connection string that will use your provider.  DB Manager uses the “providerName” attribute to map a connection string to a DB Manager database provider. If the “providerName” attribute isn’t specified for a connection string DB Manager will use the provider specified by the “defaultDBProvider” attribute of the DB Manager configuration section.

 For example if we were writing a provider for Microsoft SQL Server we would use “System.Data.SqlClient” as our “name” because SQL Server connection string will use “System.Data.SqlClient” as their providerName. In our XML DataSet example we’ll just make something up because there is no XML DataSet ADO.NET provider (the providerName attribute really specifies the ADO.NET provider to use to access the underlying data store). So our entry will look something like:

<provider name=”XMLProvider” type=”XMLDatabaseProvider.XMLDataSetProvider, XMLDatabaseProvider, Version=0.0.0.0, Culture=neutral, PublicKeyToken=d6c672ae03e4c650" /> 

If all was successful you should be able to start up Inetmgr, navigate to DB Manager, click “Add Connection” and you provider should show up under the “Database provider type” drop down. If you try selecting your database provider you’ll get an error message telling you the method or operation isn’t implemented. We’ll fix that next.

Implementing ConnectionStringArguments

For almost every call your database provider will receive you’ll also get a connection string. The connection string specifies how to connect to a database. For example a SQL Server connection string might look like:

        Server=server1;Database=customer;User ID=sqlServerUser;Password=test

The highlighted words are the keywords for which a user must specify a value. This list of keywords is what your database provider should return when implementing the ConnectionStringArgument property.

For our XML DataSet provider we’ll just ask the user to specify one value which is the location of the database. Our implementation will look like:

public override ICollection<ConnectionArgument> ConnectionStringArguments {

    get {

        List<ConnectionArgument> connectionArgList = new List<ConnectionArgument>();

 

        ConnectionArgument serverArg = new ConnectionArgument();

        serverArg.Name = "dataFile";

        connectionArgList.Add(serverArg);

 

        return connectionArgList;

    }

}

Build your DLL again, update the version in the GAC and start Inetmgr again. Add a new connection string and select our provider. Now instead of receiving an error you’ll see the connection asks for the “dataFile”. Fill in “C:\ CustomProviderDb.xml” and drop the CustomProviderDb.xml file on your C: drive.  Give the connection string a name and click OK.

Now if you try to expand your newly created connection your should see a not implemented error again.

Implementing TestConnection

It’s time to implement the test connection method. TestConnection() receives a connection string and makes sure it’s valid. If the string is not valid TestConnection should throw an exception. If it is valid then we simply return with no error.

For our XML DataSet provider we’ll verify there’s a file at the location specified by the user. Our TestConnection() then looks like:

public override void TestConnection(string connectionString) {

    if (!File.Exists(GetDatabaseLocationFromConnectionString(connectionString))) {

        throw new InvalidOperationException("Could not find XML database");

    }

}

Where GetDatabaseLocationFromConnectionString() is a private helper method used to extract the file location from the entire connection string:

private static string GetDatabaseLocationFromConnectionString(string connectionString) {

    int indexOfDatabaseName = connectionString.IndexOf("dataFile=") + 9;

    if (indexOfDatabaseName < 9 || connectionString.Length <= indexOfDatabaseName) {

        throw new InvalidOperationException("Invalid connection string");

    }

 

    return connectionString.Substring(indexOfDatabaseName);

}

Some additional validation should probably be done on the connection string but for now this will work.

Build again, update the GAC with your newest version and start Inetmgr again. Navigate to DB Manager and expand your user created connection string again. This time instead of a not implemented exception you should see the database connection expand if you’ve set up everything correctly. However trying to expand the “Tables” node will give you an exception again.

Implementing GetTables

Now it’s time to return a list of the tables available to the client. We’ll define a helper method to load the dataset specified by the connection string:

private DataSet LoadDataSetFromXml(string xmlLocation)

{

    // Read the XML file into a new DataSet

    using (FileStream fsIn =

        new FileStream(Environment.ExpandEnvironmentVariables(xmlLocation),

                       FileMode.Open, FileAccess.Read)) {

        using (XmlTextReader xtr = new XmlTextReader(fsIn)) {

            DataSet dataSet = new DataSet();

            dataSet.ReadXml(xtr, XmlReadMode.ReadSchema);

            xtr.Close();

            fsIn.Close();

            return dataSet;

        }

    }

}

Now we can implement GetTables(). Get tables returns a list of the database in the database but doesn’t return any information about the internal structure of the table itself or any of the table data. When the client needs that information it will call GetTableInfo() or GetData() on a specific table.

public override ICollection<Table> GetTables(string connectionString)

{

    DataSet dataSet = LoadDataSetFromXml(GetDatabaseLocationFromConnectionString(connectionString));

 

    List<Table> tables = new List<Table>();

 

    // loop through all dataTables and construct a Table object for each

    // the Table object doesn't contain any information about the table

    // structure itself that information is returned by GetTableInfo()

    foreach (DataTable dataTable in dataSet.Tables) {

        Table table = new Table();

        table.Name = dataTable.TableName;

        // for the example we won't use the schema property at all

        tables.Add(table);

    }

 

    return tables;

}

 

Build your DLL again and update the version in the GAC. Now when we connect to our database and expand he tables node you should see a list of the tables in your database assuming your XML database has at least one table already in it.

Implementing GetData

When the user right clicks on one of the databases and selects “Show Table Data” your database provider’s GetData() function will be called. This should read the data from the database and return it to the client.

GetData returns a QueryResult which is made up of two parts. The first is information about the columns of table which is the ColumnMetadata property of the query result. The ColumnMetadata has information like whether the column is read only, the maximum length of the column and the column name. The second member is the QueryResults member which is an ArrayList of object arrays. Each object array corresponds to a row in the database.

One thing to remember is that all the values you return to the client must be serializable by Inetmgr. For example a Decimal value would need to be converted to a double or else you’ll receive an error. One type that you’ll frequently encounter that isn’t serializable is DBNull. DB Manager uses a null value in place of DBNull when passing values between the client and the server.

Therefore our GetData() method looks like:

 

// Get the rows of the specified table. Called when "Show Table Data" is clicked.

public override QueryResult GetData(string connectionString, string tableName, string schema)

{

    DataSet dataSet = LoadDataSetFromXml(GetDatabaseLocationFromConnectionString(connectionString));

 

    // the result will be a QueryResult

    QueryResult queryResult = new QueryResult();

 

    DataTable table = dataSet.Tables[tableName];

    if (table == null) {

        throw new InvalidOperationException("Could not find table " + tableName);

    }

 

    // now we need to build up the QueryColumnMetadata part of the QueryResult

    foreach (DataColumn dataColumn in table.Columns) {

        QueryColumnMetadata m = new QueryColumnMetadata();

        m.Name = dataColumn.ColumnName;

       

        // if set > 0 this will stop limit the user to ColWidth characters for data in this column

        m.ColWidth = 0;

       

        // whether the column allows nulls

        m.IsNullable = dataColumn.AllowDBNull;

 

        // this will prevent the user from entering data into this column

        m.IsComputed = dataColumn.ReadOnly;

 

        queryResult.ColumnMetadata.Add(m);

    }

 

    // get all the data in the table

    foreach (DataRow row in table.Rows) {

        // each row is passed as an object[] to the client

        object[] itemData = new object[table.Columns.Count];

 

        for (int i = 0; i < table.Columns.Count; i++) {

            object value = row[i];

 

            // DBNull isn't serializable so we just use null

            // to signify a DBNull value

            if (value is DBNull || value == null) {

                continue;

            }

 

            // convert anything that isn't serializable to a

            // serializable form to send to the client

            if (value is Decimal) {

                itemData[i] = (double)((Decimal)value);

            }

            else {

                itemData[i] = value;

            }

        }

 

        // add each row to the results

        queryResult.QueryResults.Add(itemData);

    }

 

    return queryResult;

}

 

Implement this method, rebuild your DLL, update the GAC and start Inetmgr. Now when you select “Show Table Data” you can view the data in your table.

 

Implementing InsertRow

The next step is to allow users to add a new row. InsertData takes an argument that is a list of ColumnDataInfo’s.  Each ColumnDataInfo represents data for one column in a row. The ColumnDataInfo contains:

1.       The name of the column to which the data belongs

2.        The column’s original value (useful for the EditData method)

3.       The column’s new value

4.       IsEdited which tells you if a user specified this value or not

The reason for the IsEdited property is because we’re already using null to signify DBNull. Without IsEdited you would not be able to tell if a null meant the user specified NULL or the user did not specify any value. It is important to know whether or not a user specified a value because for a particular column it may be an error to specify any value.

InsertRow() also returns and object[]. This return object array represents the values of the inserted column after the insert. This may be different than the values passed into the InsertRow function. For example if a column has a default value it will receive a value even if the user did not specify one. This information is given back to the client via the return value of InsertRow. If you don’t want the client to automatically update the values of the inserted row simply return null.

Now that we’re changing the table data we’ll also need to save the data back out to an XML file:

private void SaveDataSetToXml(DataSet dataSet, string xmlLocation) {

    // commit any changes to the dataset

    dataSet.AcceptChanges();

 

    // Save the changed DataSet to a file.

    using (FileStream fsOut =

        new FileStream(Environment.ExpandEnvironmentVariables(xmlLocation),

                       FileMode.Truncate,

                       FileAccess.Write)) {

        using (XmlTextWriter xtw = new XmlTextWriter(fsOut, Encoding.Unicode)) {

            dataSet.WriteXml(xtw, XmlWriteMode.WriteSchema);

            xtw.Close();

            fsOut.Close();

        }

    }

}

Now we’re reading to implement InsertRow(). Notice again we’ll have to convert values we’re returning to the server that won’t serialize.

public override object[] InsertRow(string connectionString, string tableName, string schema, IList<ColumnDataInfo> columnDataInfoList)

{

    string dataFile = GetDatabaseLocationFromConnectionString(connectionString);

    DataSet dataSet = LoadDataSetFromXml(dataFile);

 

    // Insert a new row in the table.  First fill up the row.

    DataRow newRow = dataSet.Tables[tableName].NewRow();

 

    for (int i = 0; i < columnDataInfoList.Count; i++) {

        // only set values the user actually specified

        if (columnDataInfoList[i].IsEdited) {

             newRow[columnDataInfoList[i].ColumnName] = columnDataInfoList[i].NewValue;

        }

    }

 

    // Add the new row to the table

    dataSet.Tables[tableName].Rows.Add(newRow);

 

    // the Rows.Add call filled out any computed values for us so we

    // can just read everything from the newRow object

    object[] result = null;

 

    result = new object[columnDataInfoList.Count];

 

    // Move each cell in the row to the result parameter to return.

    for (int i = 0; i < columnDataInfoList.Count; i++) {

        object value = newRow[columnDataInfoList[i].ColumnName];

        if (value is DBNull) {

            result[i] = null;

        }

        else if (value is Decimal) {

            result[i] = (double)((Decimal)value);

        }

        else {

            result[i] = value;

        }

    }

 

    //Save data to XML file

    SaveDataSetToXml(dataSet, dataFile);

 

    return result;

}

Implementing EditRow

Edit row is very similar to insert row but we’ll have to located the row in the dataset first. We’ll make a function that will find our row for us:

private DataRow[] GetMatchingRows(DataTable table, IList<ColumnDataInfo> columnDataInfoList) {

    // build up select string

    StringBuilder selectString = new StringBuilder();

    foreach (ColumnDataInfo columnDataInfo in columnDataInfoList) {

        if (selectString.Length > 0) {

            selectString.Append(" AND ");

        }

        if (columnDataInfo.OriginalValue == null) {

            selectString.Append(columnDataInfo.ColumnName + " is NULL");

        }

        else {

            selectString.Append(columnDataInfo.ColumnName + "='" + columnDataInfo.OriginalValue + "'");

        }

    }

 

    DataRow[] dataRowArray = table.Select(selectString.ToString());

    return dataRowArray;

}

Notice we use all the original column values to locate the table but we’ll only update the ones the user touched.

When we implement EditRow we’ll want to make sure GetMatchingRows only returned one row. If there are two rows with the exact same values we don’t know which the user wanted to edit so we’ll just throw an error and tell the user we couldn’t do the update. The rest is straight forward:

public override void EditRow(string connectionString, string tableName, string schema, IList<ColumnDataInfo> columnDataInfoList)

{

    string dataFile = GetDatabaseLocationFromConnectionString(connectionString);

    DataSet dataSet = LoadDataSetFromXml(dataFile);

 

    DataTable table = dataSet.Tables[tableName];

 

    // this loops through every row in the table to make sure only one row matches

    DataRow[] matchingRows = GetMatchingRows(table, columnDataInfoList);

 

    // throw exception if we couldn't find the row or there were multiple rows

    if (matchingRows.Length == 0) {

        throw new InvalidOperationException("Could not find row to edit");

    }

    else if (matchingRows.Length > 1) {

        throw new InvalidOperationException("No row was updated. The data was not committed. The row values(s) updated or deleted either do not make the row unique or they alter multiple rows");

    }

 

    for (int j = 0; j < columnDataInfoList.Count; j++) {

        // don't specify values the user didn't touch

       if (columnDataInfoList[j].IsEdited) {

           matchingRows[0][columnDataInfoList[j].ColumnName] = columnDataInfoList[j].NewValue;

       }

    }

 

    //Save data to the XML file

    SaveDataSetToXml(dataSet, dataFile);

}

Implementing DeleteRow

The last method we need to write to deal with our table data is DeleteRow(). After writing the code to find a row for EditRow() this becomes trivial:

public override void DeleteRow(string connectionString, string tableName, string schema, IList<ColumnDataInfo> columnDataInfoList) {

    string dataFile = GetDatabaseLocationFromConnectionString(connectionString);

    DataSet dataSet = LoadDataSetFromXml(dataFile);

    DataTable table = dataSet.Tables[tableName];

 

    DataRow[] matchingRows = GetMatchingRows(table, columnDataInfoList);

 

    // throw exception if we couldn't find the row or there were multiple rows

    if (matchingRows.Length == 0) {

        throw new InvalidOperationException("Could not find row to edit");

    }

    else if (matchingRows.Length > 1) {

        throw new InvalidOperationException("No row was updated. The data was not committed. The row values(s) updated or deleted either do not make the row unique or they alter multiple rows");

    }

 

    table.Rows.Remove(matchingRows[0]);

 

    //Save data to the XML file

    SaveDataSetToXml(dataSet, dataFile);

}

Implementing GetTableInfo

Now that we can show the table data we should implement “Open Table Definition”.  To do this we need to create a TableInfo, fill it out with all the appropriate information such as table names, columns, indexes, etc… and send it to the client.  We’ll start out with a GetTableInfo that looks like:

public override TableInfo GetTableInfo(string connectionString, string tableName, string schema) {

    DataSet dataSet = LoadDataSetFromXml(GetDatabaseLocationFromConnectionString(connectionString));

 

    DataTable table = dataSet.Tables[tableName];

 

    TableInfo tableInfo = new TableInfo();

    tableInfo.Name = tableName;

    GetColumns(table, tableInfo.Columns);

 

    // we’ll do something with the primary key here later

 

    return tableInfo;

}

Now we need to write the GetColumns function which will read the columns one by one from the XML DataTable, create a Column object for each and then add it to tableInfo.Columns.  The mapping of attributes from DataColumn to Column is fairly straight forward but there are a few things worth pointing out. First the code:

// Get the columns in the table for display when "Open Table Definition is clicked".

private void GetColumns(DataTable table, IList<Column> columns) {

    // build up a list of all the primary key columns

    List<string> primaryKeyColumns = new List<string>();

    foreach (DataColumn col in table.PrimaryKey) {

        primaryKeyColumns.Add(col.ColumnName);

    }

 

    // loop through each column to get column info

    foreach (DataColumn col in table.Columns) {

        Column column = new Column();

        string columnName = (string)col.ColumnName;

 

        column.Name = columnName;

        // old name will remain the same even if name changes

        column.OriginalName = column.Name;

        column.ColumnType = col.DataType.Name;

 

        column.SetColumnIsInPrimaryKey(primaryKeyColumns.Contains(columnName));

 

        column.Size = col.MaxLength;

 

        object defaultValue = col.DefaultValue;

        if (defaultValue is DBNull) {

            column.DefaultValue = String.Empty;

        }

        else {

            column.DefaultValue = defaultValue.ToString();

        }

        column.AllowNulls = col.AllowDBNull;

 

        column.IsIdentity = col.AutoIncrement && col.Unique;

 

        columns.Add(column);

    }

}

One difference is the DataColumn doesn’t specify if a column is part of a primary key or not and Column would like that information so that it can display a key icon on that column. The first thing we do is build up a list of all the columns in the primary key so using DataTable.PrimaryKey so we can use it later.

Each Column has a property called OriginalName that will not be touched if the column is renamed. You can use it to located the column in EditTable() even if the column has been renamed;

Lastly the DataColumns don’t have an attribute that exactly corresponds to “identity” but we can set AutoIncrement and Unique on a column and get pretty much the same functionality (the UI should enforce that any column set to be an identity column also does not allow nulls). Therefore if we see a column with both those attributes set we’re calling it an “identity” column.

Now we’ll deal with the primary key issue. To DB Manager a primary key is just an index of type IndexType.PrimaryKey so if a PrimaryKey exists on the table we’ll create an index object for it. For each column in the index we’ll create an IndexColumn, fill out the column name and add it to index.Columns.

Therefore the final GetTableInfo() looks like:

public override TableInfo GetTableInfo(string connectionString, string tableName, string schema) {

    DataSet dataSet = LoadDataSetFromXml(GetDatabaseLocationFromConnectionString(connectionString));

 

    DataTable table = dataSet.Tables[tableName];

 

    TableInfo tableInfo = new TableInfo();

 

    tableInfo.Name = tableName;

    GetColumns(table, tableInfo.Columns);

 

    // get the primary key and show it as an index

    if (table.PrimaryKey != null && table.PrimaryKey.Length > 0) {

        Index primaryKey = new Index();

        primaryKey.IndexType = IndexType.PrimaryKey;

        // primary key has no name so just make one up

        primaryKey.Name = "Primary Key";

        primaryKey.Unique = true;

        foreach (DataColumn dataColumn in table.PrimaryKey) {

            IndexColumn indexColumn = new IndexColumn();

            indexColumn.Name = dataColumn.ColumnName;

            primaryKey.Columns.Add(indexColumn);

        }

        tableInfo.Indexes.Add(primaryKey);

    }

 

    return tableInfo;

Now you can view the columns and primary keys of your tables. You’ll notice if you click on the “type” drop down for any of the columns you’ll get an error. We need to let the client know what types of columns our database supports before we can edit the table schema.

Implementing GetServerTypes

For each column type supported by the server we’ll create a TypeInfo and set the appropriate properties. This isn’t an exhaustive list of everything supported by our XML database but it’s enough for our example:

public override ICollection<TypeInfo> GetServerTypes(string connectionString)

{

    List<TypeInfo> typesArrayList = new List<TypeInfo>();

 

    TypeInfo intType = new TypeInfo();

    intType.Name = "Int";

    intType.Id = 0;

    intType.AllowIdentity = true;

    intType.AllowNulls = true;

    typesArrayList.Add(intType);

 

    TypeInfo stringType = new TypeInfo();

    stringType.Name = "String";

    stringType.Id = 1;

    stringType.AllowNulls = true;

    typesArrayList.Add(stringType);

 

    TypeInfo longType = new TypeInfo();

    longType.Name = "Long";

    longType.Id = 2;

    longType.AllowNulls = true;

    longType.AllowIdentity = true;

    typesArrayList.Add(longType);

 

    TypeInfo floatType = new TypeInfo();

    floatType.Name = "Float";

    floatType.Id = 3;

    floatType.AllowNulls = true;

    typesArrayList.Add(floatType);

 

    TypeInfo doubleType = new TypeInfo();

    doubleType.Name = "Double";

    doubleType.Id = 4;

    doubleType.AllowNulls = true;

    typesArrayList.Add(doubleType);

 

    TypeInfo decimalType = new TypeInfo();

    decimalType.Name = "Decimal";

    decimalType.Id = 5;

    doubleType.AllowNulls = true;

    typesArrayList.Add(decimalType);

 

    TypeInfo dateTimeType = new TypeInfo();

    dateTimeType.Name = "DateTime";

    dateTimeType.Id = 6;

    dateTimeType.AllowNulls = true;

    typesArrayList.Add(dateTimeType);

 

    TypeInfo boolType = new TypeInfo();

    boolType.Name = "Boolean";

    boolType.Id = 7;

    boolType.AllowNulls = true;

    typesArrayList.Add(boolType);

 

    TypeInfo byteType = new TypeInfo();

    byteType.Name = "Byte";

    byteType.Id = 8;

    byteType.AllowNulls = true;

    typesArrayList.Add(byteType);

 

    return typesArrayList;

}

We’ll use the “Id” property later. It’s never seen by the user and for now it’s enough to say you should just make each one unique somehow. In our case we’re using sequential integers.

Implementing EditTable

It’s time to let the user edit a table. Note this is also the function that lets the user drop a table. If TableInfo.IsMarkedForDelete is set then the table should be dropped. Our EditTable() function will look like:

public override void EditTable(string connectionString, string schema, TableInfo tableInfo) {

    if (tableInfo.IsDirty) {

        string databaseLocation = GetDatabaseLocationFromConnectionString(connectionString);

        DataSet dataSet = LoadDataSetFromXml(databaseLocation);

        if (tableInfo.IsMarkedForDelete) {

            dataSet.Tables.Remove(tableInfo.Name);

        }

        else {

            UpdateTable(dataSet, schema, tableInfo);

        }

 

        // if all went well we'll save the data set back to stroage

        SaveDataSetToXml(dataSet, databaseLocation);

    }

}

And we’ll create a helper method to do the updating of the table. To update the table we’ll have to crawl all the columns and indexes and check for changes/additions/removals. For each Column/Index/ForeignKey you can check for changes by calling GetChanges() and passing the flag for the property you want to check. For example to see if a column’s “AllowNulls” property has changed you would do:

if (column.GetChanges(ColumnChanges.AllowNull)) {

    // do something here

}

We’ll write a private method to add a column to a table since this will be useful for both editing and creating tables:

private DataColumn AddColumnToTable(DataTable table, Column column) {

    DataColumn dataColumn = new DataColumn(column.Name, GetSystemTypeFromColumnTypeId(column.ColumnTypeId));

    dataColumn.AllowDBNull = column.AllowNulls;

    if (!String.IsNullOrEmpty(column.DefaultValue)) {

        dataColumn.DefaultValue = column.DefaultValue;

    }

 

    // for identity we'll set unique, autoincrement

    dataColumn.AutoIncrement = column.IsIdentity;

    dataColumn.Unique = column.IsIdentity;

 

    table.Columns.Add(dataColumn);

    return dataColumn;

}

While updating the columns we’re also going to use the “Id” value we gave to each system type. Each Column has a “TypeId” property that will be whatever we set when we were specifying the database types. Based on the TypeId we get we know which kind of system type to assign to DataColumn.DataType.  We’ll make a method out of this since we’ll use it for CreateTable code as well:

private Type GetSystemTypeFromColumnTypeId(object typeId) {

    // we know we assigned an int to the typeId so cast it back

    int typeIdInt = (int)typeId;

 

    // these values come from the values set in GetServerTypes()

    switch (typeIdInt) {

        case 0:

            return typeof(int);

        case 1:

            return typeof(string);

        case 2:

            return typeof(long);

        case 3:

            return typeof(float);

        case 4:

            return typeof(double);

        case 5:

            return typeof(decimal);

        case 6:

            return typeof(DateTime);

        case 7:

            return typeof(bool);

        case 8:

            return typeof(byte);

        default:

            throw new InvalidOperationException("Invalid column data type");

    }

}

Lastly we know the user might have added or deleted a primary key so we’ll have to check the indexes in our UpdateTable as well.  We’ll create a little private method to create the primary key if one is needed because this can also be used from CreateTable():

private void CreatePrimaryKey(DataTable table, Index index) {

    ArrayList primaryKeyColumns = new ArrayList();

    foreach (IndexColumn indexColumn in index.Columns) {

        primaryKeyColumns.Add(table.Columns[indexColumn.Name]);

    }

    table.PrimaryKey = (DataColumn[])primaryKeyColumns.ToArray(typeof(DataColumn));

}

The full code for UpdateTable():

private void UpdateTable(DataSet dataSet, string schema, TableInfo tableInfo) {

    DataTable table = dataSet.Tables[tableInfo.Name];

 

    // update all the columns

    foreach (Column column in tableInfo.Columns) {

        if (column.GetChanges(ColumnChanges.IsMarkedForDelete)) {

            table.Columns.Remove(column.OriginalName);

        }

        if (column.GetChanges(ColumnChanges.IsNew)) {

            AddColumnToTable(table, column);

        }

        else {

            DataColumn dataColumn = table.Columns[column.OriginalName];

 

            if (column.GetChanges(ColumnChanges.AllowNull)) {

                dataColumn.AllowDBNull = column.AllowNulls;

            }

 

            if (column.GetChanges(ColumnChanges.DefaultValue)) {

                dataColumn.DefaultValue = column.DefaultValue;

            }

 

            if (column.GetChanges(ColumnChanges.IsIdentity)) {

                if (column.IsIdentity) {

                    dataColumn.AllowDBNull = false;

                    dataColumn.AutoIncrement = true;

                    dataColumn.Unique = true;

                }

                else {

                    // let them set allow null if they want

                    dataColumn.Unique = false;

                    dataColumn.AutoIncrement = false;

                }

            }

 

            if (column.GetChanges(ColumnChanges.Type)) {

                dataColumn.DataType = GetSystemTypeFromColumnTypeId(column.ColumnTypeId);

            }

 

            // not supporting size changes

 

            if (column.GetChanges(ColumnChanges.Name)) {

                dataColumn.ColumnName = column.Name;

            }

        }

    }

 

    // update primary key if needed

    // do any deletes before a create and since we're only supporting

    // primary key there should only be one

    Index primaryKeyIndex = null;

    foreach(Index index in tableInfo.Indexes) {

        if (index.GetChanges(IndexChanges.IsMarkedForDelete) &&

           (index.IndexType == IndexType.PrimaryKey)) {

            table.PrimaryKey = null;

        }

        else if (index.GetChanges(IndexChanges.IsNew) &&

                (index.IndexType == IndexType.PrimaryKey)) {

            primaryKeyIndex = index;

        }

    }

 

    if (primaryKeyIndex != null) {

        CreatePrimaryKey(table, primaryKeyIndex);

    }

}

 

Now we can go in and change our table schema.

Implementing CreateTable

We’ve already got most of the code for CreateTable() ready after doing EditTable:

public override void CreateTable(string connectionString, string schema, TableInfo tableInfo) {

    string databaseLocation = GetDatabaseLocationFromConnectionString(connectionString);

    DataSet dataSet = LoadDataSetFromXml(databaseLocation);

 

    // create a new DataTable that mirrors the structure of the tableInfo

    DataTable newTable = new DataTable(tableInfo.Name);

 

    // create a DataColumn for each Column in our tableInfo

    foreach (Column column in tableInfo.Columns) {

        DataColumn dataColumn = AddColumnToTable(newTable, column);

    }

 

    // create a primary key if one was specified

    foreach (Index index in tableInfo.Indexes) {

        if (index.IndexType == IndexType.PrimaryKey) {

            CreatePrimaryKey(newTable, index);

        }

    }

 

    // add our new table to the dataset

    dataSet.Tables.Add(newTable);

 

    // if all went well we'll save the data set back to stroage

    SaveDataSetToXml(dataSet, databaseLocation);

}

Implementing GetPrimaryKey

This function is used by the foreign key column chooser dialog. When you select a table from the primary key drop down we need to look up what columns (if any) are in the primary key. Even though we haven’t implemented foreign keys for our database provider we can implement this method as an example although there’s nothing conceptually new here:

public override string[] GetPrimaryKey(string connectionString, string tableName, string schema) {

    DataSet dataSet = LoadDataSetFromXml(GetDatabaseLocationFromConnectionString(connectionString));

 

    ArrayList columnsArrayList = new ArrayList();

    DataTable dataTable = dataSet.Tables[tableName];

    foreach (DataColumn dataColumn in dataTable.PrimaryKey) {

        columnsArrayList.Add(dataColumn.ColumnName);

    }

 

    return ((string[])columnsArrayList.ToArray(typeof(string)));

}

Implementing GetStoredProcedures

The XML database doesn’t support stored procedures but implementing this should be fairly straight forward by this point. For each stored procedure in your database you’ll have to create a StoredProcedure object and assign it a name, definition (body) and schema. Add each StoredProcedure object to a list and return it to the client.

Implementing DropProcedure

The XML database doesn’t support stored procedures but to implement this function you’d simply drop the stored procedure whose name corresponds to the name passed to this function.

Impelmenting ExecuteQuery

This allows the user to execute an arbitrary query against the database. Since our XML database doesn’t support this we won’t implement it. If you were to implement this function you’d construct and return a QueryResult object almost exactly like you would for GetData(). The only difference is you only need to fill out the “Name” attribute of each QueryColumnMetadata since the editing of rows returned by ExecuteQuery() is not allowed.

Posted by andrewbi | 2 Comments

More Config Locking

Introduction

Before reading this you should read and understand all the information in this article:

http://www.iis.net/articles/view.aspx/IIS7/Managing-IIS7/Delegation-in-IIS7/Delegating-Permission-in-Config/How-to-Use-Locking-in-IIS7-Configuration

This covers a few points which might still be a little uncertain after reading the above.

The lockItem attribute versus lockElements, etc…

Although the configuration system will allow you to put a lockItem attribute on any element in config it is meaningless unless the element is a collection item (which includes configuration sections). Similarly the lockElements, lockAttributes, lockElementsExcept, and lockAttributesExcept attributes can also be placed on any element in configuration but won’t affect anything when placed on collection items.

Locking and location paths

Any locking done on a section/element/attribute only prevents them from being changed at lower configuration levels (web.config files). This does not mean it is locked at the same configuration level for a lower location path. Using the same <windowsAuthentication> section example, putting the following text in applicationHost.config is completely valid:

<location path="." overrideMode="Allow">

  <system.webServer>       

     <security>

        <authentication>

           <windowsAuthentication enabled="false" lockAttributes="enabled" />

        </authentication>

     </security>

  </system.webServer>

</location>

<location path="Default Web Site">

  <system.webServer>       

     <security>

        <authentication>

           <windowsAuthentication enabled="true"/>

        </authentication>

     </security>

  </system.webServer>

</location>

Even though the enabled attribute has been locked you can still redefine it for Default Web Site in applicationHost.config. However if you moved the text from the second location path into Default Web Site’s web.config file you’ll receive a configuration error.

Unlocking via a location path

You can’t unlock a section/element/attribute via a location path. For example the following applicationHost.config entries will leave the enabled attribute locked for lower level config files, including Default Web Site’s web.config file:

<location path="."  overrideMode="Allow">

  <system.webServer>       

     <security>

        <authentication>

           <windowsAuthentication enabled="false" lockItem="true"/>

        </authentication>

     </security>

  </system.webServer>

</location>

<location path="Default Web Site">

  <system.webServer>       

     <security>

        <authentication>

           <windowsAuthentication lockItem="false" />

        </authentication>

     </security>

  </system.webServer>

</location>

 

Posted by andrewbi | 1 Comments

Inetmgr preferences service

Introduction

This post explains the preferences service available in IIS Manager. The Preference service allows developers to save and load preferences across instances of their modules.  The settings are stored in a file in the user’s windows profile so they can be accessed later even after inetmgr is closed and opened again. The type of data that can stored is limited to bools, ints and strings but you can easily serialize other data structures to a string which enables you store just about any type of data.

In this article we’ll walk through how the preferences service works and how to save some of your own preferences.

 

Getting started using the preferences service

Let’s create a page that lists a group of files to illustrate our point. We’ll also allow the user to group the files by whether they’re hidden or visible. Our list page looks like:

namespace PreferencesDemo {

    class PreferencesDemoPage : ModuleListPage {

 

        private ColumnHeader _nameColumn;

        private ColumnHeader _creationDateColumn;

        private ColumnHeader _modificationDateColumn;

        private ColumnHeader _readOnlyColumn;

        private ColumnHeader _hiddenColumn;

 

        private ModuleListPageGrouping _hiddenGrouping;

 

        private ListViewGroup _hiddenYes;

        private ListViewGroup _hiddenNo;

 

        public override ModuleListPageGrouping[] Groupings {

            get {

                if (_hiddenGrouping == null) {

                    _hiddenGrouping = new ModuleListPageGrouping("HiddenType", "Hidden");

 

                    _hiddenYes = new ListViewGroup("Hidden Files");

                    _hiddenNo = new ListViewGroup("Visible Files");

                }

 

                return new ModuleListPageGrouping[] {

                               _hiddenGrouping,

                           };

            }

        }

 

        protected override ListViewGroup[] GetGroups(ModuleListPageGrouping grouping) {

            if (grouping == _hiddenGrouping) {

                return new ListViewGroup[] {

                    _hiddenYes,

                    _hiddenNo,

                };

            }

 

            return null;

        }

 

        protected override void InitializeListPage() {

            _nameColumn = new ColumnHeader();

            _nameColumn.Text = "Name";

            _nameColumn.Width = 175;

 

            _creationDateColumn = new ColumnHeader();

            _creationDateColumn.Text = "Creation Date";

            _creationDateColumn.Width = 135;

 

            _modificationDateColumn = new ColumnHeader();

            _modificationDateColumn.Text = "Modification Date";

            _modificationDateColumn.Width = 135;

 

            _readOnlyColumn = new ColumnHeader();

            _readOnlyColumn.Text = "Read Only";

            _readOnlyColumn.Width = 65;

 

            _hiddenColumn = new ColumnHeader();

            _hiddenColumn.Text = "Hidden";

            _hiddenColumn.Width = 65;

 

            ListView.Columns.AddRange(new ColumnHeader[] {

                _nameColumn,

                _creationDateColumn,

                _modificationDateColumn,

                _readOnlyColumn,

                _hiddenColumn,

            });

 

            ListView.MultiSelect = false;

            ListView.LabelWrap = false;

        }

 

        protected override void OnGroup(ModuleListPageGrouping grouping) {

            foreach (ListViewItem item in ListView.Items) {

                if (grouping == _hiddenGrouping) {

                    // set item.Group to appropriate group

                }

            }

        }

    }

 

For simplicity we haven’t actually implemented any methods to populate the list view but that isn’t necessary for the purpose of illustrating the preference services of IIS Manager.

If you create a module out of this code and play around with it you might notice that our PreferencesDemo module doesn’t remember the user’s last “group by” setting unlike other list view pages.  This is because we haven’t given our module a ModulePageIdentifier attribute for the preferences service to use. Place the following code above the definition of the PreferencesDemoPage class and reinstall the new module.

[ModulePageIdentifier("03b8b54f-5682-4be1-83ec-ce1b0ef8ce58")]

You’ll notice the “group by” setting is now automatically remembered. This is done by the ModuleListPage base class. How does this work? The answer is the PreferencesStore which is described in the next section.

How the PreferencesStore works

Our “group by” setting was saved because once you define a  ModulePageIdentifier for your page the ModulePage base class will call SavePreferences from ModulePage::OnDeactivating and LoadPreferences from ModulePage::OnActivated (during the initial activation only). The function signatures are:

protected override void SavePreferences(PreferencesStore store)
protected virtual void LoadPreferences(PreferencesStore store)

ModuleListPage already overrides these methods and stores preferences like the last “group by” setting, column widths and previous filter queries on modules that use filter functionality like the Application Pools page.

These settings are stored on the local machine under the windows user’s “ApplicationData\Microsoft\WebManagement\<WebManagement version>\” folder in a file called InetMgr.preferences. The data is stored in a binary format so you won’t be able to edit the file directly. The data stored does not have any state associated with it so whatever preferences you store will be loaded by your module page regardless of the connection used by IIS Manager (e.g. settings saved while using IIS Manager as administrator on the local box will be loaded next time you use IIS Manager to connect to a remote server as a different user).

Saving preferences to the PreferencesStore

Now let’s say we want to allow the user to rearrange the columns on our list view page. The first step is to let the user reorder the columns. This is done by adding:

ListView.AllowColumnReorder = true;

In the InitializeListPage function.

Next we’ll define a helper class that will take care of getting and setting the column order:

namespace PreferencesDemo {

    class ColumnOrderHelper {

 

        [DllImport("user32.dll")]        

        static extern bool SendMessage(IntPtr hWnd, Int32 msg, Int32 wParam, int[] lParam);

 

        const int LVM_FIRST = 0x1000;

        const int LVM_SETCOLUMNORDERARRAY = LVM_FIRST + 58;

        const int LVM_GETCOLUMNORDERARRAY = LVM_FIRST + 59;

 

        //returns an array of column index

        public static int[] GetDisplayColumnOrders(ListView listView) {

 

            int numColumns = listView.Columns.Count;

            int[] returnValue = new int[numColumns];

            SendMessage(listView.Handle, LVM_GETCOLUMNORDERARRAY, numColumns, returnValue);      

            return returnValue;

        }

 

        public static bool SetDisplayColumnOrder(ListView listView, int[] columnOrder) {

 

            int numColumns = listView.Columns.Count;

 

            if (columnOrder.Length != numColumns) {

                return false;

            }

 

            return SendMessage(listView.Handle, LVM_SETCOLUMNORDERARRAY, numColumns, columnOrder);

        }

    }

}

 

Now we have to save the column order to the preferences store. The preferences store has three methods you can use to save preferences:

SetValue(string name, bool value, bool defaultValue)
SetValue(string name, int value, int defaultValue)
SetValue(string name, string value, string defaultValue)

Note that SetValue takes a defaultValue just like GetValue does. If you set a value to the defaultValue then the preferences store will delete that value’s entry since it is not needed.

 As mentioned earlier you can save bools, ints and strings to the preferences store. We’ll store our column order as a string of comma separated values:

protected override void SavePreferences(PreferencesStore store) {

            base.SavePreferences(store);

 

            int[] columnOrder = ColumnOrderHelper.GetDisplayColumnOrders(ListView);

 

            // translate to a string of csv

            string columnOrderString = String.Empty;

            for(int i = 0; i < columnOrder.Length; i++) {

                if (i > 0) {

                    columnOrderString += ",";

                }

 

                columnOrderString += columnOrder[i].ToString();

            }

 

            store.SetValue(“ColumnOrderKey”, columnOrderString, String.Empty);

        }

Notice we start our method with a call to base.SavePreferences so that our ModuleListPage settings will continue to be saved.

Next we’ll write our LoadPreferences method:

        protected override void LoadPreferences(PreferencesStore store) {

            base.LoadPreferences(store);

 

            string columnOrderCSV = store.GetValue(PreferencesDemoGlobals.ColumnOrderKey, String.Empty);

 

            // String.Empty is the default value meaning we don't have any real preferences saved

            if (!String.IsNullOrEmpty(columnOrderCSV)) {

                string[] columnOrderStringArray = columnOrderCSV.Split(',');

                _loadedColumnOrder = new int[columnOrderStringArray.Length];

 

                // can't actually SET the column order here yet because we haven't run

                // InitializePage() yet so the ListView doesn't have it's columns yet.

                for (int i = 0; i < columnOrderStringArray.Length; i++) {

                    _loadedColumnOrder[i] = Int32.Parse(columnOrderStringArray[i]);

                }

            }

        }

 

Note we don’t actually load the column order yet, we just save it to an int[] named _loadedColumnOrder. This is done because our ListView object doesn’t actually have any columns defined yet so we can’t change the column order. Our columns are added to the list view object in InitializeListPage which is called by ModuleListPage::OnActivated but LoadPreferences is called by ModulePage::OnActivated which runs first. Therefore the last step is to add code to load the list view column order after all of our preferences have been loaded and the page has been initialized:

        protected override void OnActivated(bool initialActivation) {

            base.OnActivated(initialActivation);

 

            if (initialActivation) {

                if (_loadedColumnOrder != null) {

                    ColumnOrderHelper.SetDisplayColumnOrder(ListView, _loadedColumnOrder);

                }

        }

Using the Preference service from the IServiceProvider

The preferences service can also be used from outside of a ModulePage.  If you have access to the IServiceProvider object you can get the IPreferencesService:

IPreferencesService preferencesService = (IPreferencesService)_serviceProvider.GetService(typeof(IPreferencesService);

Just like we had to define a Guid for our settings in the ModulePage, you’ll have to define a Guid and use that to access your preferences store:

Guid PreferenceStoreGuid = new Guid("9745f305-2b54-4fd0-86a8-de5527e9a7b3");
PreferencesStore store = preferencesService.GetPreferencesStore(PreferenceStoreGuid);

As a note, make sure to use the same Guid each time you access your preferences store otherwise you won’t be able to find your preferences later.

Now you can use the same PreferencesStore methods we were using before to save and load ints, strings and bools.  After setting values in the store you’ll have to call IPreferencesService.Save() to commit the changes.

In the sample code I’ve posted I’ve created a class that inherits from the IHomepageTaskListProvider interface and uses the preferences store just as I’ve described here. When the provider is instantiated it reads the value of the counter out of the preferences store, increments it by one and saves it again. It also provides a home page task to display the value of the counter. It doesn’t do anything useful but does demonstrate using the preferences service outside of a ModulePage as described above.

Conclusion

As you can see the preferences service is a convenient way of storing user specific settings that are persisted between inetmgr sessions.

Sample Code

Download the sample code here. If you open the solution and build it, it should build the two DLLs and register them in the GAC for you. Then open the %windir%\system32\inetsrv\config\administration.config file and put:

<add name="PreferencesDemo" type="PreferencesDemo.PreferencesDemoModuleProvider, PreferencesDemo, Version=0.0.0.0, Culture=neutral, PublicKeyToken=80e554de7f31e558" />

In the moduleProviders section with all the other module definitions. Then start inetmgr.exe and the Demo module should show up.

Posted by andrewbi | 1 Comments
 
Page view tracker