|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Announcements
Want a new Job?
Chapters
Services
Feature Zones
|
IntroductionIn Part One of this series, we threw out about 90% of ADO.NET because, as object-oriented programmers, we didn’t need it. For us, ADO.NET is a thin data transport layer, rather than a full-service data management solution. The article showed how to perform basic CRUD operations using ADO.NET to get information to and from a business model. I deliberately kept the code in Part One as simple as possible, so the reader could focus on how ADO.NET is used. But that code was hardly object-oriented. Or, more precisely, it had very little architecture about it. In this article, we will revisit the ADO.NET CRUD operations. Only this time, we will use an application with far better architecture. Once that is done, we will move on to data binding with objects. In some circles, the .NET framework has gotten a bum rap as “good for database front-ends, but not much else”, because so much of it is built around data-driven programming. Datasets are easy, so it is said, and objects are hard. While that may have been true under .NET 1.x, it’s not true any more. Visual Studio 2005 includes new ‘Data Source’ functionality that allows objects to be data bound as easily as data sets. The feature works on just about any object or collection, but it works particularly well on objects and collections that implement certain key interfaces. The demo app attached to this article includes base classes that implement those interfaces. If you derive your data binding objects and collections from these classes, you will get not only basic data binding, but data bound sorting and searching, as well. The Demo ProgramA demo program is attached to this article. The program uses a SQL Server database, which is included in the ‘Database Files’ folder. Attach these files to SQL Server 2005 or SQL Express 2005, using SQL Server Management Studio (or Management Studio Express, which can be downloaded for free from MSDN). A word about the code: To keep things simple, the demo app doesn’t do several things that one should always do in production code. For example, it doesn’t wrap database calls in The demo program maintains a simple list of authors and the books they have written. Its main window shows a master-detail list of authors and books, on two grids:
Items may be added or changed using normal data-grid editing operations. Items may be deleted by selecting one on a grid, right-clicking it, and choosing ‘Delete’ from the context menu. Master-detail views are probably the most frequently used views for reviewing and updating objects in a containment hierarchy. Coding a master-detail view is tedious and time consuming, and prone to errors. Until Visual Studio 2005, there was no easy way to use design-time data binding to ease this chore. Now, with object data binding, you can set up a complete master-detail view, like the one above, in about ten minutes. You can create reports almost as quickly. The ‘View Report’ button in the lower right corner opens a Report window that shows the master-detail report with the same information. We’ll take a look at that window later. Architecture – The Pieces of the PuzzleThe sample application is built around the Model-View-Controller (MVC) design pattern, with a data access layer based on the Data Access Objects (DAO) pattern. The overall approach is similar to what Dr. Joe Hummel has been teaching in his current MSDN webcast series. [Hummel] The goal of the MVC design pattern is to isolate an application’s business model from its user interface. UIs are notoriously unstable, and they have a nasty habit of interfering with business models. Dependencies get tangled up, so that when we change the UI, it breaks our model. MVC starts with the assumption that the business model is the most stable part of an application. For those who maybe new to OOP, a business model is expected to be the most stable part of an application’s code. The model is probably the first part of our application to be rendered in code. And once it’s done, we need to protect it from changes in other parts of the application. The only reason to change the business model should be to more accurately reflect the underlying business process. In other words, we should never need to ‘reopen’ our business model because of changes to the user interface. MVC isolates the model from the user interface by placing a ‘controller’ between the UI and the business model. All requests from the UI to the model are filtered through the controller. And in some versions of MVC, all responses from the business model go through the controller, as well. Other versions allow the UI to respond directly to events fired by the business model. We’ll be using the latter version in the demo app. Another source of application instability is the database back-end. An application may use SQL Server today, but Oracle next year. We may use inline SQL queries to develop the application, but change to stored procedures when the app goes into production. Any of these changes will break the business model if data access is encapsulated in our business objects. Object-oriented designers often encapsulate data access operations in business objects on the grounds that objects should be self-sufficient—an object should know how to load and save its data. But the ‘Single Responsibility Principle’ states that “A class should have only one reason to change.” [Martin] If we encapsulate data access in our business objects, they have two reasons to change:
In order to respect the Single Responsibility Principle, we need to break encapsulation. The DAO pattern is widely used in the Java world to address these issues. The pattern looks something like this:
The business object delegates its data access operations to a companion data access object, which delegates its CRUD operations to a data provider object. The data access object encapsulates the higher level database calls, such as SQL queries or stored procedure calls, while the data provider object performs the actual database operations. The data provider object is built on a .NET data provider and is tightly coupled to it. The data provider object contains most of the ADO.NET code that we use. For example, in a ‘get’ operation, the data provider will create and populate a data set, which it will return to the DAO object. The DAO’s job is to transfer the data from the data set to its companion business object. The MVC and DAO patterns do a good job of isolating a business model from instability in the presentation and data tiers of an application. But MVC has some limitations of its own. Since most communications between application tiers are routed through the Controller, it has the potential to become a bottleneck. To avoid this problem, most MVC designs create a separate controller for each form in the UI. However, if multiple forms need to make the same request of the business model, the code to dispatch that request will need to be added to both controllers. That sort of code duplication is what some writers call a ‘code smell’, as in “that code smells pretty bad” [Beck and Fowler]. The Command pattern, one of the original ‘Gang of Four’ patterns, addresses these issues. Each request the UI can make is encapsulated in a separate Command object. These objects are processed by the Controller—which is reduced to a lightweight command processor that can handle the entire application. Likewise, each Command object is a very simple and lightweight object. And if two different forms need to pass the same request, they simply instantiate the Command for that message and pass the command to the Controller. This approach eliminates code duplication—if we have to change the command, we have only one place to go. Our code smells much better. In addition to cleaning up a design, the Command pattern makes it very easy to implement unlimited Undo. We won’t implement this feature in the demo app, but if you would like to see how it works, you will find my article on the subject here. Architecture—Putting It All TogetherAs I mentioned above, the demo app manages an Authors list. The list is a two-level containment hierarchy, similar to the Projects list we looked at in Part One. An Authors collection sits at the top of the hierarchy, and each Author in the collection has a Books collection. Let’s take a look at how all this stuff works in code. Processing a request from the UI resembles a journey through our application’s architecture, more than the simple execution of an algorithm. We start in FormMain, our app’s UI. A quick examination of the code reveals nothing but event handlers, and each is just a couple of lines long. That’s in keeping with the UI’s role in a .NET app: to manage the display, and dispatch user input to the Controller. The UI should never do any processing itself. Set a breakpoint at the top of the The form-load handler starts the process of loading the list by creating a command object to perform the request and passing that object to the Controller: // Get authors list
CommandGetAuthors getAuthors = new CommandGetAuthors();
m_Authors = (AuthorList)m_AppController.ExecuteCommand(getAuthors);
The Controller invokes the command object’s public override object Execute()
{
AuthorList authors = new AuthorList();
return authors;
}
The command object instantiates a new public AuthorList()
{
AuthorListDAO dao = new AuthorListDAO();
dao.LoadAuthorList(this);
}
The public void LoadAuthorList(AuthorList authorList)
{
// Build query to get authors and their books
StringBuilder sqlQuery = new StringBuilder();
sqlQuery.Append("Select AuthorID, LastName," +
" FirstName, SSNumber From Authors; ");
sqlQuery.Append("Select BookID, SkuNumber," +
" AuthorID, Title, Price From Books");
// Get a data set from the query
DataSet dataSet =
DataProvider.GetDataSet(sqlQuery.ToString());
// Create variables for data set tables
DataTable authorsTable = dataSet.Tables[0];
DataTable booksTable = dataSet.Tables[1];
// Create a data relation from Authors
// (parent table) to Books (child table)
DataColumn parentColumn = authorsTable.Columns["AuthorID"];
DataColumn childColumn = booksTable.Columns["AuthorID"];
DataRelation authorsToBooks = new
DataRelation("AuthorsToBooks", parentColumn, childColumn);
dataSet.Relations.Add(authorsToBooks);
// Load our AuthorList from the data set
AuthorItem nextAuthor = null;
BookItem nextBook = null;
foreach (DataRow parentRow in authorsTable.Rows)
{
// Create a new author
bool dontCreateDatabaseRecord = false;
nextAuthor = new AuthorItem(dontCreateDatabaseRecord);
// Fill in author properties
nextAuthor.ID = Convert.ToInt32(parentRow["AuthorID"]);
nextAuthor.FirstName = parentRow["FirstName"].ToString();
nextAuthor.LastName = parentRow["LastName"].ToString();
nextAuthor.LastName = parentRow["LastName"].ToString();
nextAuthor.SSNumber = parentRow["SSNumber"].ToString();
// Get author's books
DataRow[] childRows = parentRow.GetChildRows(authorsToBooks);
// Create BookItem object for each of the authors books
foreach (DataRow childRow in childRows)
{
// Create a new book
nextBook = new BookItem();
// Fill in book's properties
nextBook.ID = Convert.ToInt32(childRow["BookID"]);
nextBook.SkuNumber = childRow["SkuNumber"].ToString();
nextBook.Title = childRow["Title"].ToString();
nextBook.Price = Convert.ToDecimal(childRow["Price"]);
// Add the book to the author
nextAuthor.Books.Add(nextBook);
}
// Add the author to the author list
authorList.Add(nextAuthor);
}
// Dispose of the data set
dataSet.Dispose();
}
The code should be pretty self-explanatory, if you read the Part One of this series. The Note that the DAO object directly sets the properties of the Now, even though the DAO object is a pretty hefty chunk of code, you may have noticed that it never created a data set or a data adapter. It simply passed an SQL query to a Delegating to a Joe Hummel argues that this benefit provides a strong justification for using inline queries instead of stored procedures. I think he may have a point. Joe does caution that inline queries need to be properly escaped to protect against SQL injection attacks, and he acknowledges that stored procedures do have a performance advantage over inline queries. For more information, see session 6 of his current webcast series. The two-level data access approach, using inline queries or stored procedures, is particularly useful for frameworks that you might use in projects for different clients. Consider a client with three install sites: Site A uses SQL Server, Site B uses Oracle, and the database administrator at Site C says I will add stored procedures to the site’s SQL Server over his or her dead body. I can use stored procedures with SQL Server and Oracle data provider objects for the first two sites, and inline queries with a SQL Server data provider for Site C. And I don’t have to touch the business model or the UI to get any of this done. We won’t reproduce the The creation, updating, and deletion operations work the same way—we create a command object, pass it to the Controller, and the Controller processes the command. In most cases, the command makes a request of a business object, which delegates the request to a DAO object. In the demo app, CRUD operations are triggered by events that originate in the Architecture—What’s the Point?That’s a lot of objects to go through just to get an Authors list. Wouldn’t it be a lot simpler to just pass the data set? In a simple case like the demo app, of course, it would. Object-oriented design imposes a certain amount of overhead, which should be considered before designing a project. OOP is probably overkill in very simple applications. But consider a client company that uses a General Ledger with three or four levels of subledgers extending from a number of different accounts. Those of you who write financial and accounting applications know that’s not an unusual case. I wouldn’t want to try to keep that business process straight using data sets. As my wife said in Part One, “They’re fine when they work, but you can’t build a Swiss watch with them.” When something changes in my project, the changes should be localized to one layer. That means changes to one layer won’t affect any other layer. I could move this application to the web, and I wouldn’t have to do much, if anything, to the business or data layers. Likewise, I could change databases, and I wouldn’t have to do much, if anything to the UI and business layers. And the business layer is isolated from all changes except those involving the business model. Of course, if the business model changes significantly, all bets are off. But that’s the layer least likely to change. Databinding in the UI LayerAnd so, at long last, we get the Authors list back to the UI. What do we do with it then? In the old days of .NET 1.x, a lot of us used to dump the list back into a data set, which we would bind to a grid for display and update. That’s just crazy. Or, we would use a third-party grid with a decent unbound mode, and we would hand-wire that to the list. That’s painful, at best. Visual Studio 2005 provides a new ‘Data Source’ functionality to .NET. It’s not a control—a What that means is that, for the most part, we can forget about writing data binding code. We don’t need it. About 90% of the work involved in displaying a business model can now be done at design time. And that means that instead of grinding out code, we get to mess about in a Designer. I don’t know about you, but I know which I’d rather do. Since the work is done in design mode, let’s walk through the steps involved, rather than reprinting code. First, lay out your form. For the demo app, we need a master-detail display, so we use two Next, create data sources for your business objects. I used ‘Add New Data Source’ from the Data menu, although you can do the same thing from the Data Sources dockable window. Select ‘Show Data Sources’ from the Data menu to open it. I created data sources for the Next, bind the business objects to the Next, configure each grid. Select ‘Edit Columns’ from either the context menu or the Smart Panel for a I would definitely suggest changing the Once you have finished formatting your grids, it’s time to write a couple of lines of code. The design-time binding we did earlier bound the grid to our object structure, but not its data. In database terms, all we did was bind the objects’ schemas. So, now we need to bind for run time: // Bind grids
bindingSourceAuthors.DataSource = m_Authors;
bindingSourceBooks.DataSource = bindingSourceAuthors;
bindingSourceBooks.DataMember = "Books";
These lines appear at the end of the And that’s all there is to it—three lines of code. The grids in the demo app support data binding adds, changes, and deletes just as they would if they were bound to a data set. We get all of the gain with none of the pain. Data Binding and BindingList<T>One of the cool things about the Even though you can use just about any collection as a I’m not going to spend a lot of time on The demo app includes two abstract classes, The By the way, any UI control that supports data binding also supports data source binding. That means business object properties can be bound to text boxes, labels, and other controls. And one final note about Data Binding ReportsVisual Studio 2005 contains a new The demo app contains a master-detail report showing each author on a separate page, along with the books written by that author. You can see the report by clicking the ‘View Report’ button on the main form:
Here is how to build a master-detail report like the one in the demo app: start by creating the report. A report is added to a project just like any other item. Right-click the project name in the Solution Explorer and select Add > New from the context menu. The ‘Add New Item’ box will appear; select ‘Report’ and give your report a name. The demo app’s report is named ‘AuthorsReport.rdlc’. Once the blank report is open, drag items onto it from the toolbox. For starters, we will need some sort of repeating container to hold author information. Since we want a free-form page with one author per page, rather than a grid, we will use the There are no label controls; labels are added using text box controls, which you type directly into. Object fields can be dragged directly from the Data Source window onto the list. For the master report, I dragged out some labels and fields. Then I dragged out a ‘ To show the apps, we need to create a subreport—we can’t use simple grouping. The first step in creating a subreport is to put a placeholder in the main report. Drag a Next, create the actual subreport. It is simply another report file, and it is set up using the same techniques as for the main report. Our demo app subreport is named ‘BooksSubreport.rdlc’, and we have set it up using a Next, set up the communications from the main report to the subreport. Visual Studio 2005 doesn’t have the ability to pass fields directly from a report to a subreport, so we are going to have to provide a little plumbing to do that. To start, add a parameter to the subreport. The parameter will generally be the To add the parameter, make sure the subreport is visible in the Designer. Select ‘Report Parameters’ from the Report menu, and the ‘Report Parameters’ box will appear. Add a parameter for the ID to the list on the left of the box. I added a parameter called ‘ Now connect the main report to the subreport. To do that, reopen the main report. Right-click on the gray subreport block and select ‘Properties’ from the context menu. On the General tab, select the subreport file in the ‘Subreport’ drop-down. The two reports are now connected. Next, switch to the Parameters tab. We are going to link the parameter we created in the subreport to a field in the current record of the main report. Enter the name of our subreport parameter ( We’re just about done. The next step is to create a viewer for the report. Each report is bound to a separate report viewer control. For the demo app, I simply added a As with the private void FormReport_Load(object sender, EventArgs e)
{
// Get author list
CommandGetAuthors getAuthors = new CommandGetAuthors();
m_Authors = (AuthorList)m_AppController.ExecuteCommand(getAuthors);
// Subscribe to report viewer's SubreportProcessing event
reportViewer1.LocalReport.SubreportProcessing += new
SubreportProcessingEventHandler(LocalReport_SubreportProcessing);
// Bind report to authors list
bindingSourceAuthors.DataSource = m_Authors;
// Refresh report
this.reportViewer1.RefreshReport();
}
Note that the handler gets the Authors list using the same Command class that the main form used. If we were using traditional MVC, with separate controllers for the two forms, there would be code duplication, and one can see where our code could start getting tangled up. As it is, we create a Note also that we must subscribe to the The After subscribing to the That work gets done in the private void LocalReport_SubreportProcessing(object sender,
SubreportProcessingEventArgs e)
{
// Use author ID from event args to get project
int authorID = Int32.Parse(e.Parameters["AuthorID"].Values[0]);
AuthorItem currentAuthor = m_Authors.GetAuthor(authorID);
// Set author's Books property as subreport data source
ReportDataSource dataSourceAuthorsBooks = new
ReportDataSource("ObjectDataBinding_Model_BookList",
currentAuthor.Books);
e.DataSources.Add(dataSourceAuthorsBooks);
}
The Next, we use the ID to get the Note that the name we pass to the All-in-all, it’s a bit of a fire drill, and it’s not Microsoft’s most elegant piece of work. But after you have set up a couple of master-detail reports, it’s no big deal. After all, we’re only talking about four lines of code! And the results are certainly worth it. ConclusionThat wraps up our tour of ADO.NET for object-oriented programmers. Over the course of this article, we have seen:
You are welcome to use the References
| ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||