Chapter 12. Building Custom Search Pages

In This Chapter

Displaying Information from Related Entities

Building Custom Search Pages

Filtering Based on Related Entities

Dynamic Data offers powerful and flexible building blocks for creating list pages and search screens—common features in most enterprise applications. As discussed in Chapter 6, “Page Templates,” DynamicField can be used with the standard GridView control to quickly build pages that display data in tabular format. By relying on the same rich set of field templates used by the DynamicControl, the DynamicField allows you to significantly improve code reuse and create field templates for both list and single-item pages. The DynamicFilter along with the QueryExtender controls discussed in Chapter 5, “Filter Templates,” can be used to quickly build powerful search screens that rely on an extensible set of filter templates and generate LINQ queries, translated by the Entity Framework to SQL statements and executed directly by the database servers.

Although the data entry pages typically display information about a single entity, read-only pages in general and list pages in particular often need to combine information from several related entities on a single page. For instance, a product search page might need to display and allow users to search for products by the country of origin. In the Northwind Traders sample database, this means combining information from the Product and Supplier entities on the same screen because in this entity model, the product’s country is determined by the address of the supplier. In this chapter, you look at extending the capabilities of Dynamic Data to handle some of the common scenarios of working with related entities in list pages and search screens.

Displaying Information from Related Entities

The first example is a page that displays a list of items a customer previously ordered from the Northwind Traders website. For each item, the page displays name of the product, quantity, date of the order, and the order status. You can see how this page will look in Figure 12.1. Of course, in a real-world application, this page might also allow users to reorder items by clicking a link or selecting a check box next to each item. However, to keep the example simple, this particular functionality is not implemented.

Image

Figure 12.1. List page with columns from multiple related entities.

This page displays properties of three related entities: Order_Detail, Product, and Order. As you can see by looking at the Northwind entity model, the Order_Detail entity has two foreign key (also known as navigation) properties called Product and Order that point to the respective parent entities. On the sample page in the figure, the first column displays the ProductName property of the parent Product entity, and the last two columns display the OrderDate and OrderStatus properties of the parent Order entity.

Consider how this page could be implemented with the built-in controls and field templates provided by Dynamic Data. For the ProductName, you could have used the built-in ForeignKey field template, which would display product names as hyperlinks. However, there is no way to display both OrderDate and OrderStatus using just the field templates that Dynamic Data offers out of the box. The ForeignKey field template can display value of only one property, the one specified in the DisplayColumnAttribute applied to the parent entity class, so you could display either the OrderDate or the OrderStatus, but not both.

To solve this problem, the sample solution includes a new field template called Parent (for consistency with the built-in Children template). This template can be used with foreign key columns and allows you to set the DisplayField attribute to any column from the parent entity to display. Following is an example of using this template with the Order property of the Order_Detail entity to display the OrderDate property of the parent Order entity. Listing 12.4 later in this section shows a complete markup of the sample page.

<unl:UnleashedField DataField="Order" UIHint="Parent"
  DisplayField="OrderDate"/>


Note

UnleashedField is an extended version of the built-in DynamicField, which was introduced in Chapter 11, “Building Dynamic Forms.” For the purposes of this discussion, they are the same.


Listing 12.1 shows the markup of the Parent field template. Notice that unlike the built-in ForeignKey template, which determines the value of the parent column directly and displays it in a HyperLink control, the Parent field template uses a nested UnleashedControl, which as you recall inherits from the DynamicControl provided by Dynamic Data. In other words, the Parent field template uses another field template to display the parent property. That field template will be located based on the metadata information describing the parent property. This allows you to reuse the logic already implemented in the numerous built-in and custom field templates for different types of properties, including enumeration, foreign key, and Boolean, which you otherwise would have to reimplement.

Listing 12.1. Parent Field Template (Markup)


<%@ Control Language="C#" CodeBehind="Parent.ascx.cs"
  Inherits="WebApplication.DynamicData.FieldTemplates.ParentFieldTemplate" %>
<unl:DataItemContainer runat="server" ID="container">
  <unl:UnleashedControl runat="server" ID="control" OnInit="Control_Init" />
</unl:DataItemContainer>


Field templates rely on ASP.NET data binding to extract values of their columns from the entity object provided by the EntityDataSource control. Although the Parent field template itself is bound to the child entity, Order_Detail in this example, the nested field template created by its UnleashedControl, needs to be bound to the parent entity, Order or Product. To accomplish this, the Parent field template wraps the UnleashedControl inside of the DataItemContainer, a simple control, shown in Listing 12.2, that implements the IDataItemContainer interface defined by ASP.NET Framework in the System.Web.UI namespace and allows switching the data binding context for its child controls.

Listing 12.2. DataItemContainer Control


using System.Web.UI;

namespace Unleashed.DynamicData
{
  public class DataItemContainer: Control, IDataItemContainer
  {
    public object DataItem { get; internal set; }
    public int DataItemIndex { get; internal set; }
    public int DisplayIndex { get; internal set; }
  }
}


Code-behind of the Parent field template is shown in Listing 12.3. Notice the public DisplayField property, which is set as a custom attribute in page markup to specify the name of the property of the parent entity the template needs to display.

Listing 12.3. Parent Field Template (Code-Behind)


using System;
using System.Web.UI;
using System.Web.DynamicData;

namespace WebApplication.DynamicData.FieldTemplates
{
  public partial class ParentFieldTemplate : UnleashedFieldTemplate
  {
    public override Control DataControl
    {
      get { return this.control.FieldTemplate.DataControl; }
    }

    public string DisplayField { get; set; }

    protected void Control_Init(object sender, EventArgs args)
    {
      this.container.SetMetaTable(this.ForeignKeyColumn.ParentTable);
      this.control.DataField = this.DisplayField;
    }

    protected override void OnDataBinding(EventArgs e)
    {
      this.container.DataItem = this.FieldValue;
      base.OnDataBinding(e);
    }
  }
}


