• 设为首页
  • 点击收藏
  • 手机版
    手机扫一扫访问
    迪恩网络手机版
  • 关注官方公众号
    微信扫一扫关注
    公众号

[Reference]NHibernateDataSource:ADataSourceControlforASP.NET2.0

原作者: [db:作者] 来自: [db:来源] 收藏 邀请

NHibernateDataSource: A DataSourceControl for ASP.NET 2.0
http://www.codeproject.com/useritems/NHibernateDataSource.asp

Background

We've used NHibernate for two years and Hibernate for 4 years. All of it has been web applications using JSP / JSF or ASP.NET. In Java, we wrote a 300 table application with lots of complex relationships, lazy loading, and caching. It performed better than anything we'd written before. Surprisingly, no SQL / JDBC was necessary except for 2 stored procedure calls to do some heavy ETL work. Our ASP.NET applications have been small to mid-sized, and at this point I'd say that NHibernate is a suitable persistence solution for nearly any size of web application.

With the advent of .NET 2.0, we were very excited about the new DataBoundControls and 2-way binding in ASP.NET 2.0. However, we quickly grew tired of ObjectDataSource and also bumped into some annoying constraints with 2-way databinding. For a little while we hobbled along with ODS and were able to work around some problems using MyObjectDataSource from Mike at vaultofthoughts.net. However, it eventually dawned on us that we ought to be able write a DataSourceControl for NHibernate and bypass all the extra code we were writing in order to use ObjectDataSource. When we found that Paul Wilson had done the same thing for his ORM, we set out to write an NHibernateDataSource.

Benefits of NHibernate

I won't go into too much detail about the benefits of Object Relational Mappers (ORMs), but the big ones we see with NHibernate are:
  • Abstraction from the SQL implementation. For most of our applications we haven't had to write any SQL. Schema changes have been a snap and our bug counts are significantly lower because poor SQL and ADO / JDBC code has historically been the source of many of our bugs.
  • Cascading selects and updates. Greatly simplifies loading and saving of data that spans multiple tables. Of course we still use the occasional stored procedure to do big bulk updates and other heavy lifting.
  • Performance improvements. Smart outer joins + lazy loading + caching = great performance.

The Scope of This Article

This article assumes that the reader is familiar with NHibernate.

If you aren't familiar with NHibernate, it may still be useful in that it provides
  • several demonstrations of the basic features and benefits of NHibernate
  • a useful class for converting an IList to a DataTable
  • a decent example of a DataSourceControl control.

For an NHibernate tutorial, however, you should look elsewhere.

On Decoupling UI from the ORM layer

The title of the article should have told you that the UI will be aware that NHibernate is the ORM. If you are of the mind that every application must isolate the UI from any specifics regarding the data access implementation, including the ORM used, you are not going to like this approach. If you want strict separation between data and web layers, you may want to check out Billy McAfferty's article. He creates a GenericDAO that can be used to create a data access layer, using an IGenericDAO interface to make the UI agnostic of the actual NHibernate implementation behind the curtain. You could probably wire up an ObjectDataSource to it relatively easily.

If you're not sure, keep reading. At the end of the article I summarize our rationale for the coupling decision.

Overview

Infrastructure

What's presented here is the NHibernate component of the framework that our group has developed to be the foundation of all of our web applications. It should be easy to decouple the NHibernateDataSource from the infrastructure and utility classes we wrote, but in the interest of time and utility, I'm including several other useful classes.

  • NHibernateUtils provides a static way to load and configure an NHibernate Session factory, with support for dynamically generating mapping configuration from attributes.
  • SessionHolder provides static access to a Session object. Can be request-scoped for web apps or thread-scoped for client apps.
  • NHibernateModule closes the session when the request ends.
  • NHibernateTemplate is a little utility class provides a few convenience methods and simple transaction handling
  • Model Base Classes (IdentifiedObject, NamedObject) are not required but can be used as a base class for entities. They simply override Equals() and GetHashCode() for better collection support.

