Introduction
Maintaining a database off-site can be difficult, especially if you are not at home.
I wanted to learn C# and ASP.NET so I created a .NET Web Service, utilizing the
OLE DB interface, that allows you to view and edit databases, and do it all dynamically!
It allows you to:-
- Log on to a database server (e.g. MS SQL, Access, Oracle).
- Build connection string dynamically.
- Get a list of Databases or Schemas.
- Get a list of Tables, Views or Procedures.
- C++ calls for Providers not fully implemented.
- View, edit, insert, delete and sort entries.
- Check-Boxes dynamically added in Edit mode.
- Access another database/table quickly.
Logon Web Page
The OLE DB connection string is built from the UserID and Password
entry fields, the Provider ListItem box and the Server/Filename 'INPUT' field.
I have added an extra field into the The Provider ListItem Collection Editor's
'Value' Property.
If an extra parameter needs to be added to the connection string
(eg. Oracle needs 'OLEDB.NET=TRUE'), it is inserted after the provider label
delimited by the '|' character. The Server/Filename has a browse button to
allow browsing for a MDB file. The Server/Filename 'INPUT' and Password
fields do not save their values between trips for Security reasons.
The connection string is derived from a API call to a C++
routine, DBConnstr()
, in the DLL WebAppUtil. This
code was modified from some sample code found in Help. It gets the providers
CSLID
, the IDBProperties
is populated with the
User ID, Password, and Server/Filename;
the DBPROMPT_NOPROMPT
value is set to prevent
the OLE DB Connection dialog from popping up. A database instance is created
with IDataInitialize
and then the interface is queried with
IDBProperties
pointer. The IDBProperties
properties
are set, the IDBInitialize
property is initialized and then the
Initialization string is extracted. This way the Connection String can be
dynamically
generated for each interface. The getHRtext()
routine also came
from the help and extracts a text message from the HRESULT
return
code if their is an error. The error message is put in a popup window
utilizing an HIDDEN field on both pages that is invoked by a Java script
in the HTML code.
The Get Databases/Schemas
button will
get a list of databases/schemas available
on the server and insert them into the DBDropDownList
box.
Before we can fill in the DropDown ListBox, it needs to be determined if
Schema's or Catalog's are supported. The code first checks for a Catalog name
because Catalog's are not always supported. If Catalogs are supported, they
are used, otherwise Schema's are loaded into the DropDown Listbox. The
names are stored in a string array and then stored in a Session variable for
page reloads and round trips.
OleDbConnection statConn = new OleDbConnection(connStr);
...
statConn.Open();
schema = statConn.GetOleDbSchemaTable(
OleDbSchemaGuid.Schemata,new object[] {null});
if (schema.Rows[0]["CATALOG_NAME"].ToString() != "")
{
DB = statConn.GetOleDbSchemaTable(OleDbSchemaGuid.Catalogs,
new object[] {null});
LBNames = new string[DB.Rows.Count];
while (j < DB.Rows.Count)
{
LBNames[j] = DB.Rows[j]["CATALOG_NAME"].ToString();
DBDropDownList.Items.Add(LBNames[j++]);
}
...
}
else
{
schema.Dispose();
schema = statConn.GetOleDbSchemaTable(OleDbSchemaGuid.Schemata,
new object[] {null,UserIDTextBox.Text.ToString()});
LBNames = new string[schema.Rows.Count];
while (j < schema.Rows.Count)
{
LBNames[j] = schema.Rows[j]["SCHEMA_NAME"].ToString();
DBDropDownList.Items.Add(LBNames[j++]);
}
...
}
Session["myDBs"] = LBNames;
The "Select Item" RadioButtonList
causes the "Get ITEM"
buttons text to change for that Item along with the Label for the
ItemsDropDownList
. The selected item determines which
API will be called to fill in the ItemsDropDownList
and the
ArgumentsTextBox
. When the procedure parameters are displayed,
each text field should be replace with a parameter.
The "use C++ code to get Item info" Check-Box causes the
"Get ITEMS" button to use the C++ routines
in the DLL, DBGetTables()
, DBGetViews()
and
DBGetProcs()
. The DBGetIndexes()
and
DBQuote()
are used by the "Execute" button.
Some providers (e.g. MS Jet) do not support all
the new API's, it seems. These routines use the CTable, CView,
CProcedures, CProcedureParameters
and the CIndexes
classes to get the information. Also, GetLiteralInfo()
of the
CComQIPtr<IDBInfo>
template is used to get the Quote
character. I'll leave this code for you to explore.
The "Get ITEMS" button will get a list of items (e.g. tables, views or
procedures) available for
that database/schema and insert them into the ItemsDropDownList
box. The code first gets the Quote Suffix and Prefix characters and then a list
of items. Either the Catalog or the Schema name is null
at this
point. If there is a Space in the Item name, the Quote Characters are added.
If the "System Tables" Check-Box is checked, only system items
are retrieved (except for procedures).
queryType = TypeRadioButtonList.SelectedItem.ToString();
...
schemaTable = statConn.GetOleDbSchemaTable(
OleDbSchemaGuid.DbInfoLiterals,null);
DataRow[] dr = schemaTable.Select("LiteralName LIKE 'Quote_*'");
quotePrefix = dr[0]["LiteralValue"].ToString();
quoteSuffix = dr[1]["LiteralValue"].ToString();
Session["myquoteprefix"] = quotePrefix;
Session["myquotesuffix"] = quoteSuffix;
schemaTable.Dispose();
switch (queryType)
{
case "Table" :
columnName = "TABLE_NAME";
schemaTable = statConn.GetOleDbSchemaTable(OleDbSchemaGuid.Tables,
new object[] {selectedDB,selectedSchema,null,
SysTblsCheckBox.Checked ? "SYSTEM TABLE" : "TABLE"});
break;
case "View" :
columnName = "TABLE_NAME";
schemaTable = statConn.GetOleDbSchemaTable(OleDbSchemaGuid.Tables,
new object[] {selectedDB,selectedSchema,null,
SysTblsCheckBox.Checked ? "SYSTEM VIEW" : "VIEW"});
break;
case "Procedure" :
columnName = "PROCEDURE_NAME";
schemaTable = statConn.GetOleDbSchemaTable(OleDbSchemaGuid.Procedures,
new object[] {selectedDB,selectedSchema,null,null});
break;
}
items = new string[schemaTable.Rows.Count];
int i = 0;
while (i < schemaTable.Rows.Count)
{
items[i] = schemaTable.Rows[i][columnName].ToString();
i++;
}
...
for (int i = 0; i < items.Length; i++)
{
itemName = items[i];
if (queryType == "Procedure")
{
index = itemName.IndexOf(';');
if (index >= 0)
itemName = itemName.Substring(0,index);
}
index = itemName.IndexOf(' ');
if (index >= 0)
itemName = quotePrefix + itemName + quotePrefix;
ItemsDropDownList.Items.Add(itemName);
LBNames[i] = itemName;
}
itemName = ItemsDropDownList.Items[0].ToString();
ArgumentsTextBox.Text = "";
procParmNames = null;
switch (queryType)
{
case "Table" :
case "View" :
itemName = ItemsDropDownList.SelectedItem.Text;
SQLstatement = "SELECT * FROM " + itemName;
ArgumentsTextBox.Text = SQLstatement;
ArgumentsLabel.Text = "SQL Statement";
break;
case "Procedure" :
GetProcedureParms();
break;
}
...
schemaTable.Dispose();
The "Execute" button finds a key in the item and loads the
DataWebForm page. If no Key is found, the Insert DataGrid is not displayed
and the "Update" and "Delete" buttons will perform no
actions on the "DataWebForm" page.
If two or more Key names are returned for in one
key field, they are separated by a ',', only the first is used. If there is a
Space in the Index name, the Quote Characters are added.
indexName = "";
schemaTable = indexConn.GetOleDbSchemaTable(OleDbSchemaGuid.Primary_Keys,
new object[] {selectedDB,selectedSchema,itemName});
if (schemaTable.Rows.Count > 0)
indexName = schemaTable.Rows[0]["COLUMN_NAME"].ToString();
else
indexName = "";
schemaTable.Dispose();
...
index = indexName.IndexOf(',');
if (index >= 0)
indexName = indexName.Remove(index,indexName.Length - index);
index = indexName.IndexOf(' ');
if (index >= 0)
indexName = quotePrefix + indexName + quotePrefix;
...
Server.Transfer("DataWebForm.aspx");
The SQL Statement or Procedure parameters are generated each time the Item
selection is changed.
The AutoPostBack
is set True
causing the
ItemsDropDownList_SelectedIndexChanged()
routine to be called
where the Select statement/Procedure parms are rebuilt. The SQL Statement
can be modified to limit the query before the "Execute" button
is pushed, BUT, modify at your own risk. When the procedure parameters are
displayed, each text field should be replace with a parameter.
Error popup window
There is an error popup window in HTML, embedded in a Java script, that
utilizes a hidden Text field, on both pages. If there is text in the field,
the error box is displayed.
Data Web Page
There are two DataGrids, one for inserting a new record and the other for
Editing or Deleting records. There is also a read only field representing the
SQL statement/procedure used to build the grids.
The Edit/Delete DataGrid is filled from a OleDataAdapter
initialized with the SQL Statement or Procedure from the Logon page.
The Insert, Update and Delete Command
are dynamically built using the OleCommandBuilder
, if a index
was found on the LogonWebPage.
The Quote characters are retrieved from the Logon page and
set into the OleCommandBuilder
.
custCB.QuotePrefix = sourcepage.GetQuotePrefix;
custCB.QuoteSuffix = sourcepage.GetQuoteSuffix;
oleDbDataAdapter.InsertCommand = custCB.GetInsertCommand();
oleDbDataAdapter.UpdateCommand = custCB.GetUpdateCommand();
oleDbDataAdapter.DeleteCommand = custCB.GetDeleteCommand();
The Update button causes the offset into the dataset to be calculated
by multiplying the current page number times the data grid size page size
and then adding the row index.
While looping through each column of data for the row, the control type
is checked to see if it is a "System.Web.UI.WebControls.TextBox"
or "System.Web.UI.WebControls.CheckBox"
so that
the data can be extracted correctly from either a TextBox
or a
CheckBox
. As the data is being put into the DataRow
,
fields (e.g.. binary, image, sql_variant, timestamp, uniqueID, varbinary)
that were not displayed in the DataGrid, are skipped, there data remains
unchanged. If the row has changed, an Update is performed. Note that the
DataRow (for update) is now acquired from the DataViewManager
because that
is where the sorted view is found.
...
int dsItemIndexOffset = (DataGridDB.CurrentPageIndex *
DataGridDB.PageSize)
+ e.Item.ItemIndex;
string key = DataGridDB.DataKeys[e.Item.ItemIndex].ToString();
...
TextBox tb;
DataRow dr = dataSet.DefaultViewManager.DataSet.Tables
[0].DefaultView[dsItemIndexOffset].Row;
for (int i = dbTableColumnOffset; i < e.Item.Cells.Count; i++)
{
string c = e.Item.Controls[i].Controls[0].GetType().ToString();
if (c == "System.Web.UI.WebControls.TextBox")
{
tb = (TextBox)(e.Item.Cells[i].Controls[0]);
categoryField = tb.Text;
}
else if (c == "System.Web.UI.WebControls.CheckBox")
{
bool b =
((CheckBox)(
e.Item.Cells[i].Controls[0])).Checked;
if (b)
categoryField = "True";
else
categoryField = "False";
}
else
{
LogErrorMsg("Unkown field type in Grid");
return;
}
do
{
fieldType = dataSet.Tables[0].Columns
[i-dbTableColumnOffset].DataType.ToString();
if (fieldType != "System.Byte[]" &&
fieldType != "System.Object" &&
fieldType != "System.Guid")
break;
i++;
} while(i < dr.Table.Columns.Count+dbTableColumnOffset);
if (!dataSet.Tables[0].Rows[dsItemIndexOffset].ItemArray
i-dbTableColumnOffset].Equals(categoryField))
dr[i-dbTableColumnOffset] = categoryField;
}
if (dataSet.HasChanges())
{
try
{
oleDbDataAdapter.Update(dataSet);
dataSet.AcceptChanges();
}
catch (Exception er)
...
The Insert DataGrid is initialized with
the same query but does not get any records, this is done so that the column
headings can dynamically be retreived. A new blank DataRow
is added to it's associated dataset and the Grid is put in the edit mode.
The Delete button is enabled but renamed to Insert.
The Insert button creates a new DataRow
and adds
the data from the Insert grid into the DataRow
.
It skips over non-supported types so those data elements need to support nulls!
The new DataRow
is then inserted into the
other DataGrids dataset and a Update is performed.
The Check-Boxes are dynamically added when the grids are put in the Edit mode.
The Grids column type is found and the TextBox
is replaced with
a CheckBox
.
The DataGridDB_ItemCreated
is called at the end of the edit with no
DataItem
, do not know why,
but putting in a dummy CheckBox
keeps it from blowing up.
private void DataGridDB_ItemCreated(object sender,
System.Web.UI.WebControls.DataGridItemEventArgs e)
{
if (e.Item.ItemType == ListItemType.EditItem )
{
if (e.Item.DataItem == null)
{
DataSet ds = (DataSet)Session["mydataset"];
int i = 0;
do {
if (ds.Tables[0].Columns[i].DataType.ToString() ==
"System.Boolean")
{
CheckBox cb = new CheckBox();
e.Item.Controls[i+dbTableColumnOffset].Controls.Add(cb);
e.Item.Controls[i+dbTableColumnOffset].Controls.RemoveAt(0);
}
} while(++i < ds.Tables[0].Columns.Count);
}
else
{
for (int i = 0;
i < e.Item.Controls.Count-insertTableColumnOffset;
i++)
{
try
{
string itemType = dataSet.DefaultViewManager.
DataViewSettings[0].Table.
Columns[i].DataType.ToString();
if (itemType == "System.Boolean")
{
CheckBox cb = new CheckBox();
cb.ID = ((DataTable)((DataView)((DataRowView)e.Item.
DataItem).DataView).Table).Columns[i].
ColumnName.ToString();
cb.DataBinding += new EventHandler(this.BindCheckBoxData);
e.Item.Controls[i+insertTableColumnOffset].Controls.Add(cb);
e.Item.Controls[i+insertTableColumnOffset].Controls.
RemoveAt(0);
}
}
catch (Exception er)
{
LogException(er);
}
}
}
...
}
The this.BindCheckBoxData()
came from Shaun Wildes
'Adding a CheckBox column to your DataGrid' CodeProject. The column
name is stored in the check-boxes ID
field.
private void BindCheckBoxData(object sender, EventArgs e)
{
CheckBox box = (CheckBox) sender;
DataGridItem container = (DataGridItem) box.NamingContainer;
box.Checked = false;
string data = ((DataRowView)container.DataItem).ToString();
Type t = ((DataRowView)
container.DataItem).DataView.Table.Columns.DataType;
if (data.Length>0)
{
switch (t.ToString())
{
case "System.Boolean":
if (( data == "True") || (data == "true"))
{
box.Checked = true;
}
break;
default:
break;
}
}
}
Sort: The DataGridDB_SortCommand()
is called by clicking on a
column name and then the Sort field is set in the DefaultViewManager.
When updating a row, the row has to be found through the DefaultViewManager
which has the sorted dataset that matched the data displayed in the DataGrid,
see DataGridDB_UpdateCommand()
by the
"get original row from the view for update" comment.
private void DataGridDB_SortCommand(...)
{
dataSet.Dispose();
dataSet = new DataSet();
oleDbDataAdapter.Fill(dataSet);
dataSet.DefaultViewManager.DataViewSettings[0].Sort =
e.SortExpression.ToString();
DataGridDB.DataSource = dataSet;
DataGridDB.DataBind();
Session["mydataset"] = dataSet;
Session["mydataadapter"] = oleDbDataAdapter;
}
There is a "New Table" link that reloads the Logon Page
with the Session variables reloaded.
Deployment
Included is a WebSetup project. It was created using Majid Shahabfar's
Deploying Web Applications using Visual Studio .NET project.
Conclusion
This is my third and probably final incantation. I have delusions of
putting this all into a template, maybe when it snows down here :-D:.
The Oracle provider for Oracle is case sensative, FYI.
You should be able to add your own provider to the Provider DropDown ListItems,
don't forget the two '|'s at the end of the Value field.
If you see how do get around the few work-arounds I made, let me know and
I'll change the code.
Let me know what you think about all this. If you think this article and code
are useful, be sure to vote!
History
14 Sep 2002 - updated source code.
25 Nov 2002 - added insert DataGrid, selectable Provider
05 Dec 2002 - added views, procedures and sort