The Control_Init method uses the DisplayField property to initialize the DataField property of the nested UnleashedControl; however, that is only half the information it needs to locate the MetaColumn describing the parent entity property. The UnleashedControl also needs the MetaTable object describing the parent entity, which the Control_Init method provides by getting the ParentTable from the ForeignKeyColumn object and associating it with the DataItemContainer.

The overridden OnDataBinding method is responsible for changing the data binding context for the nested field template. During data binding, the FieldValue property of the field template associated with a foreign key property returns the parent entity object, lazy-loading it with the help of Entity Framework if necessary. By storing the parent entity object in the DataItem property of the DataItemContainer, this code ensures that the nested field template is bound to the parent entity object and not to the child entity object as the field template itself.

Putting it all together, Listing 12.4 shows the markup of the sample page implemented using this new field template. This markup should be already familiar to you from the previous chapters. It uses UnleashedField—the class created in Chapter 11 to extend the DynamicField provided by Dynamic Data. However, for the purposes of this discussion, you could have also used DynamicField instances.

Listing 12.4. List Page with Columns from Multiple Related Entities (Markup)


<%@ Page Language="C#"
  MasterPageFile="~/Site.master" CodeBehind="SamplePage.aspx.cs"
  Inherits="WebApplication.Samples.Ch12.ParentFieldTemplate.SamplePage" %>
<asp:Content ContentPlaceHolderID="main" runat="server">
  <asp:GridView runat="server" ID="gridView" DataSourceID="dataSource"
    AutoGenerateColumns="false" AllowSorting="true">
    <Columns>
      <unl:UnleashedField DataField="Product" HeaderText="Product" UIHint="Parent"
        DisplayField="ProductName" SortExpression="Product.ProductName"/>
      <unl:UnleashedField DataField="Quantity" HeaderText="Quantity" />
      <unl:UnleashedField DataField="Order" HeaderText="Ordered" UIHint="Parent"
        DisplayField="OrderDate" SortExpression="Order.OrderDate"/>
      <unl:UnleashedField DataField="Order" HeaderText="Status" UIHint="Parent"
        DisplayField="OrderStatus" SortExpression="Order.OrderStatus"/>
    </Columns>
  </asp:GridView>
  <asp:EntityDataSource runat="server" ID="dataSource"
    ConnectionString="name=NorthwindEntities"
    DefaultContainerName="NorthwindEntities" EntitySetName="Order_Details"/>
</asp:Content>


Notice that although the Quantity column comes directly from the Order_Detail entity, whose data is loaded by the EntityDataSource control, for the Product, Ordered, and Status columns, the UIHint attribute is set to Parent, the name of the new field template. A custom attribute, DisplayField, is used to specify the property of the parent entity whose value the page needs to display.

The code-behind file, shown in Listing 12.5, performs not only the required step of associating a MetaTable object with the GridView control, but also initializes the Include property of the EntityDataSource control. In dynamic page templates, this step is performed by the DynamicDataManager control. In this sample page, it is required to eagerly load the parent entities, Product and Order, referenced by the foreign key properties of the child Order_Detail entity. Implementation of the Parent field template assumes that the parent entity object is available during data binding. Without eager loading, this would either result in an error, if the lazy loading option was turned off, or multiple queries would be sent to the database to retrieve parent entities individually.

Listing 12.5. List Page with Columns from Multiple Related Entities (Code-Behind)


using System;
using System.Web.DynamicData;

namespace WebApplication.Samples.Ch12.ParentFieldTemplate
{
  public partial class SamplePage : System.Web.UI.Page
  {
    protected override void OnInit(EventArgs e)
    {
      base.OnInit(e);
      MetaTable table = Global.DefaultModel.GetTable(this.dataSource.EntitySetName);
      this.gridView.SetMetaTable(table);
      this.dataSource.Include = table.ForeignKeyColumnsNames;
    }
  }
}


As you can see, having the Parent field template at your disposal makes implementing list pages that display information from multiple related entities almost as easy as creating custom pages for a single entity. The Parent field template works equally well in single-item pages built with the FormView control and entity templates or the DetailsView control. However, it can only be used in Read-only mode; trying to edit properties of parent entities at the same time with the child entities would be not only challenging to implement, but also confusing for the application’s users.

Column Sorting Across Related Entities

The GridView control allows users to click the column headers to sort the rows in ascending or descending order of the column values. With LINQ-enabled data source controls, this functionality takes advantage of server-side sorting by generating LINQ queries that have an ORDER BY clause when translated to SQL. It turns out you do not have to implement any additional support for this logic in the Parent field template. In a custom page, all that is required is to specify the appropriate value in the SortExpression property of the UnleashedField (this property is actually inherited directly from the DataControlField base class). The following markup snippet illustrates how this was done in the last sample page:

<unl:UnleashedField DataField="Product" HeaderText="Product" UIHint="Parent"
  DisplayField="ProductName" SortExpression="Product.ProductName"/>

With a LINQ-enabled data source, the SortExpression can traverse related entities through the navigation properties. To allow sorting the list of order items by the product name, you simply specify SortExpression of the first column as Product.ProductName, where Product is the name of the navigation property of the Order_Detail entity. What the SortExpression does when the user clicks the Product column in the GridView control on the sample page is very similar the following LINQ query. Of course, setting the SortExpression does not require any custom code and is much more compact:

using (var context = new NorthwindEntities())
{
  IQueryable<Order_Detail> query =
    from item in context.Order_Details
    orderby item.Product.ProductName
    select item;
}

Building Custom Search Pages

Many similarities exist between the DynamicControl and field templates on one side and the DynamicFilter control and filter templates on the other. As you recall from a discussion in Chapter 10, “Building Custom Forms,” the DynamicControl allows you to assemble custom entity forms from field templates based on the type of entity properties. If you need a specific field template for a given property, such as the MultilineText template, you can specify its name in the UIHint attribute of the DynamicControl:

<asp:DynamicControl runat="server" DataField="Address"
  UIHint="MultilineText"/>

If you want to provide additional information to the field template, you can do so by specifying custom attributes directly in markup. The DynamicControl automatically assigns the custom attribute values, such as the Columns attribute shown next, to the matching public properties of the field template:

<asp:DynamicControl runat="server" DataField="Address" Columns="65"/>

When it comes to dynamic filters, the DynamicFilter control also supports the ability to specify the template name explicitly by setting its FilterUIHint attribute in markup. You can take advantage of the Text filter template created in Chapter 5 to create a filter control for the ProductName property of the Product entity:

<asp:DynamicFilter runat="server" DataField="ProductName"
  FilterUIHint="Text"/>

You also want to be able to specify custom attributes in page markup and access them in the filter template. For instance, when configuring the ProductName filter, you might need to make the TextBox control wider and allow users to see longer values without scrolling. In page markup, you would want to write something like this:

<asp:DynamicFilter runat="server" DataField="ProductName"
  FilterUIHint="Text" Columns="100"/>

In the Text filter template, you would want to define a public property called Columns and use it to initialize the TextBox the template creates, like so:

public partial class TextFilter
{
  public int Columns { get; set; }

  protected override void OnInit(EventArgs e)
  {
    base.OnInit(e);
    if (this.Columns > 0)
      this.textBox.Columns = this.Columns;
  }
}

Unfortunately, the DynamicFilter control does not support use of custom attributes to initialize public properties of the filter templates. So if you wanted to provide the ability to change the size of the TextBox control in the Text filter template from a custom page, you couldn’t simply specify it in the page markup as just shown. Instead, you would have to stick it in the FilterUIHintAttribute applied to the property in the entity model and add code to the filter template to retrieve it dynamically. This is not only more cumbersome, it also forces you to place presentation logic of a specific page in the entity model.

Consider the Parent field template that displays values of properties from a related entity. This template uses a DynamicControl (yes, an UnleashedControl in this example, which is not important for this discussion) to locate and instantiate another field template. A similar filter template could allow you to build search pages based on columns from related entities. Perhaps you need to create a page where users can search for Products by the Country of their Suppliers. Here is how you would want to use this filter template in page markup:

<asp:DynamicFilter runat="server" DataField="Supplier"
  FilterUIHint="Parent" DisplayField="Country"/>

This filter template would also need a nested DynamicFilter control to load another filter template, appropriate for the specified DisplayField. The following snippet shows how markup of this filter template could look:

<%@ Control Language="C#" CodeBehind="Parent.ascx.cs"
  Inherits="WebApplication.DynamicData.Filters.ParentFilter" %>
<asp:DynamicFilter ID="filter" runat="server" OnInit="Filter_Init"/>

Unfortunately, it is not possible to use the DynamicFilter control inside of a filter template like this. Internal implementation of the DynamicFilter control relies on the InitComplete event of the Page class to instantiate the filter template, and by the time the nested DynamicFilter is created, this event has already fired, and the nested filter template is never created.

Overcoming Limitations of Dynamic Filters

To be consistent with the capabilities offered by the DynamicControl, the DynamicFilter control needs to support custom attributes and work inside of other filter templates. These capabilities are important not only for consistency’s sake; without them, the DynamicFilter control encourages embedding presentation information in the business layer and limits your ability to create advanced filter templates necessary for real-world search pages.

Unfortunately, implementation of the DynamicFilter in ASP.NET version 4.0 does not allow you to fix these problems by inheriting from and extending this control. The only way to eliminate them is by completely rewriting this control along with other controls and classes that depend on it. Table 12.1 shows the original controls that come with Dynamic Data and the replacement controls you can find in the source code accompanying this book.

Table 12.1. Replacement Filter Classes and Controls

Image

With the exception of the UnleashedFilterTemplate, which inherits from the QueryableFilterUserControl, the rest of these classes are not related to the corresponding built-in classes that come with Dynamic Data. However, they are designed to be fully-compatible with their built-in equivalents and allow you to simply change the class names in markup and source files of your application. Figure 12.2 shows a class diagram that provides a high-level overview of the new classes.

Image

Figure 12.2. Unleashed filter classes.

UnleashedFilter

The UnleashedFilter control is a replacement for the built-in DynamicFilter control. It provides properties (DataField, FilterUIHint, and FilterTemplate) events (FilterChanged) and methods (OnFilterChanged) that mimic corresponding members of the DynamicFilter control and should be 100% compatible and familiar to you from a discussion in Chapter 5.

The DataSource property is new. It allows you to get or set the IQueryableDataSource instance with which the filter is associated. In the DynamicFilter, the data source object is stored in a private field; however, in the UnleashedFilter, you need to be able to set it not only from a data source expression, but also from a filter template that has a nested filter control.

The Table and Column properties of the UnleashedFilter are provided for consistency and mimic similar properties of the built-in DynamicControl. You can set the Table property explicitly, but the control can also determine it automatically by retrieving the MetaTable object associated with its DataSource control. Likewise, the Column property can be set directly; however, the control automatically finds the MetaColumn object in the associated MetaTable using the name specified in the DataField property.

Like the DynamicControl, the UnleashedFilter supports specifying custom attributes in page markup and automatically sets matching public properties of the filter template it instantiates. And like the UnleashedControl, the UnleashedFilter automatically initializes public properties of the filter template based on the control parameters specified for the entity property with the FilterUIHintAttribute.

UnleashedFilterRepeater

The UnleashedFilterRepeater control is a replacement for the built-in QueryableFilterRepeater. As you recall from Chapter 6, this control is used by the List page template to automatically generate filters for all filterable properties of a given entity class. It allows you to define an ItemTemplate markup, typically containing a Label and a DynamicFilter controls that are dynamically instantiated for every filterable property. QueryableFilterRepeater control is responsible for initializing the DynamicFilter instances it generates. Because the UnleashedFilter is not related to the built-in DynamicFilter, the QueryableFilterRepeater cannot initialize it dynamically, which is why it had to be replaced with the UnleashedFilterRepeater. For compatibility, the replacement control defines both ItemTemplate and DynamicFilterContainerId properties that work exactly like they do in the QueryableFilterRepeater.

