Saturday 2 August 2008

Dynamic Data and Field Templates - A Second Advanced FieldTemplate ***UPDATED***

  1. The Anatomy of a FieldTemplate.
  2. Your First FieldTemplate.
  3. An Advanced FieldTemplate.
  4. A Second Advanced FieldTemplate.
  5. An Advanced FieldTemplate with a GridView.
  6. An Advanced FieldTemplate with a DetailsView.
  7. An Advanced FieldTemplate with a GridView/DetailsView Project.

For this article we are going to convert the CascadingFilter from Dynamic Data Futures project this was thought of by Noimed in this thread.

Files Required for this Project

Here are all of the files we will need to copy to our project from the Dynamic Data Futures project:

From the sample website DynamicDataFuturesSample\DynamicData\Filters folder to the our projects DynamicData\FieldTemplates folder

  • Cascade.ascx
  • Cascade.ascx.cs

From the sample website DynamicDataFuturesSample root to our projects App_Code folder

  • CascadeAttribultes.cs

Plus we will need to add a reference to the DynamicDataFutures project or just copy the Microsoft.Web.DynamicData.dll to the bin folder of our project (you will need to create a bin folder manually if you just copy the dll).

What Cascading Filter does

Cascading filter in Edit/Insert modes, I pointed him to the previous post in this series and we eventually sorted it so it worked as a FieldTemplate. This returns the Primary Key of the parent table see Figure 1.

Order_Details relationships

Figure 1 - Order_Details relationships

In this diagram you can see that Product is grouped Category so the CascadingFilter user control would be ideal for picking the product on the Order_Detail Insert page.

Note: You can’t edit the Product on the Order_Detail form because the Primary Key of Order_Detail is OrderID combined with ProductID :D

So in our sample we will be filtering the Product by the Category.

Creating the Website Project and Adding the Files

The first thing to do will be to create a file based website and add the Northwind database to it. This can be done simply (if you have SQL Server Express 2005/2008 installed) by creating a App_Data folder and copying the Northwind.mdb file to it (the Northwind database can be downloaded from here).

Then add an App_Code folder to the website and add a new Linq to SQL classes item it call it NW.dbml and add at lease the above table to it.

Now copy the files listed in the “Files Required for this Project” and add the reference to Dynamic Data Futures project.

Lets add a reference to the Dynamic Data Futures project; I do this by first adding an existing project, you do this by clicking File->Add->Existing Project...

Adding an Existing project

Figure 2 - Adding an Existing project

Browse to the location you have you Dynamic Data Futures project and select the project file.

Now right click the website and choose Add Reference when the dialogue box pops up select the Projects tab and choose the Dynamic Data Futures project and click the OK button.

Your project should now look like Figure 3.

How the project should look after adding the files and references

Figure 3 – How the project should look after adding the files and references

Note: Don’t forget the add your data context to the Global.asax file and set ScaffoldAllTables to true

Modifying the added files

Remove the namespace for the CascadeAttribute.cs file and save that’s done.

Note: Removing the namespace is for file based website only in a Web Application Project you would need to change the namespace to match your applications.

And now lets sort out the Cascade filter. We start by renaming the Cascade.ascx to Cascade_Edit.ascx and then edit both files:

<%@ Control 
    Language="C#" 
    AutoEventWireup="true" 
    CodeFile="Cascade_Edit.ascx.cs" 
    Inherits="Cascade_EditField" %>
<%-- Controls will be added dynamically. See code file. --%>

Listing 1 – Cascade_Edit.ascx

Remove the DynamicDataFuturesSample. from the beginning of the Inherits Control property.

Then edit the Cascade_Edit.ascx.cs file:

namespace DynamicDataFuturesSample
{
    public partial class Cascade_Filter : FilterUserControlBase, ISelectionChangedAware
    {

Listing 2 – Cascade_Edit.ascx.cs

Remove the namespace from around the control class and change the inheritance from FilterUserControlBase, ISelectionChangedAware to FieldTemplateUserControl as in Listing 3.

public partial class Cascade_Filter : FieldTemplateUserControl
{

Listing 3 – Altered Cascade_Edit.ascx.cs

Remove the following section as this is for Filters

public override string SelectedValue
{
    get
    {
        return filterDropDown.SelectedValue;
    }
}

Listing 4 – Remove SelectedValue method

Open ForeignKey_Edit.ascx.cs and copy the following sections to the Cascading_Edit.ascx.cs

protected override void ExtractValues(IOrderedDictionary dictionary)
{
    //...
} public override Control DataControl { //...
}

Listing 5 – Methods to copy from ForeignKey_Edit.ascx.cs

Now Edit the ExtractValues and DataControl methods so they look like Listing 6.

protected override void ExtractValues(IOrderedDictionary dictionary)
{
    // If it's an empty string, change it to null
    string val = filterDropDown.SelectedValue;
    if (val == String.Empty)
        val = null;

    ExtractForeignKey(dictionary, val);
}

public override Control DataControl
{
    get
    {
        return filterDropDown;
    }
}

Listing 6 – Finished ExtractValues and DataControl methods

Note: You will also probably need to add the following using: using System.Collections.Specialized; for the IOrderedDictionary and using System.Web.UI; for the Control.

Adding the Metadata

We have to add the following telling the Cascade FieldTemplate what it needs, it need to know what table to use to filter the main parent table by, in this case the Category table. And we need the UIHint to tell Dynamic Data to use the Cascade FieldTemplate.

using System;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using Microsoft.Web.DynamicData;

[MetadataType(typeof(Order_DetailMD))]
public partial class Order_Detail
{
    public class Order_DetailMD
    {
        [Cascade("Category")]
        [UIHint("Cascade")]
        public object Product { get; set; }
    }
}

Listing 7 – the metadata

One last thing we need to add a Cascade.ascx FieldTemplate to the FieldTemplates folder as there is no way of Dynamic Data knowing what FieldTemplate to use in Read-Only mode. For this we will just copy ForeignKey.ascx as Cascade.ascx and change the class name from ForeignKeyField to CascadeField.

Add some Business Logic/Validation

Because we are using Order_Details table which has a composite primary key see below:

Order Details table

Figure 4 - Order Details table

So we need to add some business logic to validate this before insert.

public partial class NWDataContext
{
    partial void InsertOrder_Detail(Order_Detail instance)
    {
        var DC = new NWDataContext();
        var dk = DC.Order_Details.SingleOrDefault(
            od => od.OrderID == instance.OrderID && od.ProductID == instance.ProductID
            );

        if (dk != null)
        {
            // if a record is found throw an exception
            String error = "Duplicate Primary keys not allowed (OrderID={0} ProductID={1})";
            throw new ValidationException(String.Format(error, instance.OrderID, instance.ProductID));
        }
        else
        {
            // finnaly send to the database
            this.ExecuteDynamicInsert(instance);
        }
    }
}

Listing 8 – InsertOrder_Details partial method

This just checks the database to see if this is a duplicate primary key and if so generates a validation error.

Cascade FieldTemplate in Action 

Figure 5 – Cascade FieldTemplate in Action

Business Logic in action

Figure 6 – Business Logic in action

Adding Sorting to the Filters DropDownList ***UPDATED***

In the Cascase.ascx.cd FilterControl and Cascade_Edit.ascx.cs FieldTemplate you will find a method GetChildListFilteredByParent this returns the values for the filtered DropDownList, but as you will see this list is an unordered list. To add sorting to this list we need to add a Linq OrderBy clause. As you will see the code in Listing 9 is making use of the Expression class to create an expression tree smile_confused these are not really hard to understand, it’s just that there are so few examples and tutorials for us to get our teeth into.

So what I’ve done here is add a OrderBy clause which does the trick :D

private IQueryable GetChildListFilteredByParent(object selectedParent)
{
    var query = filterTable.GetQuery(context);
    // this make more sense as the parameter now has the table name (filteredTable.Name)
    // note the change from "product" to filterTable.Name
    var parameter = Expression.Parameter(filterTable.EntityType, filterTable.Name);
    // product.Category
    var property = Expression.Property(parameter, filterTableColumnName);
    // selectedCategory
    var constant = Expression.Constant(selectedParent);
    // product.Category == selectedCategory
    var predicate = Expression.Equal(property, constant);
    // product => product.Category == selectedCategory
    var lambda = Expression.Lambda(predicate, parameter);
    // Products.Where(product => product.Category == selectedCategory)
    var whereCall = Expression.Call(typeof(Queryable), "Where", new Type[] { filterTable.EntityType }, query.Expression, lambda);


    //================================== Order by ================================
    if (filterTable.SortColumn != null)
    {
        // this make more sense as the parameter now has the table name (filteredTable.Name)
        // table.sortColumn
        var sortProperty = Expression.Property(parameter, filterTable.SortColumn.Name);

        // Column => Column.SortColumn
        var orderByLambda = Expression.Lambda(sortProperty, parameter);

        //.OrderBy(Column => Column.SortColumn)
        MethodCallExpression orderByCall = Expression.Call(
            typeof(Queryable),
            "OrderBy",
            new Type[] { filterTable.EntityType, filterTable.SortColumn.ColumnType },
            whereCall,
            orderByLambda);


        //{
        //Table(Product).
        //Where(Products => (Products.Category = value(Category))).
        //OrderBy(Products => Products.ProductName)
        //}
        return query.Provider.CreateQuery(orderByCall);
    }//================================== Order by ================================
    else
    {
        return query.Provider.CreateQuery(whereCall);
    }
}

Listing 9 - GetChildListFilteredByParent

The section between the OrderBy comments is mine gleaned from various bits on the Internet, and also I’ve change the return line of the method to return the orderByCall which was whereCall previously.

To make this work you will need to add a DisplayColumn attribute to the metadata with the sort column added see Listing 10.

[MetadataType(typeof(ProductMD))]
[DisplayColumn("ProductName","ProductName")]
public partial class Product{}

Figure 10 – SortColumn added to DisplayColumn

The second parameter of DisplayColumn is the SortColumn when this is added then the GroupBy will be added to the where clause.

Note: You can transplant this code strait into the Cascade Filter as well smile_teeth.
Note: It should be possible to sort the parent DropDownList using a similar method.

And that about wraps it up.

Until next time.smile_teeth

11 comments:

Anonymous said...

"there is no way of Dynamic Data knowing what FieldTemplate to use in Read-Only mode."

Actually, there is a way to tell.
FieldTemplayUserControl has a property "Mode".

namespace System.Web.UI.WebControls
{
// Summary:
// Represents the different data-entry modes for a data-bound control or a particular
// field in ASP.NET Dynamic Data.
public enum DataBoundControlMode
{
// Summary:
// Represents the display mode, which prevents the user from modifying the values
// of a record or a data field.
ReadOnly = 0,
//
// Summary:
// Represents the edit mode, which enables users to update the values of an
// existing record or data field.
Edit = 1,
//
// Summary:
// Represents the insert mode, which enables users to enter values for a new
// record or data field.
Insert = 2,
}
}


Here is a snippet of what i'm using in my project:
Visible = (Mode != DataBoundControlMode.Insert);

Stephen J. Naughton said...

yes I know all about the Mode property in DD this issue I talking about is that DD doe'nt know which to use as you've added a UIHint [UIHint("Cascade")] to the field/column so DD will add _Edit for the edit and look for the "Cascade" FieldTemplate (Read only mode) and will not know to use the ForeignKey.ascx FT in its place so you copy it. In future version of DD you will be able to specify just the FT you want to overide i.e. [UIHint("Cascade_Edit")] or [UIHint("Cascade_Insert")] but ath the moment if you try this it will asume that the readonly FT is called say Cascade_Edit and the edit is called Cascade_Edit_Edit. see what I mean.
Steve :D

Cristobal Galleguillos Katz said...

Excellent article!
One question: when I switch to "Edit" mode, the cascade controls do not reflect the field values (e.g. "reset" themselves).
Is there a way to make them behave in a more natural manner?
Thanks in advance!

Stephen J. Naughton said...

Yep that's a TODO: ;-)

Hopefully I'll get around to it soon.

Steve :D

Anonymous said...

Please, can you help how we can fix issue with Edit mode.
The cascade controls do not reflect the field values (e.g. "reset" themselves).

Thanks

Stephen J. Naughton said...

Sorry Azamat, Ive had no luck with that as yet, what Ive done is create custom cascading field templates. also have a look at my article "Dynamic Data – Cascading FieldTemplates"(http://csharpbits.notaclue.net/2009/01/dynamic-data-cascading-fieldtemplates.html)

Steve :D

Anonymous said...

Download Code?????

Stephen J. Naughton said...

Hi there, if you go to the last article in this series the download is there.

Steve :D

Unknown said...

Hi Steve, i'm trying to use your Cascade FieldTemplate with EF but...

"Unable to create a constant value of type '%TableName%'. Only primitive types ('such as Int32, String, and Guid') are supported in this context."

OK, in the model.edmx you have foreignKey not as primitive types... So when try to iterate the rows: "foreach (var row in filterItems)" crashes... I'll noted that in GetChildListFilteredByParent() it gets the query to send to the EF, perhaps i can modify something there...

Can you help me?

Anonymous said...

Hi,
Question : when i switch to List Mode from a Edit mode, the Cascade filter return to standard Value "All".
Any help ?

Stephen J. Naughton said...

I think you need to look at the old Ajax History or Filter History for .Net 4
Steve