Wednesday, 13 August 2008

A Short Interlude

Hi I’m going away for a little while.

Normal service will be resumed shortly.

smile_teeth

Monday, 11 August 2008

Dynamic Data Released Visual Studio 2008 SP1 is out!

Well what you need is this:

To do a complete job of moving to the Release of Visual Studio 2008 SP1 you will need to remove the runtime by:

Opening a Visual Studio 2008 Command Prompt (with Run as Administrator if on Vista) change to the folder where you unzipped the last Dynamic Data Runtime release you installed and run the Uninstall.cmd batch file.

And then:

Visual Studio 2008 Service Pack Preparation Tool This removes the Bata 1 of SP1 and cleans up a bit.

Visual Studio 2008 SP1 (also includes .NET 3.5 SP1) This installs the release version

Saturday, 9 August 2008

Customising GridView for Row Rollover and Click in Dynamic Data ***UPDATED***

In this post I’m going to add row rollover and two types of row click.

  1. Background colour mouse rollover.
  2. Row click.
  3. Row double click.
  4. Add the same functionality to ListDetails.aspx

1. Background Colour Mouse Rollover

To add this feature to the List.aspx PageTemplate we need to add an event handler for the OnDataBound event.

To add the event handler go to Design view of the List.aspx page click the GridView click the lightening bolt at the top the properties, this will switch to the event view of the properties.

Selecting Event Properties

Figure 1 - Selecting Event Properties

 Double click the blank next to the Event name

Figure 2 - Double click the blank next to the Event name

Next double click the blank space to the right of the DataBound entry and this will take you to the code behind with a new event handler method called by default GridView1_DataBound.

protected void GridView1_DataBound(object sender, EventArgs e)
{



}

Listing 1 – Empty OnDataBound event handler

Now we need to add the code that adds the rollover event to each row.

foreach (GridViewRow row in GridView1.Rows)
{
    if (row.RowType == DataControlRowType.DataRow)
    {
        // Add javascript to highlight row
        row.Attributes["onmouseover"] = 
"javascript:NAC_ChangeBackColor(this, true, '#BAD5E8'); this.style.cursor = 'hand';"; row.Attributes["onmouseout"] = "javascript:NAC_ChangeBackColor(this, false, '');";
}
}

Listing 2 – code to add mouse rollover

Note: The this.style.cursor = 'hand'; code  in the above OnMouseOver JavaScript event handler is there simply to change the cursor to the hand :D

Also we need some JavaScript Listing 3 you can add this to head of the page or Master page inside some script tags or add it to an external JavaScript file and link it in the header of the page or Master page.

var lastColorUsed;
function NAC_ChangeBackColor(row, highlight, RowHighlightColor)
{
    if (highlight)
    {
        // set the background colour
        lastColorUsed = row.style.backgroundColor;
        row.style.backgroundColor = RowHighlightColor;
    }
    else
    {
        // restore the colour
        row.style.backgroundColor = lastColorUsed;
    }
}

Listing 3 – Rollover helper JavaScript

Note: If you add the code to an external JavaScript file and link to it in the Master page then you will need to alter the path as the Page templates are off the root in DynamicData\PageTemplates\ I found the adding “../” to the beginning of the path worked fine in the standard Dynamic Data file based website.

2. Row click

If we want to add clicking the row to our project we will need to change the buttons in the GridView’s Columns –> TemplateField collection. Listing 4 shows how the buttons need to be changed:

<Columns>
    <asp:TemplateField>
        <ItemTemplate>
            <asp:HyperLink 
                ID="EditHyperLink" 
                runat="server"
                NavigateUrl='<%# table.GetActionPath(PageAction.Edit, GetDataItem()) %>'
                Text="Edit" />
                &nbsp;
            <asp:LinkButton 
                ID="DeleteLinkButton" 
                runat="server" 
                CommandName="Delete"
                CausesValidation="false" 
                Text="Delete"
                OnClientClick='return confirm("Are you sure you want to delete this item?");'/>
                &nbsp;
            <asp:HyperLink 
                ID="DetailsHyperLink" 
                runat="server"
                NavigateUrl='<%# table.GetActionPath(PageAction.Details, GetDataItem()) %>'
                Text="Details" />
        </ItemTemplate>
    </asp:TemplateField>