Similarly to the UnleashedFilter, the UnleashedFilterRepeater class defines DataSource and Table properties, allowing you to configure it programmatically. It also does not rely on the Page events, which makes it possible to use inside of other filter templates.

UnleashedFilterExpression

The UnleashedFilterExpression is a replacement for the built-in DynamicFilterExpression. As you recall from Chapters 5 and 6, this class is used in page markup to associate DynamicFilter and QueryableFilterRepeater controls with a QueryExtender control. DynamicFilterExpression is responsible for supplying the filter controls with the IQueryableDataSource instance (an EntityDataSource or LinqDataSource) received from the query extender. The DynamicFilterExpression works with the built-in controls and just like the controls themselves, does not allow extension by inheritance, forcing a complete replacement.

UnleashedFilterTemplate

The UnleashedFilterTemplate base class is a replacement for the built-in QueryableFilterUserControl. Unlike the other filter classes, however, the QueryableFilterUserControl provides a fair amount of utility methods used in the enumeration and foreign key filter templates. As a compromise between quality of design and code reuse, the UnleashedFilterTemplate inherits from the QueryableFilterUserControl and exposes the properties and methods required for initialization via .NET Reflection.

Using .NET Reflection in a web application is far from ideal. Aside from being fragile in face of potential future changes in the .NET framework, Reflection requires the web application to run in full-trust mode, which can be a problem in a shared hosting environment. If that is a concern, you might need to reimplement the QueryableFilterUserControl entirely.

Using Replacement Filter Controls

The beginning of this section discussed several limitations in the implementation of the build-in DynamicFilter control, including its reliance on the Page events and lack of support for custom attributes and metadata-driven control parameters. In the next section, you see how overcoming the first limitation enables you to create an advanced filter template that can filter a query of child entities based on properties of their parents. However, for illustration of the immediate benefits, consider the sample page in Listing 12.6, which shows an example of using the UnleashedFilter control to specify custom attributes.

Listing 12.6. Product Search Page with Wide ProductName Filter (Markup)


<%@ Page Title="" Language="C#"
  MasterPageFile="~/Site.master" CodeBehind="SamplePage.aspx.cs"
  Inherits="WebApplication.Samples.Ch12.UnleashedFilter.SamplePage" %>
<asp:Content ContentPlaceHolderID="main" runat="server">
  <unl:UnleashedFilter runat="server" ID="productNameFilter" FilterUIHint="Text"
    DataField="ProductName" Columns="100"/>
  <asp:GridView runat="server" ID="gridView" DataSourceID="dataSource"
    AutoGenerateColumns="false">
    <Columns>
      <unl:UnleashedField DataField="ProductName"/>
      <unl:UnleashedField DataField="Supplier" HeaderText="Origin"
        UIHint="Parent" DisplayField="Country" />
    </Columns>
  </asp:GridView>
  <asp:EntityDataSource runat="server" ID="dataSource"
  ConnectionString="name=NorthwindEntities" DefaultContainerName="NorthwindEntities"
  EntitySetName="Products"/>
  <asp:QueryExtender runat="server" TargetControlID="dataSource">
    <unl:UnleashedFilterExpression ControlID="productNameFilter"/>
  </asp:QueryExtender>
</asp:Content>


This sample page demonstrates how the UnleashedFilter control enables you to set a custom attribute Columns for the UnleashedFilter control in page markup and make the text box generated by the ProductName filter wider. The custom attribute matches the public property called Columns declared by the Text filter template, and the UnleashedFilter takes care of initializing it for you. The Text filter template, located in the DynamicDataFilters folder of the sample web application, has been modified to inherit from the new UnleashedFilterTemplate. Its OnInit method initializes the Columns property of the actual TextBox control where the user enters a product name to search. Figure 12.3 shows how the resulting page looks with the wide ProductName filter text box above the grid.

Image

Figure 12.3. Product search page with wide product name filter.

Remember that to be compatible with the UnleashedFilter control, all existing filter templates must be modified to inherit from the UnleashedFilterTemplate instead of the built-in QueryableFilterUserControl. The filter templates accompanying this book have been updated; their source code is not listed here to save space.

Although this is not strictly required at this time, you might also want to go ahead and change the dynamic page templates, List and ListDetails, to use the UnleashedFilterRepeater and UnleashedFilter controls instead of the built-in QueryableFilterRepeater and DynamicFilter controls, respectively. Support the UnleashedFilter provides control parameters in the FilterUIHintAttribute is essential to building search pages dynamically, which is discussed in the Chapter 13, “Building Dynamic List Pages.”

Filtering Based on Related Entities

Now that the building blocks are in place, let’s get back to the original goal of building a page that displays information from multiple entities. The earlier example you used the Parent field template, which allows displaying values from any property of a parent entity in the same GridView or DetailsView with its children. However, when building search screens, you often need to not only display, but also allow users to filter based on information from related entities. Consider the web page shown in Figure 12.4, which displays a list of products and their countries of origin. Although the product name comes from the ProductName property of the Product entity, the country of origin comes from the Country property of the Supplier entity. The Supplier and Product entities form a parent-child relationship, connected by the Supplier foreign key property of the Product entity.

Image

Figure 12.4. Product search page filtered by supplier country.

This page displays the Origin column using the UnleashedField and Parent field template with markup shown here:

<unl:UnleashedField DataField="Supplier"
  UIHint="Parent" DisplayField="Country"/>

To implement the requirement of searching for products by their countries of origin, you would like to use a similar pattern with markup that looks like this:

<unl:UnleashedFilter runat="server" DataField="Supplier"
  FilterUIHint="Parent" DisplayField="Country"/>

In other words, the Parent filter template is also associated with the Supplier property of the Product entity and displays the filter control for the Country property of the parent Supplier entity.

