Saturday 11 April 2009

Hiding Foreign Key column Globally in Dynamic Data

This article is based on a question in the Dynamic Data forum Hide Foreign Key Column. And so I thought I’d document what I did for posterity or at least so I can find it again if ever the question arises again :D

So I decided  here is what I would need:

  1. An Attribute to mark FK relation ships as hidden.
  2. Some Extension methods to extract and test the attribute
  3. An IAutoFieldGenerator to filter the Columns on a page

The Attribute

[AttributeUsage(AttributeTargets.Class)]
public class HideFKColumnAttribute : Attribute
{
    public Boolean Hidden { get; set; }
    public HideFKColumnAttribute()
    {
        Hidden = false;
    }

    public HideFKColumnAttribute(Boolean hide)
    {
        Hidden = hide;
    }
}

Listing 1 – HideFKColumnAttribute

This attribute will be set to true if the FK relationship is to be hidden and will default to false to make testing for easy.

The Extension Methods

/// <summary>
/// Test if this FK column should be hidden.
/// </summary>
/// <param name="column">
/// The column to test.
/// </param>
/// <returns>
/// Returns true if it the column should be hidden or false if not.
/// </returns>
public static Boolean FkIsHidden(this MetaColumn column)
{
    var fkColumn = column as MetaForeignKeyColumn;
    if (fkColumn != null)
        return fkColumn.ParentTable.GetAttributeOrDefault<HideFKColumnAttribute>().Hidden;
    else
        return false;
}

/// <summary>
/// Get the attribute or a default instance of the attribute
/// if the Table attribute do not contain the attribute
/// </summary>
/// <typeparam name="T">Attribute type</typeparam>
/// <param name="table">Table 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 MetaTable table) where T : Attribute, new()
{
    return table.Attributes.OfType<T>().DefaultIfEmpty(new T()).FirstOrDefault();
}

Listing 2 – Extension methods

FkIsHidden is used to test if the ParentTable has the HideFKColumnAttribute applied. And the extension method GetAttributeOrDefault returns an HideFKColumnAttribute which is false if not set but true if set explicitly.

The IAutoFieldGenerator class

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

public class HideColumnFieldsManager : IAutoFieldGenerator
{
    protected MetaTable _table;
    protected PageTemplate _currentPage;

    public HideColumnFieldsManager(MetaTable table, PageTemplate currentPage)
    {
        _table = table;
        _currentPage = currentPage;
    }

    public ICollection GenerateFields(Control control)
    {
        var 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 ||
                column.IsLongString ||
                column.FkIsHidden())
                continue;

            var f = new DynamicField();

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

Listing 3 – HideColumnFieldManager

This is the same IAutoFieldGenerator class as used in the article Dynamic Data - Hiding Columns in selected PageTemplates only I’ve added an column.FkIsHidden() to the list of tests for column to display.

I think that about wraps this up, you can download the file from the article mentioned in the paragraph above and then add the extension methods and attribute. Then modify the HideColumnFieldManager class to test for the Hidden FK relationship.

Download

Sunday 5 April 2009

What I’m reading.

Agile Principles, Patterns, and Practices in C# (Robert C. Martin)

Agile Principles, Patterns, and Practices in C# (Robert C. Martin)

I bought this after attending Claudio Lassala’s session DEV314 Beyond the Core Concepts of Object-Oriented Programming at the recent Tech.days 24 hour event, it was well worth staying up all night. After attending the above session on Tech.days I felt as if Claudio was saying all the things I had been groping for (I won’t say “striving for” because that suggests I had some idea of where I was going and didn’t), ways I was trying to work but not quite making it. Now I think I’ve started on a new way to code that truly will be more elegant.

 

ScreenShot287

jQuery in Action

Which I noticed many talking about and recommending, I think it was recommended on one of the Geek-Speak sessions. I do love the jQuery approach, the none invasive approach to coding JavaScript in pages seems to marry so well with MVC, which I am now taking a serious look at. Hopefully I’ll have some posts on jQuery and MVC soon.

Oh and I think Silverlight 3 is cool also.

Saturday 4 April 2009

Cascading Filters and Fields – Dynamic Data Entity Framework Version (UPDATED)

Well I wanted to do this in EF and Preview 3 at the same time but I having an issue with that so I’m combining both the Cascading Fields and Filters together and when the bugs are ironed out of the Preview I’ll do it there also.

Firstly the issue with the Preview.

  1. No filters support in the DefaultEFProject
  2. Errors when saving using the DefaultDomainServiceProject

Well I’m going to build a new Dynamic Data Entities Web Application for this project and create a separate project to keep the CascadeExtensions in.

I’ll add the projects zipped to the end of the article.

Lets create the class file first. (I’m assuming you know how to operate VS :D )

Create a new Class Library project Class Library project and delete the Class.cs file and give the project a namespace like DyanmicData.CascadeExtensions

ScreenShot285

Figure 1 – Adding Assembly name and Default namespace

using System;

namespace
DynamicData.CascadeExtensions { /// <summary> /// Attribute to identify which column to use as a /// parent column for the child column to depend upon /// </summary> public class CascadeAttribute : Attribute { /// <summary> /// Name of the parent column /// </summary> public String ParentColumn { get; private set; } /// <summary> /// Default Constructor sets ParentColumn /// to an empty string /// </summary> public CascadeAttribute() { ParentColumn = ""; } /// <summary> /// Constructor to use when /// setting up a cascade column /// </summary> /// <param name="parentColumn">Name of column to use in cascade</param> public CascadeAttribute(string parentColumn) { ParentColumn = parentColumn; } } }
Listing 1 - CascadeAttribute

You will need one of my extension methods to extract the attribute later on:

/// <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 2 – GetAttributeOrDefault extension method.

I have some more extension methods to add later that will be used by both the CascadeFieldTemplate and CascadeFilterTemplates.

using System;

namespace DynamicData.CascadeExtensions
{
    /// <summary>
    /// Event Arguments for Category Changed Event
    /// </summary>
    public class SelectionChangedEventArgs : EventArgs
    {
        /// <summary>
        /// Custom event arguments for SelectionChanged 
        /// event of the CascadingFieldTemplate control
        /// </summary>
        /// <param name="value">
        /// The value of the currently selected 
        /// value of the parent control
        /// </param>
        public SelectionChangedEventArgs(String value)
        {
            Value = value;
        }
        /// <summary>
        /// The values from the control of the parent control
        /// </summary>
        public String Value { get; set; }
    }
}

Listing 3 - SelectionChangedEventArgs

Next we create two new classes called CascadeFieldTemplate and CascadeFilterTemplate.

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

namespace DynamicData.CascadeExtensions
{
    /// <summary>
    /// Modifies the standard FieldTEmplateUserControl 
    /// to support cascading of selected values.
    /// </summary>
    public class CascadingFieldTemplate : FieldTemplateUserControl
    {
        /// <summary>
        /// Controls selected value
        /// </summary>
        public String SelectedValue { get; private set; }

        /// <summary>
        /// This controls list control 
        /// </summary>
        public ListControl ListControl { get; private set; }

        /// <summary>
        /// Parent column of this column named in metadata
        /// </summary>
        public MetaForeignKeyColumn ParentColumn { get; private set; }

        /// <summary>
        /// This FieldTemplates column as MetaForeignKeyColumn
        /// </summary>
        public MetaForeignKeyColumn ChildColumn { get; private set; }

        /// <summary>
        /// Parent control acquired from ParentColumn 
        /// </summary>
        public CascadingFieldTemplate ParentControl { get; set; }

        protected virtual void Page_Init(object sender, EventArgs e)
        {
            // get the parent column
            var parentColumn = Column.GetAttributeOrDefault<CascadeAttribute>().ParentColumn;
            if (!String.IsNullOrEmpty(parentColumn))
            {
                ParentColumn = Column.Table.GetColumn(parentColumn) as MetaForeignKeyColumn;
            }

            // cast Column as MetaForeignKeyColumn
            ChildColumn = Column as MetaForeignKeyColumn;


            // get parent field (note you must specify the
            // container control type in <DetailsView> or <FormView>
            ParentControl = GetParentControl();
        }

        /// <summary>
        /// Delegate for the Interface
        /// </summary>
        /// <param name="sender">
        /// A parent control also implementing the 
        /// ISelectionChangedEvent interface
        /// </param>
        /// <param name="e">
        /// An instance of the SelectionChangedEventArgs
        /// </param>
        public delegate void SelectionChangedEventHandler(
            object sender,
            SelectionChangedEventArgs e);

        //publish event
        public event SelectionChangedEventHandler SelectionChanged;

        /// <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 RaiseSelectedIndexChanged(String value)
        {
            // make sure we have a handler attached
            if (SelectionChanged != null)
            {
                //raise event
                SelectionChanged(this, new SelectionChangedEventArgs(value));
            }
        }

        // advanced populate list control
        protected void PopulateListControl(ListControl listControl, String filterValue)
        {
            //get the parent column
            if (ParentColumn == null)
            {
                // if no parent column then just call
                // the base to populate the control
                PopulateListControl(listControl);
                // make sure control is enabled
                listControl.Enabled = true;
            }
            else if (String.IsNullOrEmpty(filterValue))
            {
                // if there is a parent column but no filter value
                // then make sure control is empty and disabled
                listControl.Items.Clear();

                if (Mode == DataBoundControlMode.Insert || !Column.IsRequired)
                    listControl.Items.Add(new ListItem("[Not Set]", ""));

                // make sure control is disabled
                listControl.Enabled = false;
            }
            else
            {
                // get the child columns parent table
                var childTable = ChildColumn.ParentTable;

                // get query {Table(Developer).OrderBy(d => d.Name)}
                var query = ChildColumn.ParentTable.GetQuery(Column.Table.CreateContext());

                // get list of values filtered by the parent's selected entity
                var itemlist = query.GetQueryFilteredByParent(ParentColumn, filterValue);

                // clear list controls items collection before adding new items
                listControl.Items.Clear();

                // only add [Not Set] if in insert mode or column is not required
                if (Mode == DataBoundControlMode.Insert || !Column.IsRequired)
                    listControl.Items.Add(new ListItem("[Not Set]", ""));

                // add returned values to list control
                foreach (var row in itemlist)
                    listControl.Items.Add(
                        new ListItem(
                            childTable.GetDisplayString(row),
                            childTable.GetPrimaryKeyString(row)));

                // make sure control is enabled
                listControl.Enabled = true;
            }
        }

        /// <summary>
        /// Gets the Parent control in a cascade of controls
        /// </summary>
        /// <param name="column"></param>
        /// <returns></returns>
        private CascadingFieldTemplate GetParentControl()
        {
            // get value of dev ddl (Community)
            var parentDataControl = GetContainerControl();

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

                // extract the parent control from the DynamicControl
                CascadingFieldTemplate parentControl = null;
                if (parentDynamicControl != null)
                    parentControl = parentDynamicControl.Controls[0] 
as
CascadingFieldTemplate; return parentControl; } 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 CompositeDataBoundControl the base class for FormView and DetailsView /// </returns> private CompositeDataBoundControl GetContainerControl() { var parentControl = this.Parent; while (parentControl != null) { // NOTE: this will not work if used in // inline editing in a list view as // ListView is a DataBoundControl. var p = parentControl as CompositeDataBoundControl; if (p != null) return p; else parentControl = parentControl.Parent; } return null; } } }

Listing 4 – CascadeFieldTemplate

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

namespace DynamicData.CascadeExtensions
{
    /// <summary>
    /// Modifies the standard FieldTEmplateUserControl 
    /// to support cascading of selected values.
    /// </summary>
    public class CascadingFilterTemplate : FilterUserControlBase
    {
        #region Properties
        /// <summary>
        /// This controls list control 
        /// </summary>
        public ListControl ListControl { get; private set; }

        /// <summary>
        /// Paretn column of this column named in metadata
        /// </summary>
        public MetaForeignKeyColumn ParentColumn { get; private set; }

        /// <summary>
        /// This FieldTemplates column as MetaForeignKeyColumn
        /// </summary>
        public MetaForeignKeyColumn ChildColumn { get; private set; }

        /// <summary>
        /// Parent control acquired from ParentColumn 
        /// </summary>
        public CascadingFilterTemplate ParentControl { get; set; }
        #endregion

        //public override IQueryable GetQueryable(IQueryable source)
        //{
        //    return source;
        //}

        protected virtual void Page_Init(object sender, EventArgs e)
        {
            // get the parent column
            var parentColumn = Column.GetAttributeOrDefault<CascadeAttribute>().ParentColumn;
            if (!String.IsNullOrEmpty(parentColumn))
                ParentColumn = Column.Table.GetColumn(parentColumn) as MetaForeignKeyColumn;

            // cast Column as MetaForeignKeyColumn
            ChildColumn = Column as MetaForeignKeyColumn;

            // get dependee field (note you must specify the
            // container control type in <DetailsView> or <VormView>
            ParentControl = GetParentControl();
        }

        /// <summary>
        /// Delegate for the Interface
        /// </summary>
        /// <param name="sender">
        /// A parent control also implementing the 
        /// ISelectionChangedEvent interface
        /// </param>
        /// <param name="e">
        /// An instance of the SelectionChangedEventArgs
        /// </param>
        public delegate void SelectionChangedEventHandler(
            object sender,
            SelectionChangedEventArgs e);

        //publish event
        public event SelectionChangedEventHandler SelectionChanged;

        /// <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 RaiseSelectedIndexChanged(String value)
        {
            // make sure we have a handler attached
            if (SelectionChanged != null)
            {
                //raise event
                SelectionChanged(this, new SelectionChangedEventArgs(value));
            }
        }

        // advanced populate list control
        protected void PopulateListControl(ListControl listControl, String filterValue)
        {
            //get the parent column
            if (ParentColumn == null)
            {
                // if no parent column then just call
                // the base to populate the control
                PopulateListControl(listControl);
                // make sure control is enabled
                listControl.Enabled = true;
            }
            else if (String.IsNullOrEmpty(filterValue))
            {
                // if there is a parent column but no filter value
                // then make sure control is empty and disabled
                listControl.Items.Clear();

                listControl.Items.Add(new ListItem("[All]", ""));

                // make sure control is disabled
                listControl.Enabled = false;
            }
            else
            {
                // get the child columns parent table
                var childTable = ChildColumn.ParentTable;

                // get query {Table(Developer).OrderBy(d => d.Name)}
                var query = ChildColumn.ParentTable.GetQuery(Column.Table.CreateContext());

                // filter the query by the parent
                var itemlist = query.GetQueryFilteredByParent(ParentColumn, filterValue);

                // clear list controls items collection before adding new items
                listControl.Items.Clear();
                listControl.Items.Add(new ListItem("[All]", ""));

                // add returned values to list control
                foreach (var row in itemlist)
                    listControl.Items.Add(
                        new ListItem(
                            childTable.GetDisplayString(row),
                            childTable.GetPrimaryKeyString(row)));

                // make sure control is enabled
                listControl.Enabled = true;
            }
        }

        /// <summary>
        /// Gets the Parent control in a cascade of controls
        /// </summary>
        /// <returns>An the parent control or null</returns>
        private CascadingFilterTemplate GetParentControl()
        {
            if (ParentColumn != null)
            {
                // get the parent container
                var parentDataControl = GetContainerControl();

                // get the parent container
                if (parentDataControl != null)
                    return parentDataControl.FindFilterControlRecursive(ParentColumn.Name)
                        as CascadingFilterTemplate;
            }
            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>
        private FilterRepeater GetContainerControl()
        {
            var parentControl = this.Parent;
            while (parentControl != null)
            {
                var p = parentControl as FilterRepeater;
                if (p != null)
                    return p;
                else
                    parentControl = parentControl.Parent;
            }
            return null;
        }
    }
}

Listing 5 – CadcadingFilterTemplate

You may note that I have removed GetQueryFilteredByParent and some other local methods from both CascadeFieldTemplate and CadcadingFilterTemplate, they will be placed in the extension methods class file later.

Now we come the the differences between the Linq to SQL implementation and this the Entity Framework implementation.

The issue I had when trying to make this work with EF was that

private IQueryable GetQueryFilteredByParent
    (MetaTable childTable,
    MetaForeignKeyColumn parentColumn,
    object selectedParent)
{
    // get query {Table(Developer)}
    var query = ChildColumn.ParentTable.GetQuery(DC);

    // {Developers}
    var parameter = Expression.Parameter(childTable.EntityType, childTable.Name);

    // {Developers.Builder}
    var property = Expression.Property(parameter, parentColumn.Name);

    // {value(Builder)}
    var constant = Expression.Constant(selectedParent);

    // {(Developers.Builder = value(Builder))}
    var predicate = Expression.Equal(property, constant);

    // {Developers => (Developers.Builder = value(Builder))}
    var lambda = Expression.Lambda(predicate, parameter);

    // {Table(Developer).Where(Developers => (Developers.Builder = value(Builder)))}
    var whereCall = Expression.Call(typeof(Queryable), 
        "Where", 
        new Type[] { childTable.EntityType }, 
        query.Expression, 
        lambda);

    // generate the query and return it
    return query.Provider.CreateQuery(whereCall);
}

Listing 6 – Old GetQueryFilteredByParent method.

In here I passed in the selectedParent which contained the entity I was filtering on anyway what I found was the EF did not like that at all it said basically I want a simple value like int, String, double etc.

This is the expression I was faced with:

Table(Developer).Where(Developers => (Developers.Builder = value(Builder)))

here I was leaving the join up to L2S but EF wanted me to be more specific

Table(Developer).Where(Developers => (Developers.BuilderId = 2))

But I knew that wouldn't work because EF does not have the FK fields in it’s entities, well not if it can help it :) so I surmised that it would want something like this:

Table(Developer).Where(Developers => (Developers.Builder.Id = 2))

Where Id is the PK of the Builder entity.

And then I was lucky enough to be trying this with Preview 3 and the DefaultDomainServiceProject which if you have a look at the ForeignKey filter you will see some nice Expression creating code:

private Expression BuildQueryBody(
    ParameterExpression parameterExpression, 
    string selectedValue)
{
    IDictionary dict = new Hashtable();
    Column.ExtractForeignKey(dict, selectedValue);

    int i = 0;
    ArrayList andFragments = new ArrayList();
    foreach (DictionaryEntry entry in dict)
    {
        string fieldName = Column.ParentTable.Name + "." 
            + Column.ParentTable.PrimaryKeyColumns[i++].Name;

        Expression propertyExpression = 
            CreatePropertyExpression(parameterExpression, fieldName);

        object value = ChangeType(entry.Value, propertyExpression.Type);
        Expression equalsExpression = Expression.Equal(
            propertyExpression, 
            Expression.Constant(value, propertyExpression.Type));

        andFragments.Add(equalsExpression);
    }

    Expression result = null;
    foreach (Expression e in andFragments)
    {
        if (result == null)
        {
            result = e;
        }
        else
        {
            result = Expression.AndAlso(result, e);
        }
    }
    return result;
}

Listing 7 – BuildQuery from the ForeignKey filter.

This helps build something like this (item.Categories.CategoryID = 1) which eventually produces this where expression and it would deal with composite keys.

Where(item => (item.Categories.CategoryID = 2))

So I swiped that from the ForeignKey filter and added it to my Extension methods, and built the missing bits from what I could glean with Reflector.

So we have the following in the extension methods class

#region IQueryable methods
/// <summary>
/// Gets a list of entities from the source IQueryable 
/// filtered by the MetaForeignKeyColumn's selected value
/// </summary>
/// <param name="sourceQuery">The query to filter</param>
/// <param name="fkColumn">The column to filter the query on</param>
/// <param name="fkSelectedValue">The value to filter the query by</param>
/// <returns>
/// An IQueryable of the based on the source query 
/// filtered but the FK column and value passed in.
/// </returns>
public static IQueryable GetQueryFilteredByParent(this IQueryable sourceQuery, MetaForeignKeyColumn fkColumn, String fkSelectedValue)
{
    // if no filter value return the query
    if (String.IsNullOrEmpty(fkSelectedValue))
        return sourceQuery;

    // {RequiredPlots}
    var parameterExpression = Expression.Parameter(sourceQuery.ElementType, fkColumn.Table.Name);

    // {(RequiredPlots.Builders.Id = 1)}
    var body = BuildWhereClause(fkColumn, parameterExpression, fkSelectedValue);

    // {RequiredPlots => (RequiredPlots.Builders.Id = 1)}
    var lambda = Expression.Lambda(body, parameterExpression);

    // Developers.Where(RequiredPlots => (RequiredPlots.Builders.Id = 1))}
    MethodCallExpression whereCall = Expression.Call(typeof(Queryable),
        "Where", new Type[] { sourceQuery.ElementType },
        sourceQuery.Expression,
        Expression.Quote(lambda));

    // create and return query
    return sourceQuery.Provider.CreateQuery(whereCall);
}

/// <summary>
/// This builds the and where clause taking
/// into account composite keys
/// </summary>
/// <param name="fkColumn">The column to filter the query on</param>
/// <param name="fkSelectedValue">The value to filter the query by</param>
/// <param name="parameterExpression">Parameter expression</param>
/// <returns>
/// Returns the expression for the where clause 
/// i.e. ((x = 1) && (Y = 2)) or (x = 1) etc.
/// </returns>
private static Expression BuildWhereClause(
    MetaForeignKeyColumn fkColumn, 
    ParameterExpression parameterExpression, 
    string fkSelectedValue)
{
    // get the FK's and value into dictionary
    IDictionary dict = new OrderedDictionary();
    fkColumn.ExtractForeignKey(dict, fkSelectedValue);

    // setup index into dictionary
    int i = 0;

    // setup array list to hold each AND fragment
    ArrayList andFragments = new ArrayList();
    foreach (DictionaryEntry entry in dict)
    {
        // get fk name 'Builders.Id'
        string keyName = fkColumn.Name
            + "." + fkColumn.ParentTable.PrimaryKeyColumns[i++].Name;

        // Build property expression 
        // i.e. {RequiredPlots.Builders.Id}
        Expression propertyExpression 
            = BuildPropertyExpression(parameterExpression, keyName);

        // sets the type based on the propertyExpression's type
        // i.e. all the values returned from the DDL are of type string
        // so the type on the expression needs setting to the correct type
        object value = ChangeType(entry.Value, propertyExpression.Type);

        // join the property expression and value in an
        // equals expression i.e. (RequiredPlots.Builders.Id = 1)
        Expression equalsExpression 
            = Expression.Equal(propertyExpression, 
            Expression.Constant(value, propertyExpression.Type));

        // add a fragment to array list
        andFragments.Add(equalsExpression);
    }

    // initialise result
    Expression result = null;
    // join add fragments of composite keys 
    // together together
    foreach (Expression e in andFragments)
    {
        if (result == null)
            result = e;
        else
            result = Expression.AndAlso(result, e);
    }
    // joined fragments look something like:
    // (RequiredPlots.Developer.Id = 1) && (RequiredPlots.HouseType.Id = 1)
    return result;
}

/// <summary>
/// Builds a property expression from the parts it joins
/// the parameterExpression and the propertyName together.
/// i.e. {RequiredPlots}  and "Builders.Id"
/// becomes: {RequiredPlots.Developers.Id}
/// </summary>
/// <param name="parameterExpression">
/// The parameter expression.
/// </param>
/// <param name="propertyName">
/// Name of the property.
/// </param>
/// <returns>
/// A property expression
/// </returns>
public static Expression BuildPropertyExpression(
    Expression parameterExpression, 
    string propertyName)
{
    Expression expression = null;
    // split the propertyName into each part to 
    // be build into a property expression
    string[] strArray = propertyName.Split(new char[] { '.' });
    foreach (string str in strArray)
    {
        if (expression == null)
            expression 
                = Expression.PropertyOrField(parameterExpression, str);
        else
            expression 
                = Expression.PropertyOrField(expression, str);
    }
    // {RequiredPlots.Developer.Id}
    return expression;
}

/// <summary>
/// Changes the type.
/// </summary>
/// <param name="value">The value to convert.</param>
/// <param name="type">The type to convert to.</param>
/// <returns>The value converted to the type.</returns>
public static object ChangeType(object value, Type type)
{
    // if type is null throw exception can't
    // carry on nothing to convert to.
    if (type == null)
        throw new ArgumentNullException("type");

    if (value == null)
    {
        // test for nullable type 
        // (i.e. if Nullable.GetUnderlyingType(type)
        // is not null then it is a nullable type 
        // OR if it is a reference type
        if ((Nullable.GetUnderlyingType(type) != null) 
            || !type.IsValueType)
            return null;
        else // for 'not nullable value types' return the default value.
            return Convert.ChangeType(value, type);
    }

    // ==== Here we are guaranteed to have a type and value ====

    // get the type either the underlying type or 
    // the type if there is no underlying type.
    type = Nullable.GetUnderlyingType(type) ?? type;

    // Convert using the type
    TypeConverter converter 
        = TypeDescriptor.GetConverter(type);
    if (converter.CanConvertFrom(value.GetType()))
    {
        // return the converted value
        return converter.ConvertFrom(value);
    }

    // Convert using the values type
    TypeConverter converter2 
        = TypeDescriptor.GetConverter(value.GetType());
    if (!converter2.CanConvertTo(type))
    {
        // if the type cannot be converted throw an error
        throw new InvalidOperationException(
            String.Format("Unable to convert type '{0}' to '{1}'", 
            new object[] { value.GetType(), type }));
    }
    // return the converted value
    return converter2.ConvertTo(value, type);
}
#endregion

Listing 8 – IQueryable extension methods

Updated: 

In the BuildWhereClause method in Listing 8 I have made a minor change that resolves a major bug:

    string keyName = fkColumn.ParentTable.Name + "." + fkColumn.ParentTable.PrimaryKeyColumns[i++].Name;

has been changed to:

         string keyName = fkColumn.Name + "." + fkColumn.ParentTable.PrimaryKeyColumns[i++].Name;

The issue here was that you would get {RequiredPlots.Developers.Id} instead of {RequiredPlots.Developer.Id} (note the plural Developers) this was fine in Entity Framework where the entity was left as it cane out of the DB but no good for Linq to SQL which uses pluralisation.

So there you have it, there’s a lot more we could do with this to streamline the code make some of the extension methods more generic etc but I think I will leave it there.

Download (UPDATED)

The download is a Web Application Project for EF bit the CascadeExtensions classes, FieldTemplate and FilterUserControl are compatible with EF and L2S.

Note: Also included are the script to create the DB and some data to import in excel format.
Updated: I’ve now added filter ordering via an extension to the FilterRepeater called SortedFilterRepeater and am mapping it in web.config, also added a general sort vi IAutoFieldGenerator on all pages

Thursday 2 April 2009

Disallow Navigation on ForeignKey FieldTemplate – Dynamic Data (UPDATED)

This article originates from a post I ding on the ASP.NET Dynamic Data Forum here Re: A few problems with my dynamic data website, Rick Anderson suggesting that it would be a useful post to link to in his FAQ. So here it is for easy access rather than having to search the forum for it.

HappyWizard

Firstly the idea is that you want to show the name of the entity but not link to if from the ForeignKey FieldTemplate.

Category has Navigation disabled

Figure 1 - Category has Navigation disabled

The code we need for this comes in three parts

  1. The Attribute
  2. The modification the the FieldTemplate
  3. The Metadata.

The Attribute

C#
[AttributeUsage(AttributeTargets.Property)] public class DisallowNavigationAttribute : Attribute { public Boolean Hide { get; private set; } /// <summary> /// Initializes a new instance of the <see cref="DisallowNavigationAttribute"/> class. /// </summary> public DisallowNavigationAttribute() { Hide = false; } public DisallowNavigationAttribute(Boolean show) { Hide = show; } }
VB
<AttributeUsage(AttributeTargets.[Property] Or AttributeTargets.[Field])> _ Public Class DisallowNavigationAttribute Inherits Attribute Private _hide As [Boolean] Public Property Hide() As [Boolean] Get Return _hide End Get Private Set(ByVal value As [Boolean]) _hide = value End Set End Property Public Sub New(ByVal hide As [Boolean]) Me.Hide = hide End Sub Public Sub New() Me.Hide = False End Sub End Class

Listing 1 – AllowNavigation attribute

Note the use of the Default see Writing Attributes and Extension Methods for Dynamic Data for the details on why.

C#
/// <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(); }
VB
Imports
System.Runtime.CompilerServices Imports System.Web.DynamicData Public Module ExtensionMethods ''' <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="column">Column to search for the attribute on.</param> ''' <returns>The found attribute or a default instance of the attribute of type T</returns> ''' <remarks>Used to simplify getting Attributes from metadata</remarks> <Extension()> _ Public Function GetAttributeOrDefault(Of T As {Attribute, New})(ByVal column As MetaColumn) As T Return column.Attributes.OfType(Of T)().DefaultIfEmpty(New T()).FirstOrDefault() End Function End Module

Listing 2 -  Extension method to get attribute

The modification the the FieldTemplate

Now we need to add a little change to the ForeignKey.aspx.cs file which can be found in the ~/DynamicData/FieldTemplates folder of your Dynamic Data site.

C#
protected string
GetNavigateUrl() { var dissallow = Column.GetAttributeOrDefault<DisallowNavigationAttribute>(); if (!AllowNavigation || dissallow.Hide) { // remove undeline :D HyperLink1.Style.Add(HtmlTextWriterStyle.TextDecoration, "none !important"); return null; } if (String.IsNullOrEmpty(NavigateUrl)) { return ForeignKeyPath; } else { return BuildForeignKeyPath(NavigateUrl); } }
VB
Protected Function
GetNavigateUrl() As String Dim dissallow = Column.GetAttributeOrDefault(Of DisallowNavigationAttribute)() If Not AllowNavigation OrElse dissallow.Hide Then Return Nothing End If If String.IsNullOrEmpty(NavigateUrl) Then Return ForeignKeyPath Else Return BuildForeignKeyPath(NavigateUrl) End If End Function

Listing 3 – ForeignKey FieldTemplate modifications

Updated: With the addition of the inline style the underline will not show, of course you could always just set the CssClass of the Hyperlink to a style with textdecoration: none !important; if you wanted.

All we’ve done here is get the attribute into the allow variable and the add it to the checks in the if statement so that:

  • If the internal AllowNavigation is set then null is returned.
  • If the custom attribute is set null is returned.

The Metadata

C#
[MetadataTypeAttribute(typeof(Products.ProductsMetadata))] public partial class Products { internal sealed class ProductsMetadata { public object ProductID { get; set; } public object ProductName { get; set; } public object QuantityPerUnit { get; set; } public object UnitPrice { get; set; } public object UnitsInStock { get; set; } public object UnitsOnOrder { get; set; } public object ReorderLevel { get; set; } public object Discontinued { get; set; } public object Order_Details { get; set; } [DisallowNavigation(true)] public object Categories { get; set; } [DisallowNavigation(true)] public object Suppliers { get; set; } } }
VB
Imports
Microsoft.VisualBasic Imports System.Web.DynamicData Imports System.ComponentModel Imports System.ComponentModel.DataAnnotations <MetadataType(GetType([Product].[ProductMD]))> _ Partial Public Class [Product] Partial Public Class [ProductMD] Public ProductID As Object Public ProductName As Object Public SupplierID As Object Public CategoryID As Object Public QuantityPerUnit As Object Public UnitPrice As Object Public UnitsInStock As Object Public UnitsOnOrder As Object Public ReorderLevel As Object Public Discontinued As Object Public Order_Details As Object <DisallowNavigationAttribute(True)> _ Public Category As Object Public Supplier As Object End Class End Class

Listing 4 – sample Metadata

Now if you attribute a foreign key column up with the AllowNavigationAttribute you can turn off the hyperlink.

Happy Coding and remember HappyWizard