</Columns>

Change to:

<Columns>
    <asp:TemplateField>
        <ItemTemplate>
            <asp:LinkButton 
                ID="LinkButton1" 
                runat="server" 
                CommandName="Edit"
                CommandArgument='<%# table.GetActionPath(PageAction.Edit, GetDataItem()) %>'
                CausesValidation="true" 
                Text="Edit"/>
            <asp:LinkButton 
                ID="DeleteLinkButton" 
                runat="server" 
                CommandName="Delete"
                CausesValidation="false" 
                Text="Delete"
                OnClientClick='return confirm("Are you sure you want to delete this item?");'/>
            <asp:LinkButton 
                ID="LinkButton2" 
                runat="server" 
                CommandName="Details"
                CommandArgument='<%# table.GetActionPath(PageAction.Details, GetDataItem()) %>'
                Text="Details"/>
        </ItemTemplate>
    </asp:TemplateField>
</Columns>

Listing 4 – changing the two HyperLink’s to LinkButtons

As we did above add an event handler for RowCommand OnRowCommand event see Listing 5 for the code to add to the interior.

protected void GridView1_RowCommand(object sender, GridViewCommandEventArgs e)
{
    String commandName = e.CommandName.ToLower();
    if (commandName == "edit" || commandName == "details")
        Response.Redirect(e.CommandArgument.ToString());
}

Listing 5 – OnRowCommand event handler

What we have done in Listings 4 & 5 is move the functionality of the HyperLink to the code behind. Now Listing 5 just checks to make sure it’s a command we want to handle and then call the URL passed in the CommandArgument. Now we have restored the Hyperlink’s original functionality.

Now that we have changed the HyperLink control to LinkButton we can add the code that implements row click functionality. For this we will need to edit the GridView1_DataBound event handler and add the code in Listing 6 just after the last row.Attributes.

foreach (Control c in row.Cells[0].Controls)
{
    if (c is LinkButton)
    {
        LinkButton selectButton = (LinkButton)c;

        // Get the javascript which is assigned to this LinkButton
        String jsClick = ClientScript.GetPostBackClientHyperlink(selectButton, "");

        // Get command name in lowercase
        String commandName = selectButton.CommandName.ToLower();

        // Add this javascript to the onclick Attribute of the row
        if (commandName == "details")
{ row.Attributes["onclick"] = jsClick; selectButton.Visible = false;
}


} }

Listing 6 – code to cycle through the rows and add the on click event code

If you run this as it is you will get an “Invalid postback or callback argument” error (but that will be hidden because of EnablePartialRendering  in the Site.Master page is enabled) to overcome this we need to register the all the row click events for event validation by calling RegisterForEventValidation on each one see Listing 7 for the code.

// Register the dynamically created client scripts
protected override void Render(HtmlTextWriter writer)
{
    // The client scripts for gvReleased were created in gvReleased_RowDataBound
    foreach (GridViewRow row in GridView1.Rows)
    {
        if (row.RowType == DataControlRowType.DataRow)
        {
            // validate the controls event
            foreach (Control c in row.Cells[0].Controls)
            {
                if (c is LinkButton)
                {
                    // get link button
                    LinkButton selectButton = (LinkButton)c;

                    if (selectButton.CommandName.ToLower() == "details")
                        Page.ClientScript.RegisterForEventValidation(selectButton.UniqueID);
                }
            }
        }
    }
    base.Render(writer);
}

Listing 7 – validate the controls event

So now we have rollover and click working :D

3. Row double click.

Next and this bit is a bit tricky as we add the extra bits to handle double click.

protected void GridView1_DataBound(object sender, EventArgs e)
{
    foreach (GridViewRow row in GridView1.Rows)
    {
        if (row.RowType == DataControlRowType.DataRow)
        {
            // Add javascript to highlight row
            row.Attributes["onmouseover"] = 
"javascript:NAC_ChangeBackColor(this, true, '#BAD5E8'); this.style.cursor = 'hand';"; row.Attributes["onmouseout"] = "javascript:NAC_ChangeBackColor(this, false, '');"; foreach (Control c in row.Cells[0].Controls) { if (c is LinkButton) { LinkButton selectButton = (LinkButton)c; // Get the javascript which is assigned to this LinkButton String jsClick = ClientScript.GetPostBackClientHyperlink(selectButton, ""); // Get command name in lowercase String commandName = selectButton.CommandName.ToLower(); // Add this javascript to the onclick Attribute of the row if (commandName == "details") row.Attributes["onclick"] = "NAC_StartSingleClick(\"" + jsClick + "\");"; // Add this javascript to the ondblclick Attribute of the row if (commandName == "edit") row.Attributes["ondblclick"] = "NAC_StartDblClick(\"" + jsClick + "\");"; // set the link button to be invisible if (commandName == "details" || commandName == "edit") selectButton.Visible = false; } } } } }

Listing 8 – final OnDataBound event handler

var NAC_TimeoutId = null;

function NAC_StartSingleClick(event)
{
    NAC_TimeoutId = setTimeout(event, 200);
}

function NAC_StartDblClick(event)
{
    window.clearTimeout(NAC_TimeoutId);
    setTimeout(event, 1);
}

Listing 9 – JavaScript functions to stop conflicts with double and single click events

In Listing 8 we altered the row.Attributes assignment to NAC_StartSingleClick(\"" + jsClick + "\"); so instead of calling the event directly it now goes through a launch function (see Listing 9 for details) in both function listed in Listing 9 the passed in button functions are surrounded with a setTimeout function one with a timeout of 200ms and the other 1ms; this is so that when you double click the NAC_StartDblClick has time to clear the waiting single click before it is actioned. This overcomes the click - double click conflict.

And also modify the OnRender event handler to deal with the edit button also.

// Register the dynamically created client scripts
protected override void Render(HtmlTextWriter writer)
{
    foreach (GridViewRow row in GridView1.Rows)
    {
        if (row.RowType == DataControlRowType.DataRow)
        {
            // validate the controls event
            foreach (Control c in row.Cells[0].Controls)
            {
                if (c is LinkButton)
                {
                    // Get linkbutton
                    LinkButton selectButton = (LinkButton)c;

                    // Get command name in lowercase
                    String commandName = selectButton.CommandName.ToLower();

                    if (commandName == "edit" || commandName == "details")
                        Page.ClientScript.RegisterForEventValidation(selectButton.UniqueID);
                }
            }
        }
    }
    base.Render(writer);
}

Listing 10 – final OnRender event handler

The OnRender event now handles both buttons click and double click.

Now if you click anywhere on the row you get redirected to the Details page and if you double click you get redirected to the Edit page.

4. Add the same functionality to ListDetails.aspx

For completeness here are the changes to the ListDetails.aspx if you are using it.

<Columns>
    <asp:TemplateField>
        <ItemTemplate>
            <asp:LinkButton 
                ID="LinkButton2" 
                runat="server" 
                CommandName="Select"
                Text="Select"/>
            <asp:LinkButton 
                ID="LinkButton1" 
                runat="server" 
                CommandName="Edit"
                CausesValidation="true" 
                Text="Edit"/>
            <asp:LinkButton 
                ID="DeleteLinkButton" 
                runat="server" 
                CommandName="Delete"
                CausesValidation="false" 
                Text="Delete"
                OnClientClick='return confirm("Are you sure you want to delete this item?");'/>
        </ItemTemplate>
    </asp:TemplateField>
</Columns>

Listing 11 – changed to the ListDetails.aspx GridView1 columns collection

Note: Don’t forget to update GridView1 with the OnDataBound="GridView1_DataBound" event handler to link it up with the code behind.
protected void GridView1_DataBound(object sender, EventArgs e)
{
    if (GridView1.Rows.Count == 0)
    {
        DetailsView1.ChangeMode(DetailsViewMode.Insert);
    }

    foreach (GridViewRow row in GridView1.Rows)
    {
        if (row.RowType == DataControlRowType.DataRow)
        {
            // Add javascript to highlight row
            row.Attributes["onmouseover"] = 
"javascript:NAC_ChangeBackColor(this, true, '#BAD5E8'); this.style.cursor = 'hand';"; row.Attributes["onmouseout"] = "javascript:NAC_ChangeBackColor(this, false, '');"; foreach (Control c in row.Cells[0].Controls) { if (c is LinkButton) { LinkButton selectButton = (LinkButton)c; // Get the javascript which is assigned to this LinkButton String jsClick = ClientScript.GetPostBackClientHyperlink(selectButton, ""); // Get command name in lowercase String commandName = selectButton.CommandName.ToLower(); // Add this javascript to the onclick Attribute of the row if (commandName == "select") row.Attributes["onclick"] = "NAC_StartSingleClick(\"" + jsClick + "\");"; // Add this javascript to the ondblclick Attribute of the row if (commandName == "edit") row.Attributes["ondblclick"] = "NAC_StartDblClick(\"" + jsClick + "\");"; // set the link button to be invisible if (commandName == "select" || commandName == "edit") selectButton.Visible = false; } } } } } // Register the dynamically created client scripts protected override void Render(HtmlTextWriter writer) { // The client scripts for gvReleased were created in gvReleased_RowDataBound foreach (GridViewRow row in GridView1.Rows) { if (row.RowType == DataControlRowType.DataRow) { // validate the controls event foreach (Control c in row.Cells[0].Controls) { if (c is LinkButton) { // Get linkbutton LinkButton selectButton = (LinkButton)c; // Get command name in lowercase String commandName = selectButton.CommandName.ToLower(); if (commandName == "edit" || commandName == "select") Page.ClientScript.RegisterForEventValidation(selectButton.UniqueID); } } } } base.Render(writer); }

Listing 12 – the additional code to be added to the ListDetails.aspx.cs code behind file

Here the single click activates Select and the double click activates Edit.

Note: This could be added the to the GridView_Edit FieldTemplate I did here An Advanced FieldTemplate with a GridView.

Happy coding smile_teeth

Thursday, 7 August 2008

Dynamic Data and Field Templates - An Advanced FieldTemplate with a GridView ***UPDATED 2008/09/24***

  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.

The Idea for this FieldTemplate came from Nigel Basel post on the Dynamic Data forum where he said he needed to have a GridView emended in another data control i.e. FormView so he could use the AjaxToolkit Tab control. So here it is with some explanation.

Files Required for Project

In this project we are going to convert a PageTemplate into a FieldTemplate so in your project you will need to copy the List.aspx and it’s code behind List.aspx.cs to the FieldTemplate folder. When copied rename the file to GridView_Edit.ascx and change the class name to GridView_EditField see Listings 1 & 2.

<%@ Page 
    Language="C#" 
    MasterPageFile="~/Site.master" 
    CodeFile="List.aspx.cs" 
    Inherits="List" %>

to

<%@ Control 
    Language="C#" 
    CodeFile="GridView_Edit.ascx.cs" 
    Inherits="GridView_EditField" %>

Listing 1 – Changing the class name of the GridView_Edit.ascx file

public partial class List : System.Web.UI.Page
{
...
}

to

public partial class GridView_EditField : FieldTemplateUserControl
{
...
}

Listing 2 - Changing the class name of the GridView_Edit.ascx.cs code behind file

Now in the GridView_Edit.ascx file trim out all the page relavent code:

i.e. remove the following tags (and their closing tags where applicable)

<asp:Content
<asp:UpdatePanel
<ContentTemplate>
<%@ Register 
    src="~/DynamicData/Content/FilterUserControl.ascx" 
    tagname="DynamicFilter" 
    tagprefix="asp" %>

<asp:DynamicDataManager 
    runat="server" 
    ID="DynamicDataManager1" 
    AutoLoadForeignKeys="true" />

Also remove from the GridView the columns tags and everything in them, and then add the following properties to the GridView:

AutoGenerateColumns="true"
AutoGenerateDeleteButton="true"
AutoGenerateEditButton="true"

and you should end up with something like Listing 3.

<%@ Control 
    Language="C#" 
    CodeFile="GridView_Edit.ascx.cs" 
    Inherits="GridView_EditField" %>

<%@ Register 
    src="~/DynamicData/Content/GridViewPager.ascx" 
    tagname="GridViewPager" 
    tagprefix="asp" %>

<asp:DynamicDataManager 
    runat="server" 
    ID="DynamicDataManager1" 
    AutoLoadForeignKeys="true" />

<asp:ScriptManagerProxy 
    runat="server" 
    ID="ScriptManagerProxy1" />

<asp:ValidationSummary 
    runat="server" 
    ID="ValidationSummary1" 
    EnableClientScript="true"
    HeaderText="List of validation errors" />
    
<asp:DynamicValidator 
    runat="server" 
    ID="GridViewValidator" 
    ControlToValidate="GridView1" 
    Display="None" />

<asp:GridView 
    runat="server" 
    ID="GridView1" 
    DataSourceID="GridDataSource"
    AllowPaging="True" 
    AllowSorting="True" 
    AutoGenerateColumns="true"
    AutoGenerateDeleteButton="true"
    AutoGenerateEditButton="true"
    CssClass="gridview">

    <PagerStyle CssClass="footer"/> 
           
    <PagerTemplate>
        <asp:GridViewPager runat="server" />
    </PagerTemplate>
    
    <EmptyDataTemplate>
        There are currently no items in this table.
    </EmptyDataTemplate>
    
</asp:GridView>

<asp:LinqDataSource 
    runat="server" 
    ID="GridDataSource" 
    EnableDelete="true">
</asp:LinqDataSource>

Listing 3 – the finished GridView_Edit.ascx file

Now we’ll trim down the GridView_Edit.ascx.cs, the first things to remove are the following methods and event handlers:

protected void Page_Load(object sender, EventArgs e)
protected void OnFilterSelectedIndexChanged(object sender, EventArgs e)

This will leave us with just the Page_Init to fill in see Listing 4 for it.

protected void Page_Init(object sender, EventArgs e)
{
    var metaChildColumn = Column as MetaChildrenColumn;
    var attribute = Column.Attributes.OfType<ShowColumnsAttribute>().SingleOrDefault();

    if (attribute != null)
    {
        if (!attribute.EnableDelete)
            EnableDelete = false;
        if (!attribute.EnableUpdate)
            EnableUpdate = false;
        if (attribute.DisplayColumns.Length > 0)
            DisplayColumns = attribute.DisplayColumns;
    }

    var metaForeignKeyColumn = metaChildColumn.ColumnInOtherTable as MetaForeignKeyColumn;

    if (metaChildColumn != null && metaForeignKeyColumn != null)
    {
        GridDataSource.ContextTypeName = metaChildColumn.ChildTable.DataContextType.Name;
        GridDataSource.TableName = metaChildColumn.ChildTable.Name;

        // enable update, delete and insert
        GridDataSource.EnableDelete = EnableDelete;
        GridDataSource.EnableInsert = EnableInsert;
        GridDataSource.EnableUpdate = EnableUpdate;
        GridView1.AutoGenerateDeleteButton = EnableDelete;
        GridView1.AutoGenerateEditButton = EnableUpdate;

        // get an instance of the MetaTable
        table = GridDataSource.GetTable();

        // Generate the columns as we can't rely on 
        // DynamicDataManager to do it for us.
        GridView1.ColumnsGenerator = new FieldTemplateRowGenerator(table, DisplayColumns);

        // setup the GridView's DataKeys
        String[] keys = new String[metaChildColumn.ChildTable.PrimaryKeyColumns.Count];
        int i = 0;
        foreach (var keyColumn in metaChildColumn.ChildTable.PrimaryKeyColumns)
        {
            keys[i] = keyColumn.Name;
            i++;
        }
        GridView1.DataKeyNames = keys;

        GridDataSource.AutoGenerateWhereClause = true;
    }
    else
    {
        // throw an error if set on column other than MetaChildrenColumns
        throw new InvalidOperationException("The GridView FieldTemplate can only be used with MetaChildrenColumns");
    }
}

Listing 4 – the Page_Init ***UPDATED 2008/09/24***

UPDATED 2008/09/24: Here I’ve completely remove the creation of the WHERE parameter into the OnDataBinding event handler see Listing 4a to handle multiple FK-PK relationships.
protected override void OnDataBinding(EventArgs e)
{
    base.OnDataBinding(e);

    var metaChildrenColumn = Column as MetaChildrenColumn;
    var metaForeignKeyColumn = metaChildrenColumn.ColumnInOtherTable as MetaForeignKeyColumn;

    // get the association attributes associated with MetaChildrenColumns
    var association = metaChildrenColumn.Attributes.
        OfType<System.Data.Linq.Mapping.AssociationAttribute>().FirstOrDefault();

    if (metaForeignKeyColumn != null && association != null)
    {

        // get keys ThisKey and OtherKey into Pairs
        var keys = new Dictionary<String, String>();
        var seperator = new char[] { ',' };
        var thisKeys = association.ThisKey.Split(seperator);
        var otherKeys = association.OtherKey.Split(seperator);
        for (int i = 0; i < thisKeys.Length; i++)
        {
            keys.Add(otherKeys[i], thisKeys[i]);
        }

        // setup the where clause 
        // support composite foreign keys
        foreach (String fkName in metaForeignKeyColumn.ForeignKeyNames)
        {
            // get the current pk column
            var fkColumn = metaChildrenColumn.ChildTable.GetColumn(fkName);

            // setup parameter
            var param = new Parameter();
            param.Name = fkColumn.Name;
            param.Type = fkColumn.TypeCode;

            // get the PK value for this FK column using the fk pk pairs
            param.DefaultValue = Request.QueryString[keys[fkName]];

            // add the where clause
            GridDataSource.WhereParameters.Add(param);
        }
    }
    // doing the work of this above because we can't
    // set the DynamicDataManager table or where values
    //DynamicDataManager1.RegisterControl(GridView1, false);
}

Listing 4a – OnDataBinding event handler ***ADDED 2008/09/24*** 

UPDATED 2008/08/11: This update removes the need for the Attribute defined in Listing 6 the GridView_Edit.ascx FieldTemplate now supports getting foreign key(s) automatically and support Clustered/Composite Keys. :D See this post from Zwitterion here Re: Dynamic child details on a dynymic details or edit page? where her mentions the Column.ColumnInOtherTable which gave me the idea for the above changes :D
UPDATED 2008/08/08: I’ve updated the above code in reply to a post where Zwitterion points out that I make the assumption that the child’s FK column is the same name as the parents PK column. so I’ve added an Attribute to fix this. And now I’ve added some more error handling to cover misuse of FieldTemplate

And now we will need to implement the GridViewColumnGenerator to fill in the rows as the DynamicDataManager would have done.

public class GridViewColumnGenerator : IAutoFieldGenerator
{
    protected MetaTable _table;

    public GridViewColumnGenerator(MetaTable table)
    {
        _table = table;
    }

    public ICollection GenerateFields(Control control)
    {
        List<DynamicField> oFields = new List<DynamicField>();

            foreach (var column in _table.Columns)
            {
                // carry on the loop at the next column  
                // if scaffold table is set to false or DenyRead
                if (!column.Scaffold)
                    continue;

                DynamicField field = new DynamicField();

                field.DataField = column.Name;
                oFields.Add(field);
            }
        return oFields;
    }
}
Listing 5 – the GridViewColumnGenerator class

In Listing 5 we have the GridViewColumnGenerator class which you can just tag onto the end the GridView_Edit.ascx.cs file as it is only used here.

[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public class GridViewTemplateAttribute : Attribute
{
    public String ForeignKeyColumn { get; private set; }

    public GridViewTemplateAttribute(string foreignKeyColumn)
    {
        ForeignKeyColumn = foreignKeyColumn;
    }
}

Listing 6 – GridViewTemplateAttribute for the above ***UPDATE 2008/08/08 *** :D

Note: This GridViewColumnGenerator class would need to be changed to the FilteredFieldsManager of the my permissions add on for Dynamic Data that I did on my blog here and here

GridView_Edit FieldTemplate in action

Figure 1 GridView_Edit FieldTemplate in action

!Important: Because you will need to apply UIHint metadata to the ChildrenColumn you wish to use this with you will also need to copy the FieldTemplate Children.ascx to GridView.ascx as in the previous post in this series, to have the column show in non edit/insert modes.

[UIHint("GridView")]
public object Order_Details { get; set; }

Listing 7 – Metadata

Until next time smile_teeth

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