NHibernateDataSource

  • Basic features include 2-way binding, filtering, and sorting.
  • Advanced features include paging, nested 2-way binding, generating a DataTable, binding to ValueTypes, binding to queries that return multiple columns, and binding entities to many-to-one properties.

Acknowledgements

The Northwind Model, both the class files and the mapping document, was originally generated with Object Mapper.

As we have written our library we have incorporated code made freely available by several generous individuals including:

NHibernateDataSource

Setup

By default, the NHibernateDataSource uses the ISessionFactory and ISession management infrastructure that we wrote, but NHibernateDataSource can be easily decoupled from it. All you have to do is provide the ISession or the ISessionFactory.

Here's how it works...

  • NHibernateDataSource needs to be able to get an ISession. You can provide the ISession yourself in one of two ways. You can extend and override CreateSession(), or you can handle the CreatingSession event and set the Session property on the NHibernateDataSource. Otherwise, SessionHolder.Current will be used.
  • If SessionHolder is used, it needs an ISessionFactory. If SessionHolder.SessionFactory is set, it will be used. You can also set SessionHolder.Interceptor with an interceptor to be added when creating a new ISession. Otherwise it will use NHibernateUtils to attempt to find and load a configuration.
  • If NHibernateUtils is used to load the Configuration...It will first check for configuration in the App Config. The nhibernate.config property may specify the path to an external nhibernate.config file. Otherwise NHibernateUtils uses NHibernate's default configuration handling by calling configuration.Configure(). The App Config may contain a nhibernate.mapping.attributes.assemblies property specifying one or more assemblies that contain attributes that should be used to generate a mapping configuration.

Class Diagram

Basic Usage

Using with GridView

Just set the TypeName and go.

<cp:NHibernateDataSource runat="server" ID="dsProducts"
TypeName="Northwind.Model.Product" />

Out of the gate you get sorting, updating, and deleting. To specify a default sort, set the DefaultSortExpression property.

<cp:NHibernateDataSource runat="server" ID="dsProducts"
TypeName="Northwind.Model.Product"
DefaultSortExpression="ProductName ASC" />      
Demo page: Products/Default.aspx.

Using with FormView

  1. Set the TypeName.
  2. Add a QueryStringParameter. Because AutoGenerateWhereClauseFromSelectParams is enabled by default, the data source automatically generates and appends the where clause fragment "WHERE Id = :Id".
<cp:NHibernateDataSource ID="dsProduct" runat="server"
TypeName="Northwind.Model.Product">
<SelectParameters>
<asp:QueryStringParameter
Name="Id" QueryStringField="Id" />
</SelectParameters>
</cp:NHibernateDataSource> 

Now all we have to do is wire it up to a FormView.

Demo page: Products/Edit.aspx.

GridView from a Query

Since the TypeName ultimately drives the generation of the query "Select o from [DataObjectType]", you can just as easily supply the query yourself by setting the Query property, in which case TypeName will be ignored.

<cp:NHibernateDataSource ID="dsOrders" runat="server"
Query="select o from Northwind.Model.Order as o where o.Customer.Id = :id "
DefaultSortExpression="OrderDate">
<SelectParameters>
<asp:QueryStringParameter Name="id"
QueryStringField="Id" />
</SelectParameters>
</cp:NHibernateDataSource> 

Notice that we still do not specify the ORDER BY clause in the query because we want to allow the NHibernateDataSource to change the ORDER BY clause depending on the sorting arguments that are passed to it.

Demo page: Products/Query.aspx

Advanced Usage

We've covered the basics. You could stop right here and what we have so far will go along way for basic CRUD operations. What follows is more advanced, a little confusing, and a little experimental, but it can be very useful if you need it.

Building a Search / Filter screen

One common use case for us is a screen that basically consists of a results list and a number of controls that specify criteria by which the data should be filtered / searched. We realized that the same features that enabled FormView binding could be used to enable the declarative creation of such a screen. In fact, they were already there. Here's an example.

I have an Order Search screen where I want to be able to specify a OrderDate to search for.

The steps are:

  1. Create the GridView and txtOrderDate
  2. Create the NHibernateDataSource
  3. Set TypeName
  4. Add a ControlParameter to the text box.
    <SelectParameters>
        <asp:ControlParameter ControlID="txtOrderDate"
        Name="OrderDate"
        PropertyName="Text" />
        </SelectParameters>

Just as when we bound the FormView to the "Id" in the QueryString, the OrderDate control parameter will generate the where clause fragment "WHERE OrderDate = :OrderDate".

But what if the user doesn't specify an OrderDate (i.e she wants to see all orders)? The query would break, because there would be a where clause without the necessary parameters. This is resolved by the ExcludeEmptySelectParameters property, which defaults to being true. If set to true, when a parameter value is null or empty, no WHERE clause is generated for it.

Get Finer-grained Control with ParameterExtenders

Let's say I want to search within a date range rather than search for a single date. Instead of txtOrderDate, I would have two controls: txtStartDate and txtEndDate.

My SelectParameters would look like this:

<SelectParameters>
<asp:ControlParameter ControlID="txtStartDate"
Name="StartDate"
PropertyName="Text" />
<asp:ControlParameter ControlID="txtEndDate"
Name="EndDate"
PropertyName="Text" />
</SelectParameters>

However, there are a couple of problems.

  1. There is no property called StartDate, nor is there an EndDate.
  2. This is a range, so I want my where clause to be something like "WHERE o.StartDate >= :StartDate AND o.EndDate <= :EndDate".

This is where ParameterExtenders come in.

I can add extra information about my parameters by adding items to the ParameterExtenders collection of my NHibernateDataSource.

<ParameterExtenders>
<cp:ParameterExtender Name="StartDate"
WhereCompareOperator=">="
PropertyName="OrderDate" />
<cp:ParameterExtender Name="EndDate"
WhereCompareOperator="<="
PropertyName="OrderDate" />
</ParameterExtenders> 

My parameter allows me to map the StartDate parameter to the OrderDate property and also lets me specify the operator that will be used for comparison.

Keep reading through the following 2 sections to see what the final data source declaration will look like.

Paging

If you are familiar with GridView and DataSource controls, you know that GridView can page, and it can either do its own pagination, or it can pass it back to a DataSourceControl control, potentially allowing the developer to push the paging all the way back to the database, thus eliminating unnecessary data retrieval.

If you are familiar with NHibernate you know that IQuery can page its results.

This would suggest that pushing pagination back to NHibernate would be easy, but there is one small kink:

  1. If NHibernate pages its results, it cannot return a count of the total result size. This means that while DataSource.CanPage is true, if paging is enabled, then DataSource.CanRetrieveTotalRowCount is false.
  2. In such a situation where DataSource.CanPage is true, but DataSource.CanRetrieveTotalCount is false, then GridView falls back on its own paging mechanism, which retrieves all the results.
  3. So, the only way to push paging to NHibernate is if we issue a second query. (it seems to me that this is something that NHibernate ought to be able to do automatically, but I'm no NHibernate developer)

We resolve the problem by finding the SELECT clause in the query and replacing it with "Select count(*)". This works in most cases, but not in all cases.

For this reason, this paging feature is disabled by default. To enable it, simply set EnablePaging to true. Once this is enabled, AutoGenerateCountQuery, which defaults to being true, determines whether NHibernateDataSource should generate the count query itself. If you want to create the count query yourself, you can handle the Selecting event and look to see whether NHibernateSelectingArgs.ExecutingSelectCount is true, and then set the Query property to the appropriate count query.

Wrapping Query Results in a DataTable

NHibernateDataSource can wrap its results in a DataTable in order to ease complex binding scenarios. Simply set GenerateDataTable to true. This is useful in several scenarios involving the limitations of ASP.NET's 2-way binding mechanism.

Binding a GridView to multiple NHibernate Query result columns

HQL allows us to receive the exact columns that we need. This can be helpful in scenarios where we want to present a table of data that involves multiple entities / tables, but we don't want to incur the cost of fully loading all of those entities. For example the query

Select o.OrderDate, o.Customer.CompanyName
from Northwind.Model.Order o

In this case, the NHibernate IQuery will return an IList of object arrays, where the elements in the array correspond to the columns returned. This is a useful feature of NHibernate, but there is no readily available way to bind the object array to a GridView.

The solution is to take the IList<object[]> returned by the IQuery and convert it to a DataTable. GenerateDataTable does this, automatically generating column names based on the array index, i.e. "Column0", "Column1", etc.

Our final declaration looks like this:

Collapse

Demo page: Orders/Default.aspx

Binding a DropDownList to a List of Primitives

While creating the demo, I wanted to be able to filter a customer list by the customer's country. There is no Country table in Northwind and thus no Country entity. So in order to get a list of countries, we must do a select distinct on the Country property of customer. This is quite easy to do with an NHibernate query:

Select distinct c.Country from Customer c order by c.Country

However, there will be a problem binding our DropDownList to the results, which will be of type IList<string>. Given a DataSourceID to bind to, DropDownList needs to know which property to bind to for the value / text of each entry. But primitive value types like string do not have such a property. This is where GenerateDataTable comes in handy. It will simply create a DataTable with one column, "Column0", which will contain each string value. We can then bind SelectedValue to Column0:

<cp:NHibernateDataSource runat="server" ID="dsCountries"
Query="Select distinct c.Country from Customer c order by c.Country"
GenerateDataTable="True"
/>
<asp:DropDownList ID="ddlCountry" runat="server"
DataSourceID="dsCountries"
DataValueField="Column0"
AutoPostBack="true"
EnableViewState="false">
</asp:DropDownList>                       
Demo page: Customers/Default.aspx

Fine-grained Control over the WHERE Clause

Until now, the NHibernateDataSource has been generating where clauses from its SelectParameters. Behind the scenes, when NHibernateDataSource processes its SelectParameters, it generates a QueryFragment for each parameter. Later, when the HQL is generated, the QueryFragments, if any are present, are put together to form the WHERE clause.

In addition to generating these QueryFragments from the SelectParameters, you can explicitly create any number of WhereClauses that may have multiple different parameter dependencies. Dependencies are used to determine whether or not a QueryFragment should be included in the WHERE clause, based on the presence or absence of the parameters upon which it depends. If a named parameter is not set, then any where clause fragments depending on it will not be included in the query. You can also have negations (by prepending with "!"), for which a where fragment will only be included if the parameter dependency is NOT present.

There is no demo of this, but it has been very useful for some filter screens.

Very Advanced / Experimental Usage

This stuff is kind of experimental and generally involves more sophisticated uses of GenerateDataTable.

ObjectDataTableAdapter

The implementation behind NHibernateDataSource.GenerateDataTable is ObjectDataTableAdapter, which is not coupled to NHibernate and is a useful class in its own right. ObjectDataTableAdapter could merit its own article, so we won't go into too much detail here.

The purpose of ObjectDataTableAdapter is to take an IList of objects and generate a DataTable for them. It also provides methods for updating the data in the list.

The strategy for generating the DataTable depends on the type of elements contained in the IList

  • primitives / value types - a single column, "Column0" will have the same Type as the element type and will contain the value of the element.
  • object[] - typed columns will be generated corresponding to the array index, i.e. "Column0", "Column1"
  • other objects / entities - typed columns will be generated by reflecting on the type and values populated by reflecting on the value.

Also, the ObjectDataTableAdapter can be configured to generate additional columns which can be aliases of complex properties, i.e. nested and indexed properties that are usually off-limits to .Net DataBinding. NHibernateDataSource exposes a DataTableConfig property to allow design-time configuration of rules determining how the ObjectDataTableAdapter will generate the DataTable.

So, not only can we wrap a list of primitives or object arrays in a DataTable, we can wrap a list of entities, and we can generate columns for nested properties like "Order.Customer.CompanyName". This has some interesting applications.

Bind to nested properties

The example in the demo is a somewhat ridiculous scenario, but it shows what can be done. If I am editing a customer, and for some inane reason, I want to be able to change the date of his first order, I can put the entire customer into a DataTable, adding a column that is an alias to the Customer.Orders[0].OrderDate> property.

My data source would look like this:

<cp:NHibernateDataSource ID="dsCustomer" runat="server"
TypeName="Northwind.Model.Customer"
GenerateDataTable="true"
>
<DataTableConfig>
<Columns>
<cp:PropertyColumn ColumnName="FirstOrderDate"
PropertyName="Orders[0].OrderDate"
TypeName="System.Nullable`1[System.DateTime]" />
</Columns>
</DataTableConfig>
<SelectParameters>
<asp:QueryStringParameter Name="Id"
QueryStringField="Id" Type="String" />
</SelectParameters>
</cp:NHibernateDataSource>

And in my FormView I would simply bind a TextBox to my aliased property:

<asp:TextBox ID="txtOrderDate" runat="server"
Text='<%# Bind("FirstOrderDate", "{0:d}") %>'></asp:TextBox>        
Demo page: Customers/Edit.aspx

Bind to Many-To-One Entities

Another common use case when using a domain model and NHibernate is the need to set a many-to-one property, i.e. a property that is not a primitive type, using a selection control like DropDownList. Say, for example, I want to edit a product and set its Category. Typically this would involve populating a DropDownList with values such that SelectedValue is the Id of the category. Then, when I postback, I have to programmatically find the corresponding Category entity and set it as the Product.Category property. Alternately, you could make SelectedValue actually store the entire Category entity, which would require it to be serialized. Sometimes this is feasible for small "lookup" objects, but it is not so for larger entities.

Our solution was to add a funky column mapping option to our DataTableConfig. It seems a bit odd at first, but it solves the many-to-one binding problem in a snap. We can bind to one property and unbind to a different one. In this case, we will create an alias column called "CategoryId". When we bind ("get" the property), it will use Product.Category.Id. When we unbind ("set" the property), it will set the Product.Category property, and NHibernateDataSource will be smart enough to know that when it is told to set a property whose Type is a mapped NHibernate entity, it should assume that the value is its Id and load the corresponding object.

The datasource looks like this:

<cp:NHibernateDataSource ID="dsProduct" runat="server"
TypeName="Northwind.Model.Product" GenerateDataTable="True">
<SelectParameters>
<asp:QueryStringParameter Name="Id" QueryStringField="Id" />
</SelectParameters>
<DataTableConfig>
<Columns>
<cp:PropertyColumn ColumnName="CategoryId"
PropertyName="Category.Id" UpdatePropertyName="Category" />
</Columns>
</DataTableConfig>
</cp:NHibernateDataSource>

And the DropDownList declaration looks like this:

<asp:DropDownList ID="ddlCategory" runat="server"
DataSourceID="dsCategories"
DataTextField="CategoryName"
DataValueField="Id"
AppendDataBoundItems="True"
SelectedValue='<%# Bind("CategoryId") %>'>
<asp:ListItem></asp:ListItem>
</asp:DropDownList>
<cp:NHibernateDataSource ID="dsCategories" runat="server"
TypeName="Northwind.Model.Category">
</cp:NHibernateDataSource>   
Demo page: Products/Edit2.aspx.

Features Not Covered

Events

The events are based on ObjectDataSource's events and are included for completeness. However, they have not been thoroughly tested.

Dependencies

All dependent libraries are included with the code. It basically boils down to:

  • NHibernate 1.2.0 beta 2. This has a number of runtime dependencies, all of which are included in the demo.
  • Spring.NET-1.1.0-P3 which needs Common.Logging

Other versions of these libraries will probably work, but we haven't tested them.

Spring.NET is only required when ObjectDataTableAdapter needs to do reflection on entity properties. It could probably be replaced with another property get/set implementation fairly easily.

Caveats

  • The original code was quick and dirty, undocumented and a little ugly. I've since cleaned it up and added several features, but it is still very much "alpha" code, still in its early stages.
  • We may not exactly be following "best practices" for session and transaction management. That said, we haven't had any problems with our approach.
  • The designer support isn't as rich as for the MS datasource controls. This is something I hope to add. It would be great to add schema access at runtime so that we could provide properties in dropdowns and autogenerate FormView templates. However, this would require instantiating an ISessionFactory whenever RefreshSchema is called at design-time, which might be slow / annoying. However, another developer made a promising comment in the Hibernate Forum that he had created an NHibernate DataSourceControl that provided HQL intellisense at design-time. Very cool stuff - let's hope he is able to open-source it.
  • We've never tried to use this control with NHibernate's versioning support.
  • We're still getting familiar with DataSourceControl s and hope to change the following as we learn more.
    • No ViewState - currently the control does not store anything in ViewState.
    • DataSourceView.OnDataSourceViewChanged is not fired.

Brief Soapbox: Coupling UI to NHibernate

Although we are big fans of good layering and loose coupling, we just don't see enough benefits to decoupling the UI from NHibernate. Here's why:

  • We write web applications and we control the environment. So keeping all deployed instances in sync is easy. If this were not the case, we might want an implementation-agnostic layer of DAO interfaces.
  • Our primary concerns are speed of development and minimal defects. We have found that the best way to ensure this is generally by minimizing complexity and producing simple, terse, readable code. Thus we try to avoid strict layering (which usually requires creating a namespace full of classes that all sort of do the same thing - delegate to an implementation) and the code generation that often comes with it, unless we deem it particularly useful.
  • NHibernate provides great abstraction away from the database schema and dialect, which is usually all the layering we need.
  • We really don't anticipate needing to replace NHibernate with another persistence solution. As long as we are developing against a relational database, NHibernate is a good choice.
  • Were we to replace NHibernate with another ORM solution like DLINQ, we expect the transition to be fairly straightforward. (We'd probably find or write a comparable DataSourceControl for our new ORM mechanism and just switch them out).
  • Were we to replace NHibernate with a non-ORM solution (like web-services), tweaking a little UI code would be the least of our worries.
  • What this all boils down to is that for us, the cost of changing our UI code pales in comparison to the cost of strict layering.

Also, the DataSourceControl is a great way of compartmentalizing data access. When we wrote NHibernateDataSource, it significantly reduced the NHibernate code in our UI. For example, in the demo, there is no NHibernate code in the UI.

About Winston Fassett


Winston is the manager of application development for an international finance and investment banking firm headquartered in Dallas.

He has 9 years of IT experience, including 2 years of business consulting and 7 years of application development in Java and .NET.

When not working, he's usually fiddling around with the mindmapper / outliner he is building on the side, or trying to decide whether he should go to graduate school.

He enjoys playing guitar and keyboard and writing in third person.

Click here to view Winston Fassett's online profile.


鲜花

握手

雷人

路过

鸡蛋
该文章已有0人参与评论

请发表评论

全部评论

专题导读
上一篇:
Asp.Net 上传图片并生成高清晰缩略图发布时间:2022-07-10
下一篇:
AjaxPro.NET框架生成高效率的Tree(Asp.net2.0)(示例代码下载)发布时间:2022-07-10
热门推荐
热门话题
阅读排行榜

扫描微信二维码

查看手机版网站

随时了解更新最新资讯

139-2527-9053

在线客服(服务时间 9:00~18:00)

在线QQ客服
地址:深圳市南山区西丽大学城创智工业园
电邮:jeky_zhao#qq.com
移动电话:139-2527-9053

Powered by 互联科技 X3.4© 2001-2213 极客世界.|Sitemap