As you can see in Listing 12.7, the approach used in the Parent filter template to locate and instantiate a filter template appropriate for the parent property is also similar to the one used to implement the Parent field template earlier. Its markup defines a nested instance of the UnleashedFilter control that will be responsible for loading another filter template based on the property of the parent entity specified as the DisplayField.

Listing 12.7. Parent Filter Template (Markup)


<%@ Control Language="C#" CodeBehind="Parent.ascx.cs"
  Inherits="WebApplication.DynamicData.Filters.ParentFilter" %>
<unl:UnleashedFilter ID="filter" runat="server" OnInit="Filter_Init"/>


Just like with the Parent field template, most of the implementation of the Parent filter template is in its code-behind file. At a high-level, it is responsible for two things—initializing the nested UnleashedFilter control with all information required to load the appropriate filter template for the parent entity property and implementing the query logic required to filter child entities based on the parent property value. In the following two sections, Initializing Nested Filter Control and Implementing Query Logic, you get a close look at the internals of this filter control. Based on this example, some of the more challenging aspects of advanced filter template design are discussed. However, you can also skip them and continue reading the section Using the Parent Filter Template later in the chapter.

Initializing Nested Filter Control

The filter control needs an IQueryableDataSource object (an EntityDataSource or a LinqDataSource), a MetaTable describing the target entity, and a name of the target entity property. Listing 12.8 shows the part of ParentFilter implementation responsible for the initialization.

Listing 12.8. Parent Filter Template (Initialization Logic)


using System;
using System.Linq;
using System.Linq.Expressions;
using System.Web.DynamicData;
using System.Web.UI.WebControls;

namespace WebApplication.DynamicData.Filters
{
  public partial class ParentFilter : UnleashedFilterTemplate
  {
    private object dataSourceContext;

    public string DisplayField { get; set; }

    protected void Filter_Init(object sender, EventArgs args)
    {
      this.filter.DataSource = this.DataSource;
      this.filter.Table = this.ForeignKeyColumn.ParentTable;
      this.filter.DataField = this.DisplayField;
      ((EntityDataSource)this.DataSource).ContextCreated +=
        this.DataSourceContextCreated;
      ((EntityDataSource)this.DataSource).Selecting += this.DataSourceSelecting;
    }

    private void DataSourceContextCreated(object sender,
      EntityDataSourceContextCreatedEventArgs e)
    {
      this.dataSourceContext = e.Context;
    }

    private void DataSourceSelecting(object sender,
      EntityDataSourceSelectingEventArgs e)
    {
      if (string.IsNullOrEmpty(e.SelectArguments.SortExpression))
      {
        e.SelectArguments.SortExpression = string.Join(",",
          this.ForeignKeyColumn.Table.PrimaryKeyColumns.Select(c => c.Name));
      }
    }
  }
}


The Filter_Init method handles the Init event of the nested UnleashedFilter control and initializes all required properties. In particular, it sets the DataSource property of the UnleashedFilter control with the DataSource object of the filter template itself. It also sets the Table property of the nested UnleashedFilter control. Normally, this is not necessary because the UnleashedFilter automatically uses the MetaTable object associated with the data source control. However, in this case you want the nested UnleashedFilter control to be associated with the MetaTable object describing the parent entity. That is why it is set explicitly, using the ParentTable property of the ForeignKeyColumn object that represents the foreign key entity property Supplier, with which the filter template is associated.


Note

In the Parent field template, the DataItemContainer provided both the MetaTable and entity object. In filter templates, which do not rely on data binding to display initial values, this works differently. Instead of data binding, filter templates rely on the DefaultValue and DefaultValues properties provided by the QueryableFilterUserControl base class.


Finally, the Filter_Init method sets the DataField property of the nested UnleashedFilter control. It does that based on the value of the DisplayField property of the filter template itself. In this current example, the DisplayField property is specified as a custom attribute of the UnleashedFilter in page markup, and the control takes care of initializing the property automatically.

The last task performed by the Filter_Init method is setting up a couple of event handlers for the data source control. The DataSourceContextCreated method handles the ContextCreated event. This method saves the ObjectContext, a NorthwindEntities object in this example, in the dataSourceContext private field of the template. This is important for the query logic discussed next that relies on the join LINQ operator and requires both queries to come from the same ObjectContext.

The DataSourceSelecting handles the Selecting event of the EntityDataSource control to work around some of its limitations. The built-in paging logic in the EntityDataSource control relies on the built-in LINQ operators Skip and Take. The Entity Framework implementation of LINQ requires these operators to be preceeded by an OrderBy and, normally, the EntityDataSource is responsible for generating it. However, it does not automatically generate the OrderBy operator when the query has been modified by a QueryExtender, which causes a runtime error. The DataSourceSelecting method in the filter template supplies a default sort expression to the EntityDataSource control unless it was already supplied by a GridView control in response to a user clicking one of the column headers.

Implementing Query Logic

As you might recall from a discussion in Chapter 5, query logic is implemented by overriding the GetQueryable method inherited from the QueryableFilterUserControl base class. This method receives an IQueryable object as a parameter, modifies the query based on the control or controls it displays, and returns the modified query. The built-in filter templates that come with Dynamic Data implement query logic by creating lambda expressions dynamically, using LINQ Expression objects. This task can be significantly simplified with Dynamic LINQ, a sample project that comes with Visual Studio and allows manipulating LINQ expressions as simple strings instead of objects. Unfortunately, as you see shortly, Dynamic LINQ cannot help implementing complex logic the Parent filter template requires, and you have to resort to the low-level LINQ Expression manipulation.


Note

This section walks you through implementation of a specific filter template. However, the general approach of starting with the high-level LINQ query and progressively digging down to deeper levels works well for developing any custom filter template in general. Although you can simply get the Parent control from the book’s source code, reading this section will help you learn how to build your own advanced filter templates that require using low-level LINQ expressions.


LINQ Expression Mechanics

Before you jump into the implementation details of query logic for an advanced filter template, like Parent, first figure out how you would implement it without the dynamic filters. Here is a LINQ query you could have written to retrieve all products imported from the UK:

using (var context = new NorthwindEntities())
{
  IQueryable<Product> query =
    from p in context.Products
    where p.Supplier.Country == "UK"
    select p;
  // ...
}

Notice how much easier it is to filter Product entities based on Supplier entity properties with LINQ than it is with plain SQL. LINQ allows you to traverse the Supplier navigation property and specify filter criteria for the parent entity without having to implement an explicit join. Here is an equivalent query that looks closer to the INNER JOIN SQL statement you would have to create without LINQ:

IQueryable<Product> query =
  from p in context.Products
  join s in context.Suppliers.Where(s => s.Country == "UK")
    on p.SupplierID equals s.SupplierID
  select p;

Obviously, this query is more verbose and difficult to understand; it is unlikely you would have chosen this approach when implementing custom filtering logic. However, it does offer an important advantage over its predecessor when it comes to building queries dynamically. By using an explicit join operator, this new query helps you to see that you can combine two completely independent queries, context.Products and context.Suppliers.Where(s => s.Country == "UK"), to return a set of child, Product, and entities filtered by search criteria applied to its parent entity, Supplier.

This is exactly how the Parent filter template works to fit into the constraints of the Dynamic Data filtering architecture. The template receives the child query, context.Products, has the nested filter template generate the parent query, context.Suppliers.Where(s => s.Country == "UK"), combines them with a join, and uses a select to return just the child entities.

Unfortunately, Dynamic LINQ does not support the join operator, so you have to implement this logic from ground up with the LINQ expression API. This is, by far, the most complex, error-prone, and time consuming programming task you will encounter when building dynamic web applications. On the bright side, it helps you to gain a new level appreciation of the heavy lifting that the C# and Visual Basic compilers do for you when converting the magically simple LINQ queries to expressions.

To build a LINQ query dynamically, you have to be familiar with the extension method syntax (as opposed to the language-integrated syntax) of building LINQ queries. LINQ expressions are an object model that represents queries built with LINQ extension methods. Because the language-integrated query syntax is a higher-level abstraction on top of the extension methods, it is easier to make a mental jump from the LINQ extension methods to LINQ expressions. Here is the same query, expressed with the LINQ extension method, Join:

IQueryable<Product> query = Queryable.Join(
  context.Products,
  context.Suppliers.Where(s => s.Country == "UK"),
  p => p.SupplierID,
  s => s.SupplierID,
  (p, s) => p);

To eliminate ambiguity, here the static Join method of the Queryable class is called explicitly, not as an extension method. This is the lowest-level code you can write in C# to create a LINQ query, but it still hides a lot of complexity. As you can see from its declaration shown next, the Join method is generic:

public static IQueryable<TResult> Join<TOuter, TInner, TKey, TResult>(
  this IQueryable<TOuter> outer,
  IEnumerable<TInner> inner,
  Expression<Func<TOuter, TKey>> outerKeySelector,
  Expression<Func<TInner, TKey>> innerKeySelector,
  Expression<Func<TOuter, TInner, TResult>> resultSelector
)

It is a bit overwhelming trying to understand something so abstract. You might find it helpful to look at the Join method with specific parameters, which you can do by writing a sample query in Visual Studio and pointing the mouse cursor at the method name to see the tooltip while stepping over it in debugger. Here is what the debugger displays for this sample query:

IQueryable<Product> Queryable.Join<Product, Supplier, int?, Product> (
  IQueryable<Product> outer,
  IEnumerable<Supplier> inner,
  Expression<Func<Product, int?>> outerKeySelector,
  Expression<Func<Supplier, int?>> innerKeySelector,
  Expression<Func<Product, Supplier, Product>> resultSelector)

To break it down into smaller pieces, the outer parameter is the query that returns child entities, and the inner parameter is the query that returns parent entities—in this example, products and suppliers, respectively. When no additional filter criteria is applied to either query, these parameters could be simply context.Products and context.Suppliers, respectively, resulting in a join performed between the two entire tables. From the standpoint of the Parent filter template, you do not need to worry about constructing these parameters with LINQ expression objects. The GetQueryable method receives the first query as its argument, and the nested filter template builds the second query.

The outerKeySelector parameter is a lambda expression that takes a Product entity as a parameter and returns value of its SupplierID property. In the original query, this looks simply like p => p.SupplierID; however, with LINQ expression objects, building the key selector is a little more involved. Here is how you could do it:

ParameterExpression parameter = Expression.Parameter(typeof(Product));
MemberExpression property = Expression.Property(parameter, "SupplierID");
LambdaExpression productKeySelector = Expression.Lambda(
  property, parameter);

The innerKeySelector parameter is very similar, only it takes a Supplier entity as a parameter and returns value of its SupplierID property. In the original query, it looked like this: s => s.SupplierID. What you don’t see, however, is that the compiler automatically converted the int value of the SupplierID property of the Supplier entity to the int? type expected by the Join method for compatibility with the SupplierID property of the Product entity, which is nullable. With LINQ expression objects, this key selector could be built like this:

ParameterExpression parameter = Expression.Parameter(typeof(Supplier));
MemberExpression property = Expression.Property(parameter, "SupplierID");
UnaryExpression converted = Expression.Convert(property, typeof(int?));
LambdaExpression supplierKeySelector = Expression.Lambda(
  converted, parameter);

Finally, the resultSelector parameter is a lambda expression that takes a Product and a Supplier objects as parameters and returns the Product object. In the original query, the lambda expression looked like this: (p, s) => p. With LINQ expression objects, this lambda can be constructed as such:

ParameterExpression product = Expression.Parameter(typeof(Product));
ParameterExpression supplier = Expression.Parameter(typeof(Supplier));
LambdaExpression resultSelector = Expression.Lambda(
  product, product, supplier);


Note

The order of arguments in the Expression.Lambda method is a little confusing. The body of the lambda expression is specified as the first argument, followed by a parameter array (C# keyword params), that specifies parameters of the lambda expression. In other words, the order of arguments is a reverse of what you might intuitively expect. Remembering this and carefully inspecting each expression object in Visual Studio debugger can help you to avoid this gotcha when programming LINQ expressions.


At this point, you know how to provide all arguments to the Queryable.Join method; what is left to do is construct the method call itself. Keep in mind that the Join operator will be translated to SQL by the Entity Framework and executed by the database server. In other words, the Join extension method will not be called by the application itself, and you need to construct it as a MethodCallExpression object. You can accomplish this task with the help of the the Expression.Call factory method, or more specifically, with this particular overload:

public static MethodCallExpression Call(
  Type type,
  string methodName,
  Type[] typeArguments,
  params Expression[] arguments
)

The type parameter represents type, whose method you need to call, Queryable in the example. The methodName parameter is a string containing the name of the method to be called, "Join" in this example. The typeArguments is an array of the type arguments that will be used by the .NET runtime to create a specific implementation of the generic Join method. The arguments parameter is an array of Expression objects that represent the arguments that will be passed to the method. With the selectors built earlier, here is how the Join LINQ expression can be created in code:

MethodCallExpression joinCall = Expression.Call(
  typeof(Queryable),
  "Join",
  new Type[] {
    typeof(Product),
    typeof(Supplier),
    typeof(int?),
    typeof(Product)
  },
  new Expression[] {
    productQuery.Expression,
    supplierQuery.Expression,
    productKeySelector,
    supplierKeySelector,
    resultSelector
  }
)


Note

It is easy to confuse typeArguments with arguments. With the dozen or so different overloads that the Expression.Call method defines, this results in compiler errors that make very little sense and do not help to understand the problem. When working with MethodCallExpression objects, remember that in a generic method call, the typeArguments are the things that go between < >, and arguments are the things that go between ( ).


The final step in constructing a LINQ query with expression objects is to convert the LINQ expression to an IQueryable object and replace the original query. Here is how this can be done in the example with products and suppliers:

IQueryProvider queryProvider = productQuery.Provider;
productQuery = queryProvider.CreateQuery(joinCall);

Here you use the IQueryProvider object returned by the Provider property of the original product query. The IQueryProvider interface defines a CreateQuery method that takes a lambda expression object as a parameter and returns an IQueryable object that represents the new query. By passing a MethodCallExpression to the CreateQuery method of the query provider, you create a new IQueryable (that is, a new LINQ query) that returns results of a join between Product and Supplier queries.


Note

Each LINQ-enabled framework, such as Entity Framework or LINQ to SQL, provides its own implementation of the IQueryProvider, which allows the same code to construct LINQ queries independently of the underlying data access technology.


Query Logic in the Parent Filter Template

Instead of hard-coding type and property names as shown in the previous section, the parent filter template builds the Join query dynamically, relying on the metadata information describing the foreign key column and the child and parent tables it links together. Still, the structure of the code is similar to the specific example just discussed and should be easy for you to follow. Listing 12.9 shows the remaining implementation of the Parent filter template. It includes the overridden GetQueryable method and the helper methods it uses to build the required lambda expressions: BuildChildKeySelector, BuildParentKeySelector, and BuildResultSelector.

For a filter template, it is important to modify the query only if the user actually entered search criteria in its control. As you are reading this code, remember that childQuery is an IQueryable object created by the EntityDataSource control on the page (Product query in the example). Notice how the code creates an empty query for the parent entity (a query that returns all Suppliers) by calling the GetQuery method of the MetaTable object describing the parent entity. This empty parent query is passed to the GetQueryable method of the nested filter template. Only if the query it returns is not the same as the original empty query will the Parent filter template generate a Join query. If the nested filter template returned the empty query without any filter criteria for the parent table, or in other words, if the user did not enter any filter criteria for the parent table, adding a Join to the child query is not necessary and would only slow it down.


Note

Calling the GetQuery method of the MetaTable object as opposed to using the corresponding entity set property (such as Suppliers) of the ObjectContext directly is important for consistency with the Dynamic Data architecture. The GetQuery method of the MetaTable class is virtual and can be overridden to implement row-level security as discussed in Chapter 14. It is also important to pass it the same ObjectContext that was collected by the ContextCreated event handler discussed earlier, to ensure that both queries in the join come from the same ObjectContext.


Listing 12.9. Parent Filter Template (Query Logic)


using System;
using System.Linq;
using System.Linq.Expressions;
using System.Web.DynamicData;
using System.Web.UI.WebControls;
namespace WebApplication.DynamicData.Filters
{
  public partial class ParentFilter : UnleashedFilterTemplate
  {
    public override IQueryable GetQueryable(IQueryable childQuery)
    {
      MetaTable parentTable = this.ForeignKeyColumn.ParentTable;
      IQueryable emptyQuery = parentTable.GetQuery(this.dataSourceContext);
      IQueryable parentQuery = this.filter.FilterTemplate.GetQueryable(emptyQuery);
      if (parentQuery != emptyQuery)
      {
        MetaTable childTable = this.ForeignKeyColumn.Table;
        MetaColumn childColumn = childTable.GetColumn(
          this.ForeignKeyColumn.ForeignKeyNames[0]);
        MetaColumn parentColumn = parentTable.PrimaryKeyColumns[0];

        MethodCallExpression joinCall = Expression.Call(
          typeof(Queryable),
          "Join",
          new Type[] {
            childQuery.ElementType,
            parentQuery.ElementType,
            childColumn.EntityTypeProperty.PropertyType,
            childQuery.ElementType
          },
          new Expression[] {
            childQuery.Expression,
            parentQuery.Expression,
            BuildChildKeySelector(childQuery.ElementType, childColumn.Name),
            BuildParentKeySelector(parentQuery.ElementType, parentColumn.Name,
              childColumn.EntityTypeProperty.PropertyType),
            BuildResultSelector(childQuery.ElementType, parentQuery.ElementType)
          });

        childQuery = childQuery.Provider.CreateQuery(joinCall);
      }

      return childQuery;
    }

    private static LambdaExpression BuildChildKeySelector(
      Type entityType, string propertyName)
    {
      ParameterExpression parameter = Expression.Parameter(entityType, "child");
      MemberExpression property = Expression.Property(parameter, propertyName);
      return Expression.Lambda(property, parameter);
    }

    private static LambdaExpression BuildParentKeySelector(
      Type entityType, string propertyName, Type expectedPropertyType)
    {
      ParameterExpression parameter = Expression.Parameter(entityType, "parent");
      MemberExpression property = Expression.Property(parameter, propertyName);
      if (property.Type == expectedPropertyType)
        return Expression.Lambda(property, parameter);

      UnaryExpression convertedProperty = Expression.Convert(property,
        expectedPropertyType);
      return Expression.Lambda(convertedProperty, parameter);
    }

    private static LambdaExpression BuildResultSelector(
      Type childEntityType, Type parentEntityType)
    {
      ParameterExpression child = Expression.Parameter(childEntityType, "child");
      ParameterExpression parent = Expression.Parameter(parentEntityType, "parent");
      return Expression.Lambda(child, child, parent);
    }
  }
}


While reading this code, you might have noticed a hard-coded assumption it makes about the number of primitive columns in the foreign key. This implementation of the Parent filter template assumes that the primary key of the parent entity consists of a single column. If your entity model uses compound keys (keys consisting of more than one column), this filtering logic would produce incorrect results. With static LINQ queries, compound foreign key values are usually represented with anonymous types, automatically generated by the compiler. With dynamic LINQ queries, this requires emitting anonymous types at run time, a large and complex topic that would be difficult to cover in this book.

Compound keys largely have fallen out of favor with database architects over the past decade. Even the Northwind sample database, which is more than ten years old, does not have compound foreign keys. This assumption helps to significantly simplify implementation of the Parent filter template. However, compound keys have some advantages that might be important in your application. If that is your situation, take a closer look at the Dynamic LINQ sample (it is included with this book’s source code). The Dynamic LINQ includes APIs for emitting anonymous types, which should help to implement a more sophisticated version of dynamic queries that support compound keys.

Using the Parent Filter Template

With the Parent filter template implementation completed, you can finally create the sample page shown back in Figure 12.4. As you recall, it includes a GridView control with one column showing product names and the other column showing their countries of origin. At the top of the page, you have two dynamic filters, one based on the ProductName property of the Product entity and the other based on the Country property of the parent Supplier entity. Listing 12.10 shows a complete markup of the sample page.

Listing 12.10. Product Search Page Filtered by Supplier Country (Markup)


<%@ Page Title="" Language="C#"
  MasterPageFile="~/Site.master" CodeBehind="SamplePage.aspx.cs"
  Inherits="WebApplication.Samples.Ch12.ParentFilterTemplate.SamplePage" %>
<asp:Content ContentPlaceHolderID="main" runat="server">
  <unl:UnleashedFilter runat="server" ID="productNameFilter" FilterUIHint="Text"
    DataField="ProductName" Columns="100"/>
  <unl:UnleashedFilter runat="server" ID="originFilter"
    DataField="Supplier" FilterUIHint="Parent" DisplayField="Country"/>
  <asp:GridView runat="server" ID="gridView" DataSourceID="dataSource"
    AutoGenerateColumns="false">
    <Columns>
      <unl:UnleashedField DataField="ProductName"/>
      <unl:UnleashedField DataField="Supplier" HeaderText="Origin"
        UIHint="Parent" DisplayField="Country" />
    </Columns>
  </asp:GridView>
  <asp:EntityDataSource runat="server" ID="dataSource"
    ConnectionString="name=NorthwindEntities"
    DefaultContainerName="NorthwindEntities" EntitySetName="Products"/>
  <asp:QueryExtender runat="server" TargetControlID="dataSource">
    <unl:UnleashedFilterExpression ControlID="productNameFilter"/>
    <unl:UnleashedFilterExpression ControlID="originFilter"/>
  </asp:QueryExtender>
</asp:Content>


Notice the UnleashedFilter control associated with the Supplier column of the Product entity. Its FilterUIHint is set to "Parent", the name of the new filter template, and the DisplayField custom attribute is set to "Country", the name of the property from the parent Supplier entity for users to filter the list of products by.

Summary

In this chapter, you saw some of the techniques for building custom pages that display information in tabular format and allow users to search for information by filtering and sorting the data. You reviewed some of the limitations of the built-in templates and controls that Dynamic Data provides out of the box and focused on the commonly requested need to display read-only information from several related tables on a single page. Unlike displaying data from a single entity, which Dynamic Data does very well, this type of functionality is not provided out of the box but not difficult to implement.

The UnleashedFilter control created in this chapter along with the UnleashedFilterTemplate base class for filter templates and the supporting UnleashedFilterExpression eliminates the limitations unnecessarily imposed by the built-in DynamicFilter control in version 4.0 of ASP.NET. In particular, by avoiding the Page events, the UnleashedFilter allows you to create advanced filter templates that use nested filter controls to generate powerful queries that span multiple related entities. Similar to the DynamicControl, the UnleashedFilter also accepts custom attributes and automatically initializes the matching public properties of the filter template it creates. Consistently with the extended functionality of the UnleashedControl created earlier in this book, the UnleashedFilter also recognizes control parameters specified in the FilterUIHintAttribute that might be applied to the property in the entity model and uses them to initialize the matching public properties of the filter template as well.

To enable building pages that display and allow filtering by properties from multiple related entities, a field template and a filter template (both called Parent) were created. Both templates are designed for use with foreign key navigation properties, like the Supplier property of the Product entity. In custom pages, you can use these templates explicitly by setting the UIHint or FilterUIHint properties of the UnleashedControl or the UnleashedFilter controls, respectively. Both templates define a property called DisplayField, which specifies the name of the property in the parent entity that you want to display on the page and, in the case of the Parent filter template, allow users to filter by.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.144.123.155