Wednesday, 3 September 2008

Dynamic Data and Field Templates - An Advanced FieldTemplate with a DetailsView ***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.

In this addition the FieldTemplates series I was asked to produce one that looked back up the relationship with the parent table to get some or all of the properties.

The basis for this FieldTemplate is the previous GridView_Edit FieldTemplate. This time I’m just going to post the files and then discuss the alterations, so here goes:

<%@ Control 
    Language="C#" 
    CodeFile="DetailsView_Edit.ascx.cs" 
    Inherits="DetailsView_EditField" %>

<asp:ValidationSummary ID="ValidationSummary1" 
    D="ValidationSummary1" 
    runat="server" 
    EnableClientScript="true"
    HeaderText="List of validation errors" />
    
<asp:DynamicValidator 
    runat="server" 
    ID="DetailsViewValidator" 
    ControlToValidate="DetailsView1"
    Display="None" />
    
<asp:DetailsView 
    ID="DetailsView1" 
    runat="server" 
    DataSourceID="DetailsDataSource"
    CssClass="detailstable"
    AutoGenerateDeleteButton="true"
    AutoGenerateEditButton="true"
    AutoGenerateInsertButton="true"
    FieldHeaderStyle-CssClass="bold">
    
</asp:DetailsView>

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

Listing 1 - DetailsView_Edit.ascx

using System;
using System.Data;
using System.Configuration;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
using System.Xml.Linq;
using System.Web.DynamicData;

public partial class ParentDetails_EditField : FieldTemplateUserControl
{
    protected MetaTable parentTable;
    protected MetaTable childTable;

    public Boolean EnableDelete { get; set; }
    //public Boolean EnableInsert { get; set; }
    public Boolean EnableUpdate { get; set; }

    public String[] DisplayColumns { get; set; }

    public ParentDetails_EditField()
    {
        // set default values
        EnableDelete = true;
        EnableUpdate = true;
        //EnableInsert = false;
    }

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

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

        var metaForeignKeyColumn = Column as MetaForeignKeyColumn;

        if (metaForeignKeyColumn != null)
        {
            childTable = metaForeignKeyColumn.Table;

            // setup data source
            DetailsDataSource.ContextTypeName = metaForeignKeyColumn.ParentTable.DataContextType.Name;
            DetailsDataSource.TableName = metaForeignKeyColumn.ParentTable.Name;

            // enable update, delete and insert
            DetailsDataSource.EnableDelete = EnableDelete;
            DetailsDataSource.EnableInsert = false; // EnableInsert;
            DetailsDataSource.EnableUpdate = EnableUpdate;
            DetailsView1.AutoGenerateDeleteButton = EnableDelete;
            DetailsView1.AutoGenerateInsertButton = false; // EnableInsert;
            DetailsView1.AutoGenerateEditButton = EnableUpdate;

            // get an instance of the MetaTable
            parentTable = DetailsDataSource.GetTable();

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

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

            // enable AutoGenerateWhereClause so that the WHERE 
            // clause is generated from the parameters collection
            DetailsDataSource.AutoGenerateWhereClause = true;

            // doing the work of this above because we can't
            // set the DynamicDataManager table or where values
            //DynamicDataManager1.RegisterControl(DetailsView1, false);
        }
        else
        {
            // throw an error if set on column other than MetaChildrenColumns
            throw new InvalidOperationException("The GridView FieldTemplate can only be used with MetaChildrenColumns");
        }
    }

    protected override void OnDataBinding(EventArgs e)
    {
        base.OnDataBinding(e);

        // get the fk column
        var metaForeignKeyColumn = Column as MetaForeignKeyColumn;

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

        if (metaForeignKeyColumn != null && association != null)
        {
            // get keys ThisKey and OtherKey into dictionary
            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(thisKeys[i], otherKeys[i]);
            }

            // setup the where clause 
            // support composite foreign keys
            foreach (String fkName in metaForeignKeyColumn.ForeignKeyNames)
            {
                // get the current FK column
                var fkColumn = metaForeignKeyColumn.Table.GetColumn(fkName);
                // get the current PK column
                var pkColumn = metaForeignKeyColumn.ParentTable.GetColumn(keys[fkName]);

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

                // get the value for this FK column
                param.DefaultValue = GetColumnValue(fkColumn).ToString();

                // add the where clause
                DetailsDataSource.WhereParameters.Add(param);
            }
        }
    }
}

Listing 2 - DetailsView_Edit.ascx.cs ***UPDATED 2008/09/24***

UPDATED 2008/09/24: The OnDataBinding event handler has been updated to handle multiple PK-FK relationships.

As you can see from examining the above file the main thing is the change of the GridView to DetailsView, however you will notice that part of the code has been remove from the Page_Init the OnDataBinding. This is because we are coming at this from the other end the value for the ForeignKey to link the DetailsView to the parent control is no longer in the Request.QueryString and so we have to extract it in the OnDataBinding event handler as access to column values is not valid untill OnDataBinding.

You will also I’ve added some properties to enable features of the DetailsView either declaratively or via attributes:

[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public class DetailsViewTemplateAttribute : Attribute
{
    public DetailsViewTemplateAttribute(params String[] displayColumns)
    {
        DisplayColumns = displayColumns;
    }

    public String[] DisplayColumns { get; set; }
    public Boolean EnableDelete { get; set; }
    public Boolean EnableInsert { get; set; }
    public Boolean EnableUpdate { get; set; }
}

Listing 3 - DetailsViewTemplateAttribute.cs

[MetadataType(typeof(OrderMD))]
public partial class Order
{
    public class OrderMD
    {
        [UIHint("DetailsView")]
        [DetailsViewTemplate
            (
                "Title",
                "FirstName",
                "LastName", 
                "Region",
                "Extension", 
                EnableDelete=false, 
                EnableUpdate=false, 
                EnableInsert=false
            )]
        public object Employee { get; set; }
    }
}

Listing 4 – Northwind Partials and Metadata

As you can see from Listing 3 and Listing 4 you are able to enable or disable Update, Delete or Insert on the DetailsView FieldTemplate and also specify which columns you want to appear in the particular instance.

public class DetailsViewRowGenerator : IAutoFieldGenerator
{
    protected MetaTable _table;
    protected String[] _displayColumns;

    public DetailsViewRowGenerator(MetaTable table, String[] displayColumns)
    {
        _table = table;
        _displayColumns = displayColumns;
    }

    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;

            if (_displayColumns != null && !_displayColumns.Contains(column.Name))
                continue;

            DynamicField f = new DynamicField();

            f.DataField = column.Name;
            oFields.Add(f);
        }
        return oFields;
    }
}

Listing 5 – DetailsViewRowGenerator (tagged on to the end of the DetailsView_Edit.ascx.cs file)

And finally the DetailsViewRowGenerator wether to show some or all of the columns in the parent table, the most important line here is:

if (_displayColumns != null && !_displayColumns.Contains(column.Name))
    continue;

which test first to see of any columns have been specified and if so the check to see if the current column is not present in the list and then drops the column appropriately, otherwise the IAutoFieldGenerator implementation is pretty much the same as the GridView FieldTemplate.

See if working below:

DetailsView_Edit FieldTemplate at work

Figure 1 - DetailsView_Edit FieldTemplate at work

No comments: