DataSources, Dynamic Data, and SoC
Abstract
This article demonstrates how ASP.NET Dynamic Data combined with Versatile DataSources allows new ways to build data entry web forms with an emphasis on separation of concerns between user interface and business objects.
DataSources, Dynamic Data, and SoC
A good strategy for application development involves Separation of Concerns (“SoC”). Build objects specific to a purpose and let other objects consume them without knowing the details of their underlying architecture. SoC is just as appropriate to building ASP.NET data entry web forms as anything else, but until recently there weren’t very good APIs to assist you. Now ASP.NET Dynamic Data and the attributes of System.ComponentModel.DataAnnotations are available and can help.
- System.ComponentModel.DataAnnotations namespace provides attributes to describe the business rules on each property of an object.
- ASP.NET Dynamic Data converts those attributes into ASP.NET web controls.
Dynamic Data employs two other technologies to construct a web form:
- DataBound controls – Containers for the data entry areas of your web form, providing read-only and editable ways to interact with properties of an object supplied to it. These controls provide DataBinding between the object containing data and the controls they host. Examples include GridView, DetailsView, ListView, and FormView.
- DataSource controls – Used by DataBound controls to get and set the object containing data. Examples include SqlDataSource (supports ADO.NET), LinqDataSource (supports LINQ to SQL), and EntityDataSource (supports ADO.NET Entity Framework.)
The “object” that is passed between DataBound and DataSource controls is often referred to as an “Entity”. It generally mimics the structure of a table with its properties matching the columns of the table. A single Entity is one record in the table.
The “Table” concept itself is a specific type of Entity. Not all data entry web forms interact with tables. Consider a web form which gathers information to send an email. It too has an Entity object and in theory you can use these Entities with ASP.NET Dynamic Data. In a data entry web application that imposes a strong separation of concerns, you probably have one or both types of classes.
Entity as a Table
public class Category
{
public Category() { }
public int CategoryID { get; set; }
public string CategoryName { get; set; }
public string Description { get; set; }
public byte[] Picture { get; set; }
}
Entity to send an email
public class EmailGenerator
{
public EmailGenerator() { }
public string FromEmailAddress { get; set; }
public string ToEmailAddress { get; set; }
public string Subject { get; set; }
public string Body { get; set; }
public void Send()
{
MailMessage mailMessage =
new MailMessage(FromEmailAddress, ToEmailAddress);
mailMessage.Subject = Subject;
mailMessage.Body = Body.ToString();
SmtpClient client =
new SmtpClient("mail.mycompany.com");
client.UseDefaultCredentials = true;
client.Send(vMailMessage);
}
}
Let’s give a name to each of these Entity types:
- DAO Entity – DAO stands for “Data Access Object”. A DAO class handles the CRUD (“Create, Read, Update, and Write”) actions that deliver the DAO Entity instances between the consumer (user interface, web service, etc) and the database.
- POCO Entity – POCO stands for “Plain Old CLR Object”. A POCO class is any class, although in this case, it’s designed with properties to store some criteria that is consumed by an action method. The action method is either built into the class, like the Send() method above, or is in a separate class, making the POCO class an argument passed in to the action method.
Our goal is to use both types of entities with Dynamic Data while pursuing a strong separation of concerns. For both, the web form’s overall structure is basically the same.
<asp:DynamicDataManager ID="DDM" runat=server >
<DataControls>
<asp:DataControlReference ControlID="FormView1" />
</DataControls>
</asp:DynamicDataManager>
<asp:FormView ID="FormView1" runat="server" DataSourceID="DataSource1">
See the chart below
</asp:FormView>
<asp:someDataSource ID="DataSource1" runat="server">
</asp:someDataSource>
You populate the Template properties of the DataBound control with DynamicControl controls and other web controls.
Editing the Category DAO Entity
<EditItemTemplate>
Category Name:
<asp:DynamicControl ID="Name" runat="server"
DataField="Name" Mode="Edit" />
Description:
<asp:DynamicControl ID="Description" runat="server"
DataField="Description" Mode="Edit" />
Picture:
<asp:DynamicControl ID="Picture" runat="server"
DataField="Picture" Mode="Edit" />
<asp:Button ID="Submit" runat="server" Text="Save" CommandName="Update" />
</EditItemTemplate>
Editing the EmailGenerator POCO Entity
<EditItemTemplate>
Your Email Address:
<asp:DynamicControl ID="From" runat="server"
DataField="FromEmailAddress" Mode="Edit" />
Subject:
<asp:DynamicControl ID="Subject" runat="server"
DataField="Subject" Mode="Edit" />
Message:
<asp:DynamicControl ID="Body" runat="server"
DataField="Body" Mode="Edit" />
<asp:Button ID="Submit" runat="server" Text="Send" CommandName="Update" />
</EditItemTemplate>
The DynamicControl uses metadata about each property, coming from the Attributes on the property like RequiredAttribute and DataTypeAttribute, and from the property’s type (int, string, etc). It selects a predefined User Control called a Field Template that has been setup with exactly the web controls you want for that data type. As a User Control, you can easily customize its interface.
Dynamic Data could not handle either of these cases until now. The DAO Entity case was available if you allowed the web control developer to describe the query on the LinqDataSource or EntityDataSource. That breaks the goal of separation of concerns. The DataSource control should always delegate the query to the business object.
The POCO Entity case simply did not exist.
The problem: There needs to be a DataSource control capable of:
- Supporting a strong separation of concerns
- Supporting Dynamic Data
Today there are two sets of solutions. Microsoft is working on the DomainDataSource (now in the preview stage) to handle the DAO Entity case. Dynamic Data has the EnableDynamicData() method for the POCO Entity case. The Versatile DataSources project on CodePlex provides EntityDAODataSource and POCODataSource, one for each type of entity.
DataSource controls for DAO Entity classes
Here are the available DataSource Controls. Use this chart to understand why DataSources of the past do not support the goals of SoC and Dynamic Data.
Control name |
Supported CRUD technologies |
Dynamic Data |
DAO Entity classes |
SoC |
Source |
SqlDataSource |
ADO.NET |
No |
No |
No |
ASP.NET 2 |
OleDbDataSource |
ADO.NET |
No |
No |
No |
ASP.NET 2 |
LinqDataSource |
LINQ to SQL |
Yes |
No |
Not for queries |
ASP.NET 3.5 |
EntityDataSource |
Entity Framework |
Yes |
No |
Not for queries |
ASP.NET 3.5 SP1 |
ObjectDataSource |
Custom – Your own classes |
No |
Yes |
No |
ASP.NET 2 |
DomainDataSource |
LINQ to SQL Entity Framework Custom |
Yes |
Yes |
Yes |
|
EntityDAODataSource |
ADO.NET LINQ to SQL Entity Framework Custom |
Yes |
Yes |
Yes |
“CRUD technologies” refers to a framework that handles CRUD actions: ADO.NET, LINQ to SQL, ADO.NET Entity Framework, and the like.
Choosing between DomainDataSource and EntityDAODataSource
The DomainDataSource or EntityDAODataSource are the two solutions now available. Since they attempt to solve the same problem, here’s a comparison. Since EntityDAODataSource is of my own creation, I certainly hope you see its advantages, but choose the right one for you.
|
DomainDataSource |
EntityDAODataSource |
Base classes for developing Data Access Objects |
LINQ to SQL, ADO.NET Entity Framework. Can be extended to handle ADO.NET |
ADO.NET, LINQ to SQL, ADO.NET Entity Framework |
DAO model |
Domain – 1 class for all Entities |
Per entity |
DAO initial code |
Code Generator creates Domain class |
Inheritance |
Setup |
Very easy |
Very easy |
Supports filters from DataSource |
Yes |
Yes |
Supports sort expression from the DataBound control |
Yes |
Yes |
Supports paging |
Yes |
Yes |
Supports caching queries |
Could not determine |
Yes |
Supports DynamicFilter and QueryableFilterRepeater controls |
Yes – Using QueryExtender control. |
Yes – Directly. No use of QueryExtender control. |
Design mode support |
Limited |
Extensive |
Example
The user wants to display a list of Products and offer search criteria on the Unit Price field. They elect to use the GridView for its quick setup, including the automatic generation of the columns list. The Data Access Object has defined a query method called “SelectPriceRange” which takes the low and high price criteria and other parameters to handle sorting, paging, etc.
The Data Access Object class: Using DomainDataSource
DomainDataSource implements the Data Access Object in the DomainService subclass where the SelectPriceRange() method is defined.
public class LinqToSqlNorthwindDomainService : LinqToSqlDomainService<NorthwindDataContext>
{
[Query()]
public IQueryable<Product> SelectPriceRange(decimal startPrice, decimal endPrice,
string sortExpression, int startRowIndex, int maxRows)
{ … }
}
The Data Access Object class: Using EntityDAODataSource
EntityDAODataSource requires a separate Data Access Object class for each Entity class. The DAO class for Products is called ProductsDAO where the SelectPriceRange() method is defined.
While these two examples are very similar, the big difference is in the last parameter of the SelectPriceRange method, SelectArgs. It allows the Select method to handle sorting, filtering, paging, and caching without making the web form developer be concerned about those details. It also allows the implementation to be changed to start supporting those features without affecting the user interface.
public class ProductsDAO : BaseEntityDAO<Product>
{
[DataObjectMethod(DataObjectMethodType.Select, false)]
[SelectArgs(WhereClause=false, FilterExpressions=false, SortExpression=true, Paging=true)]
public IEnumerable<Product> SelectPriceRange(decimal startPrice, decimal endPrice,
SelectArgs pSelectArgs)
{ … }
}
The Web form
This form includes several parts: DynamicDataManager control, Filtering controls, GridView, and DataSource control. Notice how aside from the DataSource controls, the web form is the same for both cases.
<asp:DynamicDataManager ID="DynamicDataManager1" runat="server" AutoLoadForeignKeys="true" >
<DataControls>
<asp:DataControlReference ControlID="GridView1" />
</DataControls>
</asp:DynamicDataManager>
Price Range:
<asp:TextBox ID="StartPrice" runat="server" Text="0" ></asp:TextBox>
<asp:RequiredFieldValidator ID="ProductNameTextRequired" runat="server"
ControlToValidate="StartPrice" ErrorMessage="Required" Display="Dynamic" />
<asp:CompareValidator ID="StartPriceDTC" runat="server"
ControlToValidate="StartPrice" Operator="DataTypeCheck"
ErrorMessage="Bad format" Display="Dynamic" />
-
<asp:TextBox ID="EndPrice" runat="server" Text="0" ></asp:TextBox>
<asp:RequiredFieldValidator ID="EndPriceTextRequired" runat="server"
ControlToValidate="EndPrice" ErrorMessage="Required" Display="Dynamic" />
<asp:CompareValidator ID="EndPriceDTC" runat="server"
ControlToValidate="EndPrice" Operator="DataTypeCheck"
ErrorMessage="Bad format" Display="Dynamic" />
<asp:Button ID="SearchBtn" runat="server" Text="Go" />
<br />
<asp:GridView ID="GridView1" runat="server"
DataSourceID="GridDataSource" DataKeyNames="ProductID"
AllowPaging="True" AllowSorting="True" >
<PagerTemplate>
<asp:GridViewPager runat="server" />
</PagerTemplate>
<EmptyDataTemplate>
There are currently no items in this table.
</EmptyDataTemplate>
</asp:GridView>
<asp:DomainDataSource ID="GridDataSource" runat="server"
ContextTypeName="Product" SelectMethod="SelectPriceRange"
SortParameterName="sortExpression" StartRowIndexParameterName="startRowIndex"
MaximumRowsParameterName="maxRows" >
<SelectParameters>
<asp:ControlParameter ControlID="StartPrice" />
<asp:ControlParameter ControlID="EndPrice" />
</SelectParameters>
</asp:DomainDataSource>
<poco:EntityDAODataSource ID="GridDataSource" runat="server"
EntityTypeName="Product" SelectMethod="SelectPriceRange">
<SelectParameters>
<asp:ControlParameter ControlID="StartPrice" />
<asp:ControlParameter ControlID="EndPrice" />
</SelectParameters>
</poco:EntityDAODataSource>
DataSource controls for POCO Entity classes
Only the POCODataSource control (found on CodePlex) can handle POCO Entity classes with Dynamic Data using a strong separation of concerns.
Dynamic Data also has the EnableDynamicData() method. The POCODataSource control is more consistent with other uses of Dynamic Data and is less CPU intensive on each page request.
Example
Let’s go use the EmailGenerator class from before, with attributes applied. Your web form will define the value for ToEmailAddress. The rest are entered by the user.
public class EmailGenerator
{
public EmailGenerator() { }
[Required()]
[DataType(DataType.EmailAddress)]
[RegularExpression(@"^([\w\.!#\$%\-+.'_]+@[A-Za-z0-9\-]+(\.[A-Za-z0-9\-]{2,})+)$")]
public string FromEmailAddress { get; set; }
[Required()]
[DataType(DataType.EmailAddress)]
[RegularExpression(@"^([\w\.!#\$%\-+.'_]+@[A-Za-z0-9\-]+(\.[A-Za-z0-9\-]{2,})+)$")]
public string ToEmailAddress { get; set; }
[Required()]
[StringLength(80)]
public string Subject { get; set; }
[Required()]
[DataType(DataType.MultilineText)]
public string Body { get; set; }
public void Send()
{
MailMessage mailMessage = new MailMessage(FromEmailAddress, ToEmailAddress);
mailMessage.Subject = Subject;
mailMessage.Body = Body.ToString();
SmtpClient client = new SmtpClient("mail.mycompany.com");
client.UseDefaultCredentials = true;
client.Send(vMailMessage);
}
}
Web form
<asp:FormView ID="FormView1" runat="server" DataSourceID="POCODataSource1" DefaultMode="Edit"
onitemupdated="FormView1_ItemUpdated" >
<EditItemTemplate>
Your Email Address:
<asp:DynamicControl ID="From" runat="server" DataField="FromEmailAddress" Mode="Edit" />
Subject:
<asp:DynamicControl ID="Subject" runat="server" DataField="Subject" Mode="Edit" />
Message:
<asp:DynamicControl ID="Body" runat="server" DataField="Body" Mode="Edit" />
<asp:Button ID="Submit" runat="server" Text="Send" CommandName="Update" />
</EditItemTemplate>
</asp:FormView>
<poco:POCODataSource ID="POCODataSource1" runat="server"
OnCreatePOCOInstance="POCODataSource1_CreatePOCOInstance" />
Web form code behind
public partial class RunProductReport : System.Web.UI.Page
{
protected void FormView1_ItemUpdated(object sender, FormViewUpdatedEventArgs e)
{
if (Page.IsValid)
{
EmailGenerator emailGenerator = (EmailGenerator) POCODataSource1.POCOInstance;
emailGenerator.Send();
}
}
protected void POCODataSource1_CreatePOCOInstance(
object sender, PeterBlum.DataSources.CreatePOCOInstanceEventArgs e)
{
EmailGenerator emailGenerator = new EmailGenerator();
emailGenerator.ToEmailAddress = "CEO@mycompany.com";
e.POCOInstance = emailGenerator;
}
}