The 13th page of the OfficeTest General Tests tutorial looks again at using ObjectDataSource and custom made business objects. This time we'll look at one method of using sub-class properties to handle foreign key relationships. If you want to review the other tutorials, the central page can be found here Firebird and .NET 2.0 development Example.
13. ObjectDataSource, Custom business objects
Project: BLL - Business Logic Layer.
Lets start by taking a look at whats different in the ProjectData class. Overall its basically the same. There is a private member and a public property for each column in the Project table and 2 constructors. This time however the ManagerID, which is a foreign key to the Employee table, is replaced with a sub-class object of type EmployeeData.
ProjectData
public class ProjectData
{
...
private EmployeeData manager;
public ProjectData() {
this.projectID = -1;
this.manager = new EmployeeData();
}
public ProjectData(int projectID, string name, string description, bool isActive, EmployeeData manager) {
this.projectID = projectID;
this.name = name;
this.description = description;
this.isActive = isActive;
this.manager = manager;
}
public EmployeeData Manager {
get {
return manager;
}
set {
manager = value;
}
}
...
}
ProjectList
[DataObject]
public class ProjectList
{
...
[DataObjectMethod(DataObjectMethodType.Select)]
public static List<ProjectData> LoadTable(int startRowIndex, int maximumRows, string sortBy) {
// This select routine fully supports paging and sorting.
List<ProjectData> items = new List<ProjectData>();
string connectionString = WebConfigurationManager.ConnectionStrings["OfficeTestFirebird"].ConnectionString;
FbConnection con = new FbConnection(connectionString);
string sql;
if (maximumRows == -1)
sql = "SELECT ";
else
sql = "SELECT FIRST " + maximumRows.ToString() + " SKIP " + startRowIndex.ToString() + " ";
sql += "\"ProjectID\", \"Name\", \"Description\", \"IsActive\", \"ManagerID\" FROM \"Project\" ";
if (sortBy == "")
sql += " ORDER BY \"ProjectID\"";
else {
sql += " ORDER BY " + sortBy;
}
FbCommand cmd = new FbCommand(sql, con);
try {
con.Open();
FbDataReader reader = cmd.ExecuteReader();
try {
items.Clear();
EmployeeList managers = new EmployeeList();
managers.Populate();
while (reader.Read()) {
EmployeeData manager = new EmployeeData();
if (reader["ManagerID"] != DBNull.Value) {
int managerID = (int)reader["ManagerID"];
foreach (EmployeeData man in managers.items) {
if (man.EmployeeID == managerID) {
manager = man;
break;
}
}
}
ProjectData data = new ProjectData((int)reader["ProjectID"],
(string)reader["Name"],
reader["Description"].ToString(),
((short)reader["IsActive"] == 1),
manager);
items.Add(data);
}
} finally {
reader.Close();
}
} finally {
con.Close();
}
return items;
}
[DataObjectMethod(DataObjectMethodType.Select)]
public static List<ProjectData> LoadRecord(int ProjectID) {
List<ProjectData> items = new List<ProjectData>();
string connectionString = WebConfigurationManager.ConnectionStrings["OfficeTestFirebird"].ConnectionString;
FbConnection con = new FbConnection(connectionString);
try {
con.Open();
string sql = "SELECT \"ProjectID\", \"Name\", \"Description\", \"IsActive\", \"ManagerID\" FROM \"Project\" "+
"WHERE \"ProjectID\" = @ProjectID";
FbCommand cmd = new FbCommand(sql, con);
cmd.Parameters.Clear();
cmd.Parameters.Add("@ProjectID", ProjectID);
FbDataReader reader = cmd.ExecuteReader();
try {
items.Clear();
while (reader.Read()) {
// This routine should only return a single row really so we can just load
// the single employee entry for it using the static LoadRecord method.
EmployeeData manager = new EmployeeData();
if (reader["ManagerID"] != DBNull.Value) {
int managerID = (int)reader["ManagerID"];
List<EmployeeData> managers = EmployeeList.LoadRecord(managerID);
manager = managers[0];
}
ProjectData data = new ProjectData((int)reader["ProjectID"],
(string)reader["Name"],
reader["Description"].ToString(),
((short)reader["IsActive"] == 1),
manager);
items.Add(data);
}
} finally {
reader.Close();
}
} finally {
con.Close();
}
return items;
}
[DataObjectMethod(DataObjectMethodType.Insert)]
public static int Insert(ProjectData data) {
string connectionString = WebConfigurationManager.ConnectionStrings["OfficeTestFirebird"].ConnectionString;
FbConnection con = new FbConnection(connectionString);
try {
con.Open();
FbCommand cmd = new FbCommand("insertProject", con);
cmd.CommandType = CommandType.StoredProcedure;
cmd.Parameters.Clear();
cmd.Parameters.Add("@Name", data.Name);
cmd.Parameters.Add("@Description", data.Description);
int isActive = 0;
if (data.IsActive)
isActive = 1;
cmd.Parameters.Add("@IsActive", isActive);
if (data.Manager.EmployeeID != -1)
cmd.Parameters.Add("@ManagerID", data.Manager.EmployeeID);
else
cmd.Parameters.Add("@ManagerID", FbDbType.Integer);
FbParameter param = new FbParameter("@ProjectID", TypeCode.Int32);
param.Direction = ParameterDirection.ReturnValue;
cmd.Parameters.Add(param);
cmd.ExecuteScalar();
return (int)param.Value;
} finally {
con.Close();
}
}
...
}
LoadTable
EmployeeList managers = new EmployeeList();
managers.Populate();
while (reader.Read()) {
EmployeeData manager = new EmployeeData();
if (reader["ManagerID"] != DBNull.Value) {
int managerID = (int)reader["ManagerID"];
foreach (EmployeeData man in managers.items) {
if (man.EmployeeID == managerID) {
manager = man;
break;
}
}
}
ProjectData data = new ProjectData((int)reader["ProjectID"],
(string)reader["Name"],
reader["Description"].ToString(),
((short)reader["IsActive"] == 1),
manager);
items.Add(data);
}
Obviously rebuilding the EmployeeList each time we load the the Project table is a waste. In a larger application the EmployeeList should really be pulled from a cache.
LoadRecord
EmployeeData manager = new EmployeeData();
if (reader["ManagerID"] != DBNull.Value) {
int managerID = (int)reader["ManagerID"];
List<EmployeeData> managers = EmployeeList.LoadRecord(managerID);
manager = managers[0];
}
Insert/Update
if (data.Manager.EmployeeID != -1)
cmd.Parameters.Add("@ManagerID", data.Manager.EmployeeID);
else
cmd.Parameters.Add("@ManagerID", FbDbType.Integer);
WebPage Markup properties
Lets take a quick look at the aspx code. The ObjectDataSource odsAllProjects and odsEmployees work the same as in other tutorials so we don't really need to look at them.
ObjectDataSource
<asp:ObjectDataSource ID="odsCurrentProject"
OnInserting="odsCurrentProject_Inserting"
OnUpdating="odsCurrentProject_Updating">
</asp:ObjectDataSource>
DetailsView
<asp:DetailsView ID="dvProject">
<Fields>
<asp:TemplateField HeaderText="Manager" SortExpression="Manager">
<EditItemTemplate>
<asp:DropDownList ID="DropDownList1" runat="server" AppendDataBoundItems="True" DataSourceID="odsEmployees" DataTextField="FirstName" DataValueField="EmployeeID" SelectedValue='<%# Eval("Manager.EmployeeID") %>' OnSelectedIndexChanged="DropDownList1_SelectedIndexChanged" OnDataBound="DropDownList1_DataBound">
<asp:ListItem Selected="True" Value="-1">Null</asp:ListItem>
</asp:DropDownList>
</EditItemTemplate>
<InsertItemTemplate>
<asp:DropDownList ID="DropDownList2" runat="server" AppendDataBoundItems="True" DataSourceID="odsEmployees" DataTextField="FirstName" DataValueField="EmployeeID" SelectedValue='<%# Eval("Manager.EmployeeID") %>' OnSelectedIndexChanged="DropDownList1_SelectedIndexChanged" OnDataBound="DropDownList1_DataBound">
<asp:ListItem Selected="True" Value="">Null</asp:ListItem>
</asp:DropDownList>
</InsertItemTemplate>
<ItemTemplate>
<asp:Label ID="Label1" runat="server" Text='<%# Bind("Manager") %>'></asp:Label>
</ItemTemplate>
</asp:TemplateField>
</Fields>
</asp:DetailsView>
The binding for the selected value of the list is Eval("Manager.EmployeeID"). This is a pretty simple binding but it doesn't get us an instance of the EmployeeData class which is what we want in the end. OnSelectedIndexChanged="DropDownList1_SelectedIndexChanged" and OnDataBound="DropDownList1_DataBound" event handlers have been added so we can get the EmployeeData class instance we need when an employee is selected from the list. We'll discuss how we do this further down in this tutorial.
GridView
<asp:GridView ID="gvProjects">
<Columns>
<asp:BoundField DataField="Manager" HeaderText="Manager" />
</Columns>
</asp:GridView>
Partial WebPage class code
Now lets take a look at the code related to the event handlers mentioned above.
public partial class ProjectDataObjects : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e) {
if (!this.IsPostBack) {
Session["Manager"] = new EmployeeData();
gvProjects.SelectedIndex = 0;
}
}
...
protected void odsCurrentProject_Updating(object sender, ObjectDataSourceMethodEventArgs e) {
// For some reason after updating all fields are correct except the manager is cleared
ProjectData project = (ProjectData)e.InputParameters[0];
EmployeeData manager = (EmployeeData)Session["Manager"];
project.Manager = manager;
}
protected void odsCurrentProject_Inserting(object sender, ObjectDataSourceMethodEventArgs e) {
ProjectData project = (ProjectData)e.InputParameters[0];
EmployeeData manager = (EmployeeData)Session["Manager"];
project.Manager = manager;
}
protected void DropDownList1_SelectedIndexChanged(object sender, EventArgs e) {
// Set to new manager after selection
SetManager((DropDownList)sender);
}
protected void DropDownList1_DataBound(object sender, EventArgs e) {
// Grab and store the original manager since it seems to get lost
SetManager((DropDownList)sender);
}
private void SetManager(DropDownList list) {
// During INSERT all fields are initialized as EMPTY or DBNull. This causes a problem with
// the -1 issue. Would be better if it started with an initialized ProjectData() object instead.
// It may be possible to do this but I'm not sure the cleanest way.
int managerID = -1;
if (list.SelectedValue != "")
managerID = Int32.Parse(list.SelectedValue);
EmployeeData manager = new EmployeeData();
if (managerID != -1) {
List<EmployeeData> managers = EmployeeList.LoadRecord(managerID);
manager = managers[0];
}
Session["Manager"] = manager;
}
}
The reason the manager is cleared is because the original class instance is not used. A new instance is created during insert or update and the other properties are initialized from what is available in the DetailsView. Unfortunately sub-classes are not handled because no way was provided in the current architecture to handle them automatically.
In fact the entire concept of sub-classes and foreign key relationships is not addressed in any way in any part of the entire .NET framework development tools. It appears Microsoft never even considered it a requirement and just skipped it altogether. Unfortunately it is a very real requirement in all applications and thus you'll notice that no matter what method you use to work with a webpage the easy approach is always messed up by trying to come up with an elegant solution to this problem.
When an employee is selected from one of the drop down lists the DropDownList1_SelectedIndexChanged or DropDownList1_DataBound is called, which I use to call SetManager. SetManager intializes and loads an EmployeeData record for the selected EmployeeID and puts it in the cache so its available to the odsCurrentProject_Updating and odsCurrentProject_Inserting events.
Conclusions
This tutorial demonstrates the issues of dealing with a subclass in your business logic layer. If you try to keep your BLL clean by not adding unnecessary subclass handling code you end up with the mess we have here in your webpage code.
The final code we have in the WebPage partial class is less than ideal and becomes much more complicated as the number of subclasses grows, especially if more than one subclass of the same class type exists in your primary class.
While the core class and methods used here are a good starting point I would not recommend the process I used here for keeping the Manager in sync with the Project. In the next tutorial I'll go over the same code and the same basic webpage but with a cleaner solution.
See next Tutorial...Project 2: Custom Business/DataAccess objects including subclasses for handling foreign key relationships.