Sunday 31 May 2009

Sorting Filters in Dynamic Data (DD v1.0)

This is just a small sample of how to sort your filters in Dynamic Data using the ColumnOrderAttribute from the Dynamic Data Futures project. I was surprised at how easy it was and that I’d not thought of it before.

using System;
using System.Linq;
using System.Web.DynamicData;

namespace DynamicData.CascadeExtensions
{
    /// <summary>
    /// Allows to specify the ordering of columns. 
/// Columns are will be sorted in increasing order based
/// on the Order value. Columns without this attribute
/// have a default Order of 0. Negative values are
/// allowed and can be used to place a column before all other columns. /// </summary> [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field,
Inherited = true,
AllowMultiple = false)] public class ColumnOrderAttribute : Attribute, IComparable { public static ColumnOrderAttribute Default = new ColumnOrderAttribute(0); public ColumnOrderAttribute(int order) { Order = order; } /// <summary> /// The ordering of a column. Can be negative. /// </summary> public int Order { get; private set; } public int ListOrder { get; set; } public int CompareTo(object obj) { return Order - ((ColumnOrderAttribute)obj).Order; } } public static partial class HelperExtansionMethods { public static ColumnOrderAttribute GetColumnOrdering(this MetaColumn column) { return column.Attributes.OfType<ColumnOrderAttribute>()
.DefaultIfEmpty(ColumnOrderAttribute.Default).First(); } } }

Listing 1 - Column Order attribute

So Listing 1 is just a direct rip off of the ColumnOrderAttribute in the Futures project. and then the code to order the filters based on the ColumnOrderAttribute.

protected void Page_Load(object sender, EventArgs e)
{
    table = GridDataSource.GetTable();
    Title = table.DisplayName;

    InsertHyperLink.NavigateUrl = table.GetActionPath(PageAction.Insert);

    // Disable various options if the table is readonly
    if (table.IsReadOnly)
    {
        GridView1.Columns[0].Visible = false;
        InsertHyperLink.Visible = false;
    }

    // set the filter order
    var filters = from c in table.Columns
                  where c is MetaForeignKeyColumn
                  orderby c.GetColumnOrdering()
                  select c;
    FilterRepeater.DataSource = filters;
    FilterRepeater.DataBind();
}

Listing 2 – modified Page_Load event handler

Here in Listing 2 you can see the code highlighted in bold italic is setting the filter sort order simples.

Note: the only caveat is that the filters are generated twice smile_sad

That caveat in mind I decided to do this instead:

public class SortedFilterRepeater : FilterRepeater
{
    protected override IEnumerable<MetaColumn> GetFilteredColumns()
    {
        // sort the filters by their filter order as specified in FilterAttribute.
        // Table.Columns.Where(c => IsFilterableColumn(c))
// .OrderBy(column => FilterOrdering(column));
var filteredColumns = from c in Table.Columns where IsFilterableColumn(c) orderby FilterOrdering(c) select c; return filteredColumns; } protected bool IsFilterableColumn(MetaColumn column) { // don't filter custom properties by default if (column.IsCustomProperty) return false; // always filter FK columns and bools if (column is MetaForeignKeyColumn) return true; if (column.ColumnType == typeof(bool)) return true; return false; } private ColumnOrderAttribute FilterOrdering(MetaColumn column) { return column.Attributes
.OfType<ColumnOrderAttribute>()
.DefaultIfEmpty(ColumnOrderAttribute.Default)
.First(); } }

Listing 3 – SortedFilterRepeater

Updated: Listing 3 is just a ripp-off on the AdvancedFilterRepeater in the Dynamic Data Futures sample.

I implemented this using by adding a mapping to the web.config as in Listing 4.

<pages>
    <tagMapping>
        <add tagType="System.Web.DynamicData.FilterRepeater"
mappedTagType="DynamicData.CascadeExtensions.SortedFilterRepeater" /> </tagMapping> </pages>

Listing 4 – Adding mapping to web.config

The advantage of the above is:

  1. It only generates the filters once
  2. This will work with my CascadingFilters

So I think this addendum wraps this up as we have sorting of filters in Dynamic Data Preview 4.

Saturday 30 May 2009

Move Command Link Column to End Column – Dynamic Data

Whilst answering this question Move Edit, Details, Delete (col 0) to the Rightmost column of table on the Dynamic Data Forum I found this had been answered before (Move GridView command column or dynamically append columns) and decided to post it to my blog in full for easy access.

The post suggest using IAutoFieldGenerator to add the command column to the end like this:

public ICollection GenerateFields(Control control)
{
    // Get all of table's columns, take only the ones that should be automatically included in a fields collection,
    // sort the result by the ColumnOrderAttribute, and for each column create a DynamicField
    var fields = from column in _table.Columns
                 where IncludeField(column)
                 orderby ColumnOrdering(column), column.Name
                 select new DynamicField()
                 {
                     DataField = column.Name,
                     HeaderText = column.DisplayName
                 };

    List<DynamicField> flds = fields.ToList();
    if (_table.PrimaryKeyColumns.Count > 0)
    {
        // get the first primary key field                
        DynamicField ctrl = new DynamicField();
        ctrl.HeaderText = "Commands";
        ctrl.DataField = _table.PrimaryKeyColumns[0].Name;
        ctrl.UIHint = "GridCommand";
        flds.Add(ctrl);
    }

    return flds;
}

Listing 1 – GenerateFields method of the IAutoFieldGenerator

The main change to my usual IAutoFieldGenerator is shown in Bold Italic this works by only adding the GridCommand FieldTemplate if the table a=has a Primary Key.

<asp:HyperLink 
    ID="EditHyperLink" 
    runat="server" 
    NavigateUrl='<%# Table.GetActionPath(PageAction.Edit, Row) %>' 
    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, Row) %>' 
    Text="Details" />

Listing 2 – GridCommand FieldTemplate

In Listing 2 we see the GridCommand FieldTemplate layed out neatly (you may need to tidy it so that it is similar to the section it is copied form in the List page. >&nbsp;<)

Download

The download contains the full IAutoFieldGenerator and GridCommand FieldTemplate plus Column sort capability.

Wednesday 20 May 2009

Password FieldTemplates for Dynamic Data (UPDATED)

See this thread on the Dynamic Data Forum where I suggested you could use [DataType(DataType.Password)] attribute to mark the field as a password field and alternative would be to create a custom FieldTemplate and use [UIHint("Password")] to set it.

This just means the extra logic I implement for the password is only used on password fields and not all Text fields.

<%@ Control 
    Language="C#" 
    CodeBehind="Password.ascx.cs" 
    Inherits="DD_EF4_PasswordField.PasswordField" %>

<asp:Literal runat="server" ID="Literal1" />

Listing 1 – Password.ascx

using System;
using System.Web.UI;

namespace DD_EF4_PasswordField
{
    public partial class PasswordField : System.Web.DynamicData.FieldTemplateUserControl
    {
        protected void Page_Load(object sender, EventArgs e)
        {
            // add a string of '*' the max length of the field
            var length = Column.MaxLength > 20 ? 20 : Column.MaxLength;
            Literal1.Text = new String('*', length);
        }

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

Listing 2 – Password.ascx.cs

Listing 1 & 2 represent the Password (Normal) FieldTemplate.

This is FieldTemplate just displays a list of '*' the max length of the field instead of the actual text.

<%@ Control 
    Language="C#" 
    CodeBehind="Password_Edit.ascx.cs" 
    Inherits="DD_EF4_PasswordField.Password_EditField" %>

<asp:TextBox 
    ID="TextBox1" 
    runat="server" 
    TextMode="Password"
    CssClass="DDTextBox" >
</asp:TextBox>

<asp:RequiredFieldValidator 
    runat="server" 
    ID="RequiredFieldValidator1"
    CssClass="DDControl" 
    ControlToValidate="TextBox1" 
    Display="Dynamic" 
    Enabled="false" />
<asp:RegularExpressionValidator 
    runat="server" 
    ID="RegularExpressionValidator1" 
    CssClass="DDControl" 
    ControlToValidate="TextBox1" 
    Display="Dynamic" 
    Enabled="false" />
<asp:DynamicValidator 
    runat="server" 
    ID="DynamicValidator1" 
    CssClass="DDControl" 
    ControlToValidate="TextBox1" 
    Display="Dynamic" />

Listing 3 – Password_Edit.ascx

using System;
using System.Collections.Specialized;
using System.Web.DynamicData;
using System.Web.UI;

namespace DD_EF4_PasswordField
{
    public partial class Password_EditField : FieldTemplateUserControl
    {
        protected void Page_Load(object sender, EventArgs e)
        {
            if (Column.MaxLength < 20)
            {
                TextBox1.Columns = Column.MaxLength;
            }
            TextBox1.ToolTip = Column.Description;

            SetUpValidator(RequiredFieldValidator1);
            SetUpValidator(RegularExpressionValidator1);
            SetUpValidator(DynamicValidator1);
        }

        protected override void OnDataBinding(EventArgs e)
        {
            base.OnDataBinding(e);
            TextBox1.MaxLength = Math.Max(FieldValueEditString.Length, Column.MaxLength);
        }

        protected override void ExtractValues(IOrderedDictionary dictionary)
        {
            if (TextBox1.Text.Length > 0)
                dictionary[Column.Name] = ConvertEditedValue(TextBox1.Text);
        }

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

Listing 4 – Password_Edit.ascx.cs

Listings 3 & 4 are for the Edit version Password_Edit FieldTemplate.

Updated: Listing 4 has been updated with the line if (TextBox1.Text.Length > 0) so that in edit mode if you do not enter a value it will not return an empty string.

Here the TextBox’s TextMode is set to Password so that any password you type in is hidden.

Note: The TextBox does not show any text in the TextBox.Text property when in Password mode this is normal behaviour.

Friday 15 May 2009

Remembering Page Index when returning to a List Page

This article is a continuation of the previous Retaining Pager Size in Dynamic Data GridViewPager now we want to retain the page index when moving between pages. For this we will use AJAX History.

Note: Also see this article back in February AJAX History in a Dynamic Data Website

I won’t go over the AJAX History in detail as Mike Ormond has done a great video on it on MSDN Screencasts here Managing Browser History with ASP.NET AJAX and the ASP.NET 3.5 Extensions Preview.

So let’s start.

Fist we will add a global private variable  to hold a reference to the ScriptManager.

private ScriptManager _scriptManager;

Next we need to initialise a few things

/// <summary>
/// Handles the Init event of the Page control.
/// </summary>
/// <param name="sender">The source of the event.</param>
/// <param name="e">
/// The <see cref="System.EventArgs"/> instance 
/// containing the event data.
/// </param>
protected void Page_Init(object sender, EventArgs e)
{
    // get the containing GridView
    _gridView = this.GetParentOfType<GridView>();
    if (_gridView != null)
    {
        // add event to GridView to collect page index changed
        _gridView.PageIndexChanged += GridViewPageIndexChanged;

        // get the datasource on the gridview
        var GridDataSource = _gridView.FindDataSourceControl();
        
        // get the table
        _table = GridDataSource.GetTable();

        // Add OnNavigate handler to restore History points.
        _scriptManager = ScriptManager.GetCurrent(Page);
        if (_scriptManager != null)
            // add the navigate handler
            _scriptManager.Navigate += ScriptManagerOnNavigate;
    }
}

Listing 1 – Page_Init for the GridViewPager

In Listing 1 we get the GridView and ScriptManger, we then add a handler for the PageIndexChanged event on the GridView and add an event handler for the Navigate event of the ScriptManager.

/// <summary>
/// Grids the view page index changed.
/// </summary>
/// <param name="sender">The sender.</param>
/// <param name="e">
/// The <see cref="System.EventArgs"/> 
/// instance containing the event data.
/// </param>
protected void GridViewPageIndexChanged(object sender, EventArgs e)
{
    // add history point
    if (_scriptManager != null)
        _scriptManager.AddHistoryPoint("PageIndex", _gridView.PageIndex.ToString());
}

/// <summary>
/// Scripts the manager on navigate.
/// </summary>
/// <param name="sender">The sender.</param>
/// <param name="e">
/// The <see cref="System.Web.UI.HistoryEventArgs"/> 
/// instance containing the event data.
/// </param>
protected void ScriptManagerOnNavigate(object sender, HistoryEventArgs e)
{
    // restore history point
    if (!String.IsNullOrEmpty(e.State["PageIndex"]))
        _gridView.PageIndex = int.Parse(e.State["PageIndex"]);
}

Listing 2 – Handlers for the PageIndexChanged and Navigate events

Updated: Removed HistoryPointValue = e.State["PageIndex"]; not required. I think HistoryPointValue was originally a global private member variable.

In Listing 2 we are adding history points and restoring them again we also add a history point in Listing 3 if the page index TextBox is changed.

protected void TextBoxPage_TextChanged(object sender, EventArgs e)
{
    if (_gridView == null)
        return;

    int page;
    if (int.TryParse(TextBoxPage.Text.Trim(), out page))
    {
        if (page <= 0)
            page = 1;
        if (page > _gridView.PageCount)
            page = _gridView.PageCount;
        _gridView.PageIndex = page - 1;

        // add history point
        if (_scriptManager != null)
            _scriptManager.AddHistoryPoint("PageIndex", _gridView.PageIndex.ToString());
    }
    TextBoxPage.Text = (_gridView.PageIndex + 1).ToString();
}

Listing 3 – Changes to the TextBoxPage_TextChanged event handler

And that is pretty much it HappyWizard

Thursday 14 May 2009

Retaining Pager Size in Dynamic Data GridViewPager

For the motivation for this article see Retain "Number of Records" to display in GridView by defyant_2004 where the idea is, if I change the the pager size in the list page go to another page and then return to the ListPage chances are I still want the pager size to be the same.

So here the simple solution:

protected void DropDownListPageSize_SelectedIndexChanged(object sender, EventArgs e)
{
    if (_gridView == null)
        return;

    DropDownList dropdownlistpagersize = (DropDownList)sender;
    _gridView.PageSize = Convert.ToInt32(dropdownlistpagersize.SelectedValue);

    // save pager size
    if (Session["DD_PagerSize"] != null)
        Session["DD_PagerSize"] = _gridView.PageSize.ToString();
    else
        Session.Add("DD_PagerSize", _gridView.PageSize.ToString());

    int pageindex = _gridView.PageIndex;
    _gridView.DataBind();
    if (_gridView.PageIndex != pageindex)
    {
        //if page index changed it means the previous page was not valid and was adjusted. Rebind to fill control with adjusted page
        _gridView.DataBind();
    }
}

Listing 1 – the DropDownListPageSize_SelectedIndexChanged event for the GridViewPager

protected void Page_Load(object sender, EventArgs e)
{
    Control c = Parent;
    while (c != null)
    {
        if (c is GridView)
        {
            _gridView = (GridView)c;

            // if pager size saved then restore it
            var pagerSize = Session["DD_PagerSize"];
            if (_gridView != null && pagerSize != null)
            {
                _gridView.PageSize = Convert.ToInt32(pagerSize);
            }

            break;
        }
        c = c.Parent;
    }
}

Listing 2 – the Page_Load event for the GridViewPager

Listings 1 & 2 are the minimum we need to do to get this functionality see the Forum post for a VB example.

But my thoughts turn to what if I want a different setting for different tables?

So here it is:

Add a private variable to the user control

public partial class GridViewPager : System.Web.UI.UserControl
{
    private GridView _gridView;
    private MetaTable _table;

Listing 3 – global _table variable

protected void Page_Load(object sender, EventArgs e)
{
    Control c = Parent;
    while (c != null)
    {
        if (c is GridView)
        {
            _gridView = (GridView)c;

            // if pager size saved then restore it
            var GridDataSource = _gridView.FindDataSourceControl();
            _table = GridDataSource.GetTable();

            var pagerSize = Session[_table.Name + "_PagerSize"];
            if (_gridView != null && pagerSize != null)
            {
                _gridView.PageSize = Convert.ToInt32(pagerSize);
            }

            break;
        }
        c = c.Parent;
    }
}

Listing 4 – Page_Load

In Listing 4 we get the GridViews DataSource and then find out which table we are working on, then we create a session variable to hold the pager size for this table by combining the table name with “_PagerSize” string.

protected void DropDownListPageSize_SelectedIndexChanged(object sender, EventArgs e)
{
    if (_gridView == null)
        return;

    DropDownList dropdownlistpagersize = (DropDownList)sender;
    _gridView.PageSize = Convert.ToInt32(dropdownlistpagersize.SelectedValue);

    // save pager size
    if (Session[_table.Name + "_PagerSize"] != null)
        Session[_table.Name + "_PagerSize"] = _gridView.PageSize.ToString();
    else
        Session.Add(_table.Name + "_PagerSize", _gridView.PageSize.ToString());

    int pageindex = _gridView.PageIndex;
    _gridView.DataBind();
    if (_gridView.PageIndex != pageindex)
    {
        // if page index changed it means the previous page was page 
        // not valid and was adjusted. Rebind to fill control with adjusted
        _gridView.DataBind();
    }
}

Listing 5 - DropDownListPageSize_SelectedIndexChanged

Now in Listing 5 we can retrieve the pager size based on table name.

I think this may be useful. HappyWizard

Friday 8 May 2009

Communicating Between FieldTemplates in Dynamic Data (UPDATED)

A question that is asked a lot on the Dynamic Data Forum is how can I get a reference to a FieldTemplate, the reason people ask this is because they are used to doing things this was from classic ASP.Net code; the problem with this is that it leads to specialised code in the page, which means custom page and I always go for custom FieldTemplate over custom page.

The problem with most of the custom FieldTemplates I’ve written for production code is that they are not generic which can be ok, but I tend to find myself writing the same sort of things again and again. So with question on the Dynamic Data Forum and on this blog I thought I’d tackle one of these types of problem in a more generic reusable way. This solution come from the previous cascading articles I’ve culminating with Cascading Filters and Fields – Dynamic Data Entity Framework Version which allows fields and filters to cascade. Here I’m going to use the same event model so one control can alert other controls to a change in it’s state thus facilitating say a checkbox hiding or disabling other field on the form depending upon its state. In this article I’m going to look at Checkbox as parent controls enabling other controls to change there status.

What we will need to Build This.

  • Event Interface
  • Event Delegate
  • EventArgs
  • Implementations
    • Parent
    • Child

The Code

Here I will quickly layout the code (each listing is fully commented) we are going to use it is not majorly different form the Cascading FieldTemplate mentioned  here

/// <summary>
/// The interface for parent controls to implement.
/// </summary>
public interface IChangeNotifyingFieldTemplate
{
    /// <summary>
    /// Gets the parent column.
    /// </summary>
    /// <value>The parent column.</value>
    MetaColumn ParentColumn { get;}

    /// <summary>
    /// Gets the state.
    /// </summary>
    /// <value>The state.</value>
    String State { get; }

    /// <summary>
    /// Occurs when [state changed].
    /// </summary>
    event ChangingAwareEventHandler StateChanged;
}

Listing 1 – the IChangingAware event interface

In Listing 1 we have our interface which has an event and three properties we will need top implement in our FieldTemplates. Now we will need a way of sending the current status of the parent control to the child control for this will will use an EventArgs class.

/// <summary>
/// Event Arguments for Changing Aware Event
/// </summary>
public class ChangingAwareEventArgs : EventArgs
{
    /// <summary>
    /// Custom event arguments for SelectionChanged 
    /// event of the ParentChangingAwareFieldTemplate control
    /// </summary>
    /// <param name="value">
    /// The value of the currently selected 
    /// value of the parent control
    /// </param>
    public ChangingAwareEventArgs(String state)
    {
        State = state;
    }
    /// <summary>
    /// The values from the control of the parent control
    /// </summary>
    public String State { get; set; }
}

Listing 2 – Changing Aware EventArgs

As you can see in Listing 2 Changing Aware EventArgs has only one property which is a string for simplicity. We will use Value to pass the current value of the parent control to the child.

/// <summary>
/// Delegate for the changing aware Interface
/// </summary>
/// <param name="sender">Parent Control</param>
/// <param name="e">An instance of the ChangingAwareEventArgs</param>
public delegate void ChangingAwareEventHandler(
    object sender,
    ChangingAwareEventArgs e);

Listing 3 – The delegate for our parent and child controls

In Listing 3 you can see the delegate for our controls event.

public class ParentChangeNotifyingFieldTemplate 
: FieldTemplateUserControl, IChangeNotifyingFieldTemplate { /// <summary> /// Gets or sets the value. /// </summary> /// <value>The state.</value> public virtual String State { get; private set; } /// <summary> /// Gets or sets the parent column. /// </summary> /// <value>The parent column.</value> public MetaColumn ParentColumn { get; private set; } /// <summary> /// publish event. /// </summary> public event ChangingAwareEventHandler StateChanged; /// <summary> /// Raises the <see cref="E:System.Web.UI.Control.Init"/> event. /// </summary> /// <param name="e"> /// An <see cref="T:System.EventArgs"/> /// object that contains the event data. /// </param> protected override void OnInit(EventArgs e) { ParentColumn = Column; base.OnInit(e); } /// <summary> /// Raises the event checking first that an event if hooked up /// </summary> /// <param name="value">The value of the currently selected item</param> public void RaiseStatusChanged(String value) { // make sure we have a handler attached if (StateChanged != null) { //raise event StateChanged(this, new ChangingAwareEventArgs(value)); } } }

Listing 4 – Parent Change Notifying FieldTemplate

In Listing 4 the control that parent FieldTemplate will inherit so that they can generate events for the child control to subscribe to.

public class ChildChangingAwareFieldTemplate : FieldTemplateUserControl
{
    /// <summary>
    /// Gets or sets the parent column.
    /// </summary>
    /// <value>The parent column.</value>
    public MetaColumn ParentColumn { get; private set; }

    /// <summary>
    /// Gets or sets the parent control.
    /// </summary>
    /// <value>The parent control.</value>
    public IChangeNotifyingFieldTemplate ParentControl { get; set; }

    /// <summary>
    /// Raises the <see cref="E:System.Web.UI.Control.Init"/> event.
    /// </summary>
    /// <param name="e">An <see cref="T:System.EventArgs"/> object that contains the event data.</param>
    protected override void OnInit(EventArgs e)
    {
        // get the parent column
        var parentColumn = Column.GetAttributeOrDefault<ChangingAwareAttribute>().ParentColumn;
        if (!String.IsNullOrEmpty(parentColumn))
            ParentColumn = Column.Table.GetColumn(parentColumn) as MetaColumn;

        // get parent field (note you must specify the container control type in
        // DetailsView and FormView = CompositeDataBoundControl : DataBoundControl
        // ListView = DataBoundControl
        if (ParentColumn != null)
            ParentControl = GetParentControl();

        // finally call base
        base.OnInit(e);
    }

    /// <summary>
    /// Gets the Parent control in a cascade of controls
    /// </summary>
    /// <param name="column"></param>
    /// <returns></returns>
    private IChangeNotifyingFieldTemplate GetParentControl()
    {
        if (ParentColumn != null)
        {
            // get value of dev ddl (Community)
            var parentDataControl = this.GetContainerControl<DataBoundControl>();

            // Get Parent FieldTemplate
            var parentDynamicControl = parentDataControl
                .FindDynamicControlRecursive(ParentColumn.Name)
                as DynamicControl;

            // extract the parent control from the DynamicControl
            IChangeNotifyingFieldTemplate parentControl = null;
            if (parentDynamicControl != null)
                parentControl = parentDynamicControl.Controls[0] as IChangeNotifyingFieldTemplate;

            return parentControl;
        }
        return null;
    }
}

Listing 5 – Child Changing Aware FieldTemplate

And Listing 5 is the control that child FieldTemplates will inherit, so it can subscribe to events from the parent control. It contains the logic to find the parent control and a couple of properties to hold the parent column and controls in.

/// <summary>
/// Get the attribute or a default instance of the attribute
/// if the Column attribute do not contain the attribute
/// </summary>
/// <typeparam name="T">
/// Attribute type
/// </typeparam>
/// <param name="table">
/// Column to search for the attribute on.
/// </param>
/// <returns>
/// The found attribute or a default 
/// instance of the attribute of type T
/// </returns>
public static T GetAttributeOrDefault<T>(this MetaColumn column) where T : Attribute, new()
{
    return column.Attributes.OfType<T>().DefaultIfEmpty(new T()).FirstOrDefault();
}

Listing 6 – Get attribute extension method

The extension method in Listing 6 is there to simplify the code for getting an attribute which we do a lot in Dynamic Data.

/// <summary>
/// Get the DynamicControl by searching recursively for it by DataField.
/// </summary>
/// <param name="Root">The control to start the search at.</param>
/// <param name="Id">The DataField of the control to find</param>
/// <returns>The found control or NULL if not found</returns>
/// public static Control FindDynamicControlRecursive<T>(this Control root, string dataField) where T : Control
public static Control FindDynamicControlRecursive(this Control root, string dataField)
{
    var dc = root as DynamicControl; //Category
    if (dc != null)
    {
        if (String.Compare(dc.DataField, dataField, true) == 0)
            return dc;
    }

    foreach (Control Ctl in root.Controls)
    {
        Control FoundCtl = FindDynamicControlRecursive(Ctl, dataField);

        if (FoundCtl != null)
            return FoundCtl;
    }
    return null;
}

/// <summary>
/// Get the Data Control containing the FiledTemplate
/// usually a DetailsView or FormView
/// </summary>
/// <param name="control">
/// Use the current field template as a starting point
/// </param>
/// <returns>
/// A FilterRepeater the control that 
/// contains the current control
/// </returns>
public static T GetContainerControl<T>(this Control control) where T : Control
{
    var parentControl = control.Parent;
    while (parentControl != null)
    {
        var p = parentControl as T;
        if (p != null)
            return p;
        else
            parentControl = parentControl.Parent;
    }
    return null;
}

Listing 7 – A group of extension methods to get the parent control

Listing 7 is the two extension methods used by the child control to find the parent, by first using GetContainerControl to find the DetailsView, FormView or GridView etc.

!Important:

ALL previous Cascading examples have a minor flaw/bug/feature. The issue occurs when the parent control appears in the list of controls after the child control, which means in the controls OnInit event all following controls are not in the list. There are two options here

  1. Force the order of columns shown in the data control
  2. let each child control capture the OnDataBound event of the container DataControl and then find the parent control there, which may be too late to hookup the event
In this article we are going to use the first method and so I will introduce a field generator and an attribute to set the column order.
/// <summary>
/// Allows to specify the ordering of columns. Columns are will
///
be sorted in increasing order based on the Order value. Columns without /// this attribute have a default Order of 0. Negative values are /// allowed and can be used to place a column before all other columns. /// unashamedly nicked from the DD Futures project :D /// </summary> [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field,
Inherited = true,
AllowMultiple = false)] public class ColumnOrderAttribute : Attribute, IComparable { public static ColumnOrderAttribute Default = new ColumnOrderAttribute(0); public ColumnOrderAttribute(int order) { Order = order; } /// <summary> /// The ordering of a column. Can be negative. /// </summary> public int Order { get; private set; } public int ListOrder { get; set; } #region IComparable Members
public int CompareTo(object obj) { return Order - ((ColumnOrderAttribute)obj).Order; } #endregion } public static partial class HelperExtansionMethods { public static ColumnOrderAttribute GetColumnOrdering(this MetaColumn column) { return column.Attributes.OfType<ColumnOrderAttribute>()
.DefaultIfEmpty(ColumnOrderAttribute.Default).First(); } }

Listing 8 – Column Order attribute

/// <summary>
/// Implements the IAutoFieldGenerator interface and 
/// supports advanced scenarios such as declarative 
/// column ordering, workaround for attribute 
/// localization issues.
/// Again mostly swiped from DD Futures
/// </summary>
public class AdvancedFieldGenerator : IAutoFieldGenerator
{

    private MetaTable _table;
    private bool _multiItemMode;

    /// <summary>
    /// Allows to explicitly declare which columns should be skipped
    /// </summary>
    public List<MetaColumn> SkipList
    {
        get;
        set;
    }

    /// <summary>
    /// Creates a new AdvancedFieldGenerator.
    /// </summary>
    /// <param name="table">The table this class generates fields for.</param>
    /// <param name="multiItemMode"><value>true</value> to indicate a multi-item control such as GridView, <value>false</value> for a single-item control such as DetailsView.</param>
    public AdvancedFieldGenerator(MetaTable table, bool multiItemMode)
    {
        if (table == null)
        {
            throw new ArgumentNullException("table");
        }

        _table = table;
        _multiItemMode = multiItemMode;
        SkipList = new List<MetaColumn>();
    }

    private bool IncludeField(MetaColumn column)
    {
        // Skip columns that should not be scaffolded
        if (!column.GetScaffold())
            return false;

        // Don't display long strings in controls that show multiple items
        if (column.IsLongString && _multiItemMode)
            return false;

        // Skip columns that are on the skip list
        if (SkipList.Contains(column))
            return false;

        return true;
    }

    private ColumnOrderAttribute ColumnOrdering(MetaColumn column)
    {
        return column.Attributes.OfType<ColumnOrderAttribute>().DefaultIfEmpty(ColumnOrderAttribute.Default).First();
    }

    #region IAutoFieldGenerator Members

    public ICollection GenerateFields(Control control)
    {
        // Get all of table's columns, take only the ones that should be automatically included in a fields collection,
        // sort the result by the ColumnOrderAttribute, and for each column create a DynamicField
        var fields = from column in _table.Columns
                     where IncludeField(column)
                     orderby ColumnOrdering(column)
                     select new DynamicField()
                     {
                         DataField = column.Name,
                         HeaderText = column.DisplayName
                     };

        return fields.ToList();
    }

    #endregion
}

public static partial class HelperExtansionMethods
{
    /// <summary>
    /// Gets a value indicating if the column should be scaffolded. This honors the
    /// ScaffoldColumnAttribute as well as returns true if the column is an enumerated type.
    /// </summary>
    /// <param name="column"></param>
    /// <returns></returns>
    public static bool GetScaffold(this MetaColumn column)
    {
        // make sure we honor the ScaffoldColumnAttribute. The framework already does this
        // but we want to do this again as the first thing.
        var scaffoldAttribute = column.GetAttribute<ScaffoldColumnAttribute>();
        if (scaffoldAttribute != null)
            return scaffoldAttribute.Scaffold;

        // always return true for enumerated types
        return column.ColumnType.IsEnum || column.Scaffold;
    }
}

Listing 9 – The IAutoFieldGenerator

I have included Listing 8 & 9 for completeness they can both be found in the ASP.NET July 2007 Futures Source Code project on Codeplex and all I’m going to do is add [ColumnOrder(-1)] to the Discontinued column of the Products table (-1 is before zero and the default value is zero).

So now we are ready to setup some FieldTemplates to act as parents and children. Here we will create on parent control by modifying the default Boolean FieldTemplate Boolean_Edit.ascx.

Implementing the above classes in the FieldTemplates

Here we have a class for parent FieldTemplates to inherit and one for children, the parent exposes two properties and an event and the child class does the dirty business of finding the parent control.

Here we are going to use the Boolean_Edit.ascx for as out parent, you could use any theoretically but I thought Boolean made for a good sample.

#region Changing Aware code
/// <summary>
/// override the Value property and 
/// return the controls curretn state
/// </summary>
public override string State
{
    get
    {
        return CheckBox1.Checked.ToString();
    }
}

public MetaColumn ChildColumn { get { return Column; } }

protected void CheckBox1_CheckedChanged(object sender, EventArgs e)
{
    RaiseStatusChanged(this.CheckBox1.Checked.ToString());
}
#endregion

Listing 10 – code to add to the parent control (Boolean_Edit.ascx)

You just need to add the code from Listing 10 to the Boolean_Edit.ascx.cs file and then change the classes inheritance to ParentChangeNotifyingFieldTemplate now Boolean_Edit FieldTemplate is publishing its ChangingAware event.

#region Changing Aware Control
// added page init to hookup the event handler
protected override void OnDataBinding(EventArgs e)
{
    // get the parent column
    var parentColumn = Column.GetAttributeOrDefault<ChangingAwareAttribute>().ParentColumn;

    if (!String.IsNullOrEmpty(parentColumn))
    {
        //TODO: get the value from Row of the ParentColumn
        Object value = DataBinder.GetPropertyValue(Row, parentColumn);
        if (String.Compare(value.ToString(), "true", true) == 0)
            this.Visible = false;
        else
            this.Visible = true;
    }

    base.OnDataBinding(e);
}
#endregion

Listing 11 – this is the code for the Text.ascx.cs file

All you need to do is add the above code Listing 11 the to ReadOnly FieldTemplates that you want to hide in response to the parent in our case its just the Text.ascx.cs file.

#region Event
// added page init to hook-up the event handler
protected void Page_Init(object sender, EventArgs e)
{
    if (ParentColumn != null && ParentControl != null)
    {
        // regiter for the event
        ParentControl.StateChanged += StateChanged;
    }
}

// consume event
protected void StateChanged(object sender, ChangingAwareEventArgs e)
{
    if (ParentColumn != null && ParentControl != null)
    {
        // show or hide depending on current state of parent
        if (String.Compare(e.State, "true", true) == 0)
            this.Visible = false;
        else
            this.Visible = true;
    }
}

// added data binding to allow field to be hidden on load
protected override void OnDataBinding(EventArgs e)
{
    if (ParentControl != null)
        this.Visible = ParentControl.State == "True" ? false : true;
    base.OnDataBinding(e);
}

Listing 12 – this is the code for the Text_Edit.ascx and Integer_Edit.ascx files (UPDATED)

Listing 12 code is added to both the Text_Edit.ascx.cs and Integer_Edit.ascx.cs files

Updated: I’ve update the code in the OnDataBinding event handler to fix a bug during insert where there would be no value in the parent filed.
[MetadataType(typeof(ProductMD))]
public partial class Product
{
    public class ProductMD
    {
        public object ProductID {get;set;}
        public object ProductName {get;set;}
        public object SupplierID {get;set;}
        public object CategoryID {get;set;}

        [ChangingAware("Discontinued")]
        public object QuantityPerUnit {get;set;}

        public object UnitPrice {get;set;}
        public object UnitsInStock {get;set;}

        [ChangingAware("Discontinued")]
        public object UnitsOnOrder {get;set;}

        [ChangingAware("Discontinued")]
        public object ReorderLevel {get;set;}

        [ColumnOrder(-1)]
        public object Discontinued {get;set;}
        // EntitySet
        public object Order_Details {get;set;}
        // EntityRef
        public object Category {get;set;}
        public object Supplier {get;set;}

    }
}

Listing 13 – the Metadata

As you can see in Listing 13 of the metadata I’ve added the ChangeAware attribute to several columns these will be hidden id the row is discontinued. And to make sure that the children can see the parent when looking for it I’ve added a ColumnOrder attribute to the Discontinued column with a value of –1 for force it to be the first field in the row see Figure 1, 2 and 3 .

Figure 1 – As you can see the bottom row is discontinued and some field are hidden appropriately

Figure 1 – As you can see the bottom row is discontinued and some field are hidden appropriately.

 

Figure 2 - normal Figure 3 - Discontinued
Figure 2 - normal Figure 3 - Discontinued

 

So there we have it

Download (UPDATED)

Happy coding HappyWizard