Sunday, 13 June 2010

Securing Dynamic Data 4 (Replay)

This is an updated version of the series Securing Dynamic Data Preview 4 from July 2009 here I playnto streamline the class libraries for the RTM version of Dynamic Data 4  and Visual Studio 2010.

This version is mostly the same as in Part 1 except I’ve done a great deal of refactoring and so I will list everything again here. The main difference is that there are now no user controls to replace the Delete buttons. Also I have changed the permissions system to be restrictive by default at Table level i.e. you must have a permission set on every table for the table to be seen, but a Column level you deny columns you don’t want to be seen.

Permissions Enums

The TableActions (renamed from TableDeny) enum Listing 1 has had a CombinedActions class Listing 2 added that combine sets of TableActions into logical security groups (i.e. ReadOnly equates to combining TablesActions Details and List to give an more descriptive was of assigning rights to a security Role).

/// <summary>
/// Table permissions enum, allows different
/// levels of permission to be set for each 
/// table on a per role bassis.
/// </summary>
[Flags]
public enum TableActions
{
    /// <summary>
    /// Default no permissions
    /// </summary>
    None = 0x00,
    /// <summary>
    /// Details page
    /// </summary>
    Details = 0x01,
    /// <summary>
    /// List page
    /// </summary>
    List = 0x02,
    /// <summary>
    /// Edit page
    /// </summary>
    Edit = 0x04,
    /// <summary>
    /// Insert page
    /// </summary>
    Insert = 0x08,
    /// <summary>
    /// Delete operations
    /// </summary>
    Delete = 0x10,
}

Listing 1 – TableActions

/// <summary>
/// Combines Table permissions enums
/// into logical security groups
/// i.e. ReadOnly combines TableActions
/// Details and List
/// </summary>
public static class CombinedActions
{
    /// <summary>
    /// Read Only access 
    /// TableActions.Details or 
    /// TableActions.List
    /// </summary>
    public const TableActions ReadOnly = 
        TableActions.Details | 
        TableActions.List;
    /// <summary>
    /// Read and Write access 
    /// TableActions.Details or 
    /// TableActions.List or
    /// TableActions.Edit
    /// </summary>
    public const TableActions ReadWrite = 
        TableActions.Details | 
        TableActions.List | 
        TableActions.Edit;
    /// <summary>
    /// Read Insert access 
    /// TableActions.Details or 
    /// TableActions.List or 
    /// TableActions.Insert
    /// 
    /// </summary>
    public const TableActions ReadInsert = 
        TableActions.Details | 
        TableActions.List | 
        TableActions.Insert;
    /// <summary>
    /// Read Insert and Delete access 
    /// TableActions.Details or 
    /// TableActions.List or 
    /// TableActions.Insert or 
    /// TableActions.Delete)
    /// </summary>
    public const TableActions ReadInsertDelete = 
        TableActions.Details | 
        TableActions.List | 
        TableActions.Insert | 
        TableActions.Delete;
    /// <summary>
    /// Read and Write access 
    /// TableActions.Details or 
    /// TableActions.List or 
    /// TableActions.Edit or 
    /// TableActions.Insert)
    /// </summary>
    public const TableActions ReadWriteInsert = 
        TableActions.Details | 
        TableActions.List | 
        TableActions.Edit | 
        TableActions.Insert;
    /// <summary>
    /// Full access 
    /// TableActions.Delete or
    /// TableActions.Details or 
    /// TableActions.Edit or 
    /// TableActions.Insert or 
    /// TableActions.List)
    /// </summary>
    public const TableActions Full = 
        TableActions.Delete | 
        TableActions.Details | 
        TableActions.Edit | 
        TableActions.Insert | 
        TableActions.List;
}

Listing 2 – CombinedActions

ColumnActions Listing 3  are used to deny either Write or Read access.

/// <summary>
/// Actions a Column can 
/// have assigned to itself.
/// </summary>
[Flags]
public enum ColumnActions
{
    /// <summary>
    /// Action on a column/property
    /// </summary>
    DenyRead = 1,
    /// <summary>
    /// Action on a column/property
    /// </summary>
    DenyWrite = 2,
}

Listing 3 – ColumnActions

Secure Dynamic Data Route Handler

The SecureDynamicDataRouteHandler has changed very little since the original article all I have added is the catch all tp.Permission == CombinedActions.Full in the if statement to streamline the code.

/// <summary>
/// The SecureDynamicDataRouteHandler enables the 
/// user to access a table based on the following:
/// the Roles and TableDeny values assigned to 
/// the SecureTableAttribute.
/// </summary>
public class SecureDynamicDataRouteHandler : DynamicDataRouteHandler
{
    /// <summary>
    /// Creates the handler.
    /// </summary>
    /// <param name="route">The route.</param>
    /// <param name="table">The table.</param>
    /// <param name="action">The action.</param>
    /// <returns>An IHttpHandler</returns>
    public override IHttpHandler CreateHandler(
        DynamicDataRoute route,
        MetaTable table,
        string action)
    {
        var httpContext = HttpContext.Current;
        if (httpContext != null && httpContext.User != null)
        {
            var usersRoles = Roles.GetRolesForUser(httpContext.User.Identity.Name);
            var tablePermissions = table.Attributes.OfType<SecureTableAttribute>();

            // if no permission exist then full access is granted
            if (tablePermissions.Count() == 0)
                return null;

            foreach (var tp in tablePermissions)
            {
                if (tp.HasAnyRole(usersRoles))
                {
                    // if no action is allowed return no route
                    var tpAction = tp.Permission.ToString().Split(new char[] { ',', ' ' }, 
                        StringSplitOptions.RemoveEmptyEntries);

                    if (tp.Permission == CombinedActions.Full || tpAction.Contains(action))
                        return base.CreateHandler(route, table, action);
                }
            }
        }
        return null;
    }
}

Listing 4 – Secure Dynamic Data Route Handler

This then covers all Edit, Insert and Details actions but not Delete.

Delete Actions

In the previous article we had a User Control that handled securing the Delete action, here we have a SecureLinkButton. All we do is override the Render method and test to see if the button is disabled via the users security roles.

/// <summary>
/// Secures the link button when used for delete actions
/// </summary>
public class SecureLinkButton : LinkButton
{
    private const String DISABLED_NAMES = "SecureLinkButtonDeleteCommandNames";
    private String[] delete = new String[] { "delete" };

    /// <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)
    {
        if (ConfigurationManager.AppSettings.AllKeys.Contains(DISABLED_NAMES))
            delete = ConfigurationManager.AppSettings[DISABLED_NAMES]
                .ToLower()
                .Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);

        base.OnInit(e);
    }

    /// <summary>
    /// Renders the control to the specified HTML writer.
    /// </summary>
    /// <param name="writer">
    /// The <see cref="T:System.Web.UI.HtmlTextWriter"/> 
    /// object that receives the control content.
    /// </param>
    protected override void Render(HtmlTextWriter writer)
    {
        if (!IsDisabled())
            base.Render(writer);
        else
            writer.Write(String.Format("<a>{0}</a>", Text));
    }

    /// <summary>
    /// Determines whether this instance is disabled.
    /// </summary>
    /// <returns>
    /// 	<c>true</c> if this instance is 
    /// 	disabled; otherwise, <c>false</c>.
    /// </returns>
    private Boolean IsDisabled()
    {
        if (!delete.Contains(CommandName.ToLower()))
            return false;

        // get restrictions for the current
        // users access to this table
        var table = DynamicDataRouteHandler.GetRequestMetaTable(Context);
        var usersRoles = Roles.GetRolesForUser();
        var tableRestrictions = table.Attributes.OfType<SecureTableAttribute>();

        // restrictive permissions
        if (tableRestrictions.Count() == 0)
            return true;

        foreach (var tp in tableRestrictions)
        {
            // the LinkButton is considered disabled if delete is denied.
            var action = CommandName.ToEnum<TableActions>();
            if (tp.HasAnyRole(usersRoles) && (tp.Actions & action) == action)
                return false;
        }
        return true;
    }
}

Listing 5 – Secure Link Button

In more detail the IsDisabled method check to see if the LinkButtons CommandName is the same as the the “SecureLinkButtonDeleteCommandNames” application setting set in the web.config, note the default is “delete”. And then if the user does not have Delete permission then the button is disabled.

So how do we use this SecureLinkButton we add a tagMapping in the web.config file see Listing 6.

<configuration>
    <system.web>
        <pages>
            <controls>
                <!-- custom tag assignments -->
                <add tagPrefix="asp" namespace="NotAClue.Web.DynamicData" 
                    assembly="NotAClue.Web.DynamicData" />
            </controls>
            <!-- custom tag mappings -->
            <tagMapping>
                <add tagType="System.Web.UI.WebControls.LinkButton"
                    mappedTagType="NotAClue.Web.DynamicData.SecureLinkButton" />
            </tagMapping>
        </pages>
    </system.web>
</configuration>

Listing 6 – Tag Mapping in web.config

This means that our SecureLinkButton will replace the LinkButton throughout the site, however if you do not like this you can just rename each delete <asp:LinkButton to <asp:SecureLinkButton and then you will get the same effect and not add the tagMapping section to the web.config.

The Secure Meta Model

Here the two main parts are the SecureMetaTable and the three MetaColumn types (SecureMetaColumn, SecureMetaForeignKeyColumn and SecureMetaChildrenColumn)

SecureMetaTable

In the SecureMetaTable we override the GetScaffoldColumns method and filter the column list to where columns do not have a DenyRead action applied for any of the current users security roles.

SecureMetaColumn, SecureMetaForeignKeyColumn and SecureMetaChildrenColumn

With these types we do have to repeat ourselves a little as we override the IsReadOnly property to check to see if the column has a DenyWrite action applied for one of the users roles.
Note: Thanks to the ASP.NET team for listening and making this property virtual.

There is one issue I found and that is the default FieldTemplateFactory caches the DynamicControl model (ReadOnly, Edit and Insert) I did toy with adding the relevant code the default EntityTemplates see Listing 7, but decided again it.

protected void DynamicControl_Init(object sender, EventArgs e)
{
    DynamicControl dynamicControl = (DynamicControl)sender;
    dynamicControl.DataField = currentColumn.Name;

    // test for read-only column
    if (currentColumn.IsReadOnly)
        dynamicControl.Mode = DataBoundControlMode.ReadOnly;
}


Listing 7 – adding control mode code to the default EntityTemplates

Instead I decided to use a custom FieldTemplateFactory see Listing 8

public class SecureFieldTemplateFactory : FieldTemplateFactory
{
    public override IFieldTemplate CreateFieldTemplate(MetaColumn column,
        DataBoundControlMode mode, 
        string uiHint)
    {
        // code to fix caching issue
        if (column.IsReadOnly)
            mode = DataBoundControlMode.ReadOnly;

        return base.CreateFieldTemplate(column, mode, uiHint);
    }
}

Listing 8 – Secure Field Template Factory

The code here is simple we just check to see if the column is read-only (remembering that the SecureMetaColumns are already checking this for us) then set the Mode to DataBoundControlMode.ReadOnly. This nicely keeps our code DRY.

Secure Table and Column Attributes

These are essentially unchanged from the previous series of articles with just a little refactoring to make the code more readable.

!Important: For code see sample at end of article.

Putting It Together

Nearly all the work to get Secure Dynamic Data working is done simply in the Global.asax file.

Note: There are some changes you need to make to add Login etc but that is standard ASP.Net and specific to Dynamic Data.

Adding Security to Dynamic Data

Figure 1 – Adding Security to Dynamic Data

Also you need the tag mapping from Listing 6, there are some more bits we need to do but they are standard ASP.Net Security, so let’s get that done next.

!Important: To use this as we currently are you will need SQL Server 200x Express installed otherwise you will need to add a specific connection string and use Creating the Application Services Database for SQL Server to make your ASPNETDB database for membership and roles.
<authentication mode="Forms">
    <forms loginUrl="~/Login.aspx" protection="All" defaultUrl="~/Default.aspx" path="/"/>
</authentication>
<authorization>
    <deny users="?"/>
</authorization>
<membership>
    <providers>
        <remove name="AspNetSqlMembershipProvider"/>
        <add name="AspNetSqlMembershipProvider"
            type="System.Web.Security.SqlMembershipProvider, System.Web, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
            connectionStringName="LocalSqlServer"
            enablePasswordRetrieval="false"
            enablePasswordReset="true"
            requiresQuestionAndAnswer="true"
            applicationName="/"
            requiresUniqueEmail="false"
            passwordFormat="Hashed"
            maxInvalidPasswordAttempts="5"
            minRequiredPasswordLength="7"
            minRequiredNonalphanumericCharacters="0"
            passwordAttemptWindow="10"
            passwordStrengthRegularExpression=""/>
    </providers>
</membership>
<roleManager enabled="true" />

Listing 9 – Adding standard ASP.Net security to web.config

<location path="Site.css">
    <system.web>
        <authorization>
            <allow users="*"/>
        </authorization>
    </system.web>
</location>

Listing 10 – Allowing access to style sheet.

With SQL Server 200x Express edition installed you will get the ASPNETDB created automatically.

Note: I generally do this to create the ASPNETDB then move it to where I want it and setup a specific connection string. Also you can use the ASP.Net Configuration utility to create users and roles.
ASP.Net Configuration Utility
Figure 1 - ASP.Net Configuration Utility

Downloads

I think that is about it, so here is the download it contains three projects the Class Library and two sample projects one Entity Framework and one Linq to SQL. Have fun.

Sunday, 6 June 2010

Part 3 – A Cascading Hierarchical Field Template & Filter for Dynamic Data

This would have been the final part of the series of articles, however I realised that we will need a final article that covers the more complex action of the CascadingHierarchicalFilter, this where at each level of selection we filter the list.

Cascading Hierarchical Filter

Figure 1 – Cascading Hierarchical Filter.

e.g. in Figure 1 the CascadingHierarchicalFilter (the basic version) no selection occurs until we select a value from the final list control, however in the complex CascadingHierarchicalFilter we will filter at each level and so we will hold that over until we have completed the basic version of the CascadingHierarchicalFilter.

And so to the basic version, first we will need a starting place so copy the ForeignKey filter and rename it to CascadingHierarchical and the class name to CascadingHierarchicalFilter. Next we remove the content of the aspx page so it looks like Listing 1.

<%@ Control 
    Language="C#" 
    CodeBehind="CascadeHierarchical.ascx.cs" 
    Inherits="CascadeHierarchicalFieldTemplate.CascadeHierarchicalFilter" %>

Listing 1 – CascadingHierarchical.aspx page

Then we start on the code behind by adding our member variables, adjusting the two properties and replacing the default Page_Init with ours, which is very similar to the Edit templates Page_Init see Listing 2.

#region member variables
private const string NullValueString = "[null]";
// hold the current data context to access the model
private object context;
// hold the list of filters
private SortedList<int, HierachicalListControl> filters = new SortedList<int, HierachicalListControl>();
// hold the attribute
private CascadeHierarchicalAttribute cascadeHierarchicalAttribute;
#endregion

private new MetaForeignKeyColumn Column
{
    get { return (MetaForeignKeyColumn)base.Column; }
}

public override Control FilterControl
{
    get
    {
        if (filters[0] != null && filters[0].ListControl != null)
            return filters[0].ListControl;
        else
            return null;
    }
}

protected void Page_Init(object sender, EventArgs e)
{
    //if (!Column.IsRequired)
    // check we have a cascade hierarchical attribute if not throw error
    cascadeHierarchicalAttribute = Column.GetAttribute<CascadeHierarchicalAttribute>();
    if (cascadeHierarchicalAttribute == null)
        throw new InvalidOperationException("Was expecting a CascadeFilterAttribute.");

    // check we have correct column type if not throw error
    if (!(Column is MetaForeignKeyColumn))
        throw new InvalidOperationException(String.Format("Column {0} must be a foreign key column navigation property", Column.Name));

    // get current context
    context = Column.Table.CreateContext();

    // get hierarchical cascade columns
    var parentColumns = new SortedList<int, String>();
    for (int i = 0; i < cascadeHierarchicalAttribute.Parameters.Length; i++)
        parentColumns.Add(i, cascadeHierarchicalAttribute.Parameters[i]);

    // add extra column to represent this column itself
    parentColumns.Add(cascadeHierarchicalAttribute.Parameters.Length, String.Empty);

    //get current column into a local variable
    MetaForeignKeyColumn currentColumn = Column;

    // setup list of filter definitions
    for (int i = 0; i < parentColumns.Count; i++)
    {
        // get parent column name
        var parentColumnName = parentColumns[i];

        // create dropdown list
        var ddl = new DropDownList()
        {
            ID = String.Format("ListControl{0}", i),
            Enabled = false,
            AutoPostBack = true
        };

        // create filter
        var filter = new HierachicalListControl(ddl) { Column = currentColumn };

        // check for last parent filter
        if (!String.IsNullOrEmpty(parentColumnName))
        {
            // set parent column from parent table
            filter.ParentColumn = (MetaForeignKeyColumn)currentColumn.ParentTable.GetColumn(parentColumnName);

            // set current column to parent column
            currentColumn = filter.ParentColumn;
        }
        else
        {
            // this is the last parent and has
            // no parent itself so set to null
            filter.ParentColumn = null;
            currentColumn = null;
        }
        // add filter to list of filters
        filters.Add(i, filter);
    }

    // add dropdown list to page in correct order 2, 1, 0
    // last parent, parent<N>, child
    for (int i = parentColumns.Count - 1; i >= 0; i--)
    {
        // setup dropdown list
        filters[i].ListControl.Items.Clear();
        filters[i].ListControl.Items.Add(new ListItem("------", String.Empty));

        // add parent list controls event handler
        if (i > 0)
            filters[i].ListControl.SelectedIndexChanged += ListControls_SelectedIndexChanged;
        else
            filters[i].ListControl.SelectedIndexChanged += ListControl0_SelectedIndexChanged;

        // add control to place holder
        this.Controls.Add(filters[i].ListControl);
    }

    if (DefaultValue != null)
        PopulateAllListControls(DefaultValue);
    else
    {
        // fill last parent filter
        var lastParentIndex = filters.Count - 1;
        var parentTable = filters[lastParentIndex].Column.ParentTable;
        var parentQuery = parentTable.GetQuery(context);

        // set next descendant list control
        PopulateListControl(lastParentIndex, parentQuery);
    }
}

Listing 2 – Member variables and Page_Init

Now we add the following Listings 3 & 4 these are virtually the same as from the Edit template and so I am thinking in the next revision they may all be removed to the class library.

/// <summary>
/// Sets the default values.
/// </summary>
/// <param name="fieldValue">The value.</param>
private void PopulateAllListControls(object fieldValue)
{
    var displayStrings = new SortedList<int, String>();

    #region Get list of propert values
    // get property values
    var propertyValues = new SortedList<int, Object>();
    propertyValues.Add(0, fieldValue);
    for (int i = 0; i < filters.Count - 1; i++)
    {
        var parentName = filters[i].ParentColumn.Name;
        object pv = propertyValues[i].GetPropertyValue(parentName);
        propertyValues.Add(i + 1, pv);
    }
    #endregion

    // stating at the first filter and work way up to the last filter
    for (int i = 0; i < filters.Count; i++)
    {
        var parentTable = filters[i].Column.ParentTable;
        var parentQuery = parentTable.GetQuery(context);
        IQueryable listItemsQuery;
        if (i == cascadeHierarchicalAttribute.Parameters.Length)
        {
            listItemsQuery = parentQuery.GetQueryOrdered(parentTable);
        }
        else
        {
            var pcol = filters[i + 1].Column;
            var selectedValue = filters[i].ParentColumn.GetForeignKeyString(propertyValues[i]);
            listItemsQuery = parentQuery.GetQueryFilteredFkColumn(pcol, selectedValue);
        }

        // set next descendant list control
        PopulateListControl(i, listItemsQuery);

        // set initial values
        var selectedValueString = filters[i].Column.Table.GetPrimaryKeyString(propertyValues[i]);
        ListItem item = filters[i].ListControl.Items.FindByValue(selectedValueString);
        if (item != null)
            filters[i].ListControl.SelectedValue = selectedValueString;
    }
}

Listing 3 – PopulateAllListControls

/// <summary>
/// Handles the SelectedIndexChanged event of the parentListControls 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>
/// <summary>
/// Setups the parent list control.
/// </summary>
/// <param name="table">The table.</param>
/// <param name="filterIndex">The parent id.</param>
/// <param name="items">The items.</param>
public void PopulateListControl(int filterIndex, IQueryable items)
{
    // clear the list controls list property
    filters[filterIndex].ListControl.Items.Clear();
    // enable list control
    filters[filterIndex].ListControl.Enabled = true;

    // add unselected value showing the column name
    // [Styles]
    filters[filterIndex].ListControl.Items.Add(
        new ListItem(String.Format("[{0}]",
            filters[filterIndex].Column.DisplayName), NullValueString));

    foreach (var row in items)
    {
        // populate each item with the display string and key value
        filters[filterIndex].ListControl.Items.Add(
            new ListItem(filters[filterIndex].Column.ParentTable.GetDisplayString(row),
            filters[filterIndex].Column.ParentTable.GetPrimaryKeyString(row)));
    }
}

/// <summary>
/// Resets all descendant list controls.
/// </summary>
/// <param name="startFrom">The start from.</param>
private void ResetAllDescendantListControls(int startFrom)
{
    for (int i = startFrom - 1; i >= 0; i--)
    {
        filters[i].ListControl.Items.Clear();
        filters[i].ListControl.Items.Add(new ListItem("----", String.Empty));
        filters[i].ListControl.Enabled = false;
    }
}

Listing 4 – PopulateListControl and ResetAllDescendantListControls

Listing 5 is again virtually the same as the Edit templates but I kept it separate as it is an event handler and may make more sense to keep it in the template.

/// <summary>
/// Handles the SelectedIndexChanged event for each List control, 
/// and populates the next list control in the hierarchy.
/// </summary>
/// <param name="sender">The source of the event.</param>
/// <param name="e">
/// The <see cref="System.EventArgs"/> instance containing the event data.
/// </param>
void ListControls_SelectedIndexChanged(object sender, EventArgs e)
{
    // get list control
    var listControl = (ListControl)sender;

    // get the sending list controls id as an int
    var id = ((Control)sender).ID;

    // use regular expression to find list control index
    var regEx = new Regex(@"\d+");
    var parentIndex = int.Parse(regEx.Match(id).Value);

    if (listControl.SelectedValue != NullValueString)
    {
        if (parentIndex > 0)
        {
            // set child index
            var childIndex = parentIndex - 1;

            // get parent table
            var parentTable = filters[childIndex].Column.ParentTable;

            // get query from table
            var query = parentTable.GetQuery(context);

            // get items for list control
            var itemQuery = query.GetQueryFilteredFkColumn(
                filters[parentIndex].Column,
                listControl.SelectedValue);

            // populate list control
            PopulateListControl(childIndex, itemQuery);

            // reset all descendant list controls
            ResetAllDescendantListControls(childIndex);
        }
    }
    else
    {
        // reset all descendant list controls
        ResetAllDescendantListControls(parentIndex);
    }
}

Listing 5 – ListControls_SelectedIndexChanged

Next we replace the DropDownList1_SelectedIndexChanged with ListControl0_SelectedIndexChanged Listing 6 really only a name change to make the code read clearly.

protected void ListControl0_SelectedIndexChanged(object sender, EventArgs e)
{
    OnFilterChanged();
}

Listing 6 – ListControl0_SelectedIndexChanged

Finally we need to change the GetQueryable method to replace DropDownList1 with filters[0].ListControl.

Download

Happy coding

Note: With a few changes around the last two listings you could adapt this to either the old Dynamic Data Futures VS2008 SP1 RTM filters on CodePlex or Josh Heyes’s Dynamic Data Filtering.

Monday, 31 May 2010

Part 2 – A Cascading Hierarchical Field Template & Filter for Dynamic Data

In this second part we are going to complete the field templates by adding the CascadeHierarchical_Edit field template (see Figure 1) and in the next adding a CascadeHierarchical filter.

Starting with a copy of the ForeignKey_Edit field template as our base we will end up with a field template that looks like Figure 1 allowing you to have a hierarchy ‘n’ levels deep.

Cascade Hierarchical Field Template

Figure 1 – Cascade Hierarchical Field Template in action.

Note: It turns out the the Product field on the Order is only available in insert not edit so I have moved away from Northwind and have created a very construed Vehicles sample DB, I have included as a script in the download at the end of this article.

The first we must make the copy of the ForeignKey_Edit field template, and then make sure we rename it to CascadeHierarchical_Edit, also we must change the class name all it’s files; this sample being a Web Application project there are three files:

  • CascadeHierarchical_Edit.ascx
  • CascadeHierarchical_Edit.ascx.cs
  • CascadeHierarchical_Edit.ascx.designer.cs

Each must be modified as follows (I generally follow the naming convention use in the other standard field templates i.e. Text_Edit’s class name is Text_EditField), so we will set our field templates class name to CascadeHierarchical_EditField.

Note: if you change CascadeHierarchical_Edit.ascx first then you will not need to change the CascadeHierarchical_Edit.ascx.designer.cs will happen automatically. (I don’t know why the CascadeHierarchical_Edit.ascx.cs is not also changed automatically when the ascx file’s Inherits property is changed).

In the .cs file change the class name to CascadeHierarchical_EditField and in the CascadeHierarchical_Edit.ascx change the Inherits property to  be namespace.CascadeHierarchical_EditField, namespace is usually the name of the project, in any case if will already be filled in in the template unless you are copying from one project to another.

Now we have a basis to begin, lets first start with the Page_Init and some explanation.

<%@ Control 
    Language="C#" 
    CodeBehind="CascadeHierarchical_Edit.ascx.cs" 
    Inherits="CascadeHierarchicalFieldTemplate.CascadeHierarchical_EditField" %>

<asp:RequiredFieldValidator 
    runat="server" 
    ID="RequiredFieldValidator1" 
    CssClass="DDControl DDValidator" 
    Display="Static" 
    Enabled="false"/>
<asp:DynamicValidator 
    runat="server" 
    ID="DynamicValidator1" 
    CssClass="DDControl DDValidator" 
    Display="Static"/>

Listing 1 – the aspx page.

Here in Listing 1 you can see all we have are the required and dynamic validators, this is all we need at the dropdown lists will all be created dynamically.

#region member variables
// hold the current data context to access the model
private object context;
// hold the list of filters
private SortedList<int, HierachicalListControl> filters = new SortedList<int, HierachicalListControl>();
// hold the attribute
private CascadeHierarchicalAttribute cascadeHierarchicalAttribute;
#endregion

protected void Page_Init(object sender, EventArgs e)
{
    // check we have a cascade hierarchical attribute if not throw error
    cascadeHierarchicalAttribute = Column.GetAttribute<CascadeHierarchicalAttribute>();
    if (cascadeHierarchicalAttribute == null)
        throw new InvalidOperationException("Was expecting a CascadeFilterAttribute.");

    // check we have correct column type if not throw error
    if (!(Column is MetaForeignKeyColumn))
        throw new InvalidOperationException(String.Format("Column {0} must be a foreign key column navigation property", Column.Name));

    // get current context
    context = Table.CreateContext();

    // get hierarchical cascade columns
    var parentColumns = new SortedList<int, String>();
    for (int i = 0; i < cascadeHierarchicalAttribute.Parameters.Length; i++)
        parentColumns.Add(i, cascadeHierarchicalAttribute.Parameters[i]);

    // add extra column to represent this column itself
    parentColumns.Add(cascadeHierarchicalAttribute.Parameters.Length, "");

    //get current column into a local variable
    MetaForeignKeyColumn column = ForeignKeyColumn;

    // setup list of filter definitions
    for (int i = 0; i < parentColumns.Count; i++)
    {
        // get parent column name
        var parentColumnName = parentColumns[i];

        // create dropdown list
        var ddl = new DropDownList()
        {
            ID = String.Format("ListControl{0}", i),
            Enabled = false,
            AutoPostBack = true
        };
        // create filter
        var filter = new HierachicalListControl(ddl) { Column = column };

        // check for last parent filter
        if (!String.IsNullOrEmpty(parentColumnName))
        {
            // set parent column from parent table
            filter.ParentColumn = (MetaForeignKeyColumn)column.ParentTable.GetColumn(parentColumnName);

            // set current column to parent column
            column = filter.ParentColumn;
        }
        else
        {
            // this is the last parent and has
            // no parent itself so set to null
            filter.ParentColumn = null;
            column = null;
        }
        // add filter to list of filters
        filters.Add(i, filter);
    }

    // add dropdown list to page in correct order 2, 1, 0
    // last parent, parent<N>, child
    for (int i = parentColumns.Count - 1; i >= 0; i--)
    {
        // setup dropdown list
        filters[i].ListControl.Items.Clear();
        filters[i].ListControl.Items.Add(new ListItem("------", ""));

        // add parent list controls event handler
        if (i > 0)
            filters[i].ListControl.SelectedIndexChanged += ListControls_SelectedIndexChanged;

        // add control to place holder
        this.Controls.Add(filters[i].ListControl);
    }

    if (Mode == DataBoundControlMode.Insert)
    {
        // fill last parent filter
        var lastParentIndex = filters.Count - 1;
        var parentTable = filters[lastParentIndex].Column.ParentTable;
        var parentQuery = parentTable.GetQuery(context);

        // set next descendant list control
        PopulateListControl(lastParentIndex, parentQuery);
    }
}

Listing 2 -  Page_Init

The reason for using Page_Init (see Listing 2) is that we are going to create the dropdown lists dynamically, these need to be instantiated in the OnInit event to be fully involved in post back.

The first thing we need to do in the Page_Init is to make sure we have the correct Column Type (MetaForeignKeyColumn) and that the column has a CascadeHierarchicalAttribute assigned, if either of these in not present then we throw an error.

There are three for loops in the Page_Init, the first for loop we are building a list of parent columns to help with the next for loop which builds the filters list (by having two loops we make it easy to put the list controls onto the page in the most logical order, last parent to the left and child to the right).

Note: We also set each dropdown list’s SelectedIndexChanged event to post back to the same ListControls_SelectedIndexChanged handler.

This list of  filters is used throughout the rest of the field template to build each list control and made up of a dropdown list the current column and it’s parent column (see Listing 3).

/// <summary>
/// Class to contains information about cascading dropdown lists
/// </summary>
protected internal class HierachicalListControl
{
    /// <summary>
    /// Returns a <see cref="System.String"/> that represents this instance.
    /// </summary>
    /// <returns>
    /// A <see cref="System.String"/> that represents this instance.
    /// </returns>
    public override string ToString()
    {
        var parentColumn = ParentColumn != null ? ParentColumn.Name : "null";
        return String.Format("{0}.{1}", Column.Name, parentColumn);
        //return this.Column.Name;
    }

    /// <summary>
    /// Initializes a new instance of the <see cref="HierachicalListControl"/> class.
    /// </summary>
    /// <param name="column">This column.</param>
    /// <param name="parentColumn">This column's parent column.</param>
    public HierachicalListControl(ListControl listControl)
    {
        ListControl = listControl;
    }

    /// <summary>
    /// Gets or sets the filter column.
    /// </summary>
    /// <value>The column.</value>
    public MetaForeignKeyColumn Column { get; set; }

    /// <summary>
    /// Gets or sets the filter column's parent column.
    /// </summary>
    /// <value>The parent column.</value>
    public MetaForeignKeyColumn ParentColumn { get; set; }

    /// <summary>
    /// Gets or sets the list control.
    /// </summary>
    /// <value>The list control.</value>
    public ListControl ListControl { get; set; }
}

Listing 3 – HierachicalListControl used in list of filters .

Note: I have overridden the ToString method to make the filters list more readable in debug mode, in Column.ParentColumn format, this is not required for the sample to function correctly.

The final for loop is use to initialise each dropdown list with it’s default item, hook-up the SelectionIndexChanged event and then add it to the page.

The final segment of code in Page_Init is the if statement for when we are in insert mode as the OnDataBound event will not fire, so we populate the last filter the (highest in the hierarchy).

protected void Page_Load(object sender, EventArgs e)
{
    if (filters[0] != null &&
        filters[0].ListControl != null)
    {
        RequiredFieldValidator1.ControlToValidate = "ListControl0";
        DynamicValidator1.ControlToValidate = "ListControl0";
        SetUpValidator(RequiredFieldValidator1);
        SetUpValidator(DynamicValidator1);
    }
}

Listing 4 – Page_Load event handler

The Page_Load event just makes sure that we have some filter[0] and then sets up the validators. The next event handler is the OnDataBinding handler here we make sure we are in Edit mode and that the current value is not null and then we call the main method for setting up the filters SetupListControls.

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

    // Set initial value
    if (Mode == DataBoundControlMode.Edit && FieldValue != null)
        PopulateAllListControls(FieldValue);
}

/// <summary>
/// Sets the default values.
/// </summary>
/// <param name="fieldValue">The value.</param>
private void PopulateAllListControls(object fieldValue)
{
    var displayStrings = new SortedList<int, String>();

    #region Get list of propert values
    // get property values
    var propertyValues = new SortedList<int, Object>();
    propertyValues.Add(0, fieldValue);
    for (int i = 0; i < filters.Count - 1; i++)
    {
        var parentName = filters[i].ParentColumn.Name;
        object pv = propertyValues[i].GetPropertyValue(parentName);
        propertyValues.Add(i + 1, pv);
    }
    #endregion

    // stating at the first filter and work way up to the last filter
    for (int i = 0; i < filters.Count; i++)
    {
        var parentTable = filters[i].Column.ParentTable;
        var parentQuery = parentTable.GetQuery(context);
        IQueryable listItemsQuery;
        if (i == cascadeHierarchicalAttribute.Parameters.Length)
        {
            listItemsQuery = parentQuery.GetQueryOrdered(parentTable);
        }
        else
        {
            var pcol = filters[i + 1].Column;
            var selectedValue = filters[i].ParentColumn.GetForeignKeyString(propertyValues[i]);
            listItemsQuery = parentQuery.GetQueryFilteredFkColumn(pcol, selectedValue);
        }

        // set next descendant list control
        PopulateListControl(i, listItemsQuery);

        // set initial values
        var selectedValueString = filters[i].Column.Table.GetPrimaryKeyString(propertyValues[i]);
        ListItem item = filters[i].ListControl.Items.FindByValue(selectedValueString);
        if (item != null)
            filters[i].ListControl.SelectedValue = selectedValueString;
    }
}

Listing 5 – OnDataBinding and PopulateAllListControls

The first thing SetupListControls does is get a list of actual values for each filter in the cascade of filters using fieldValue as a starting point, working back from the passed in fieldValue, it calls the method GetPropertyValue which uses reflection to get the value of the next entity using the current value and the parent column’s name. These values are in turn used to fill each filter with appropriately filtered values. The last parent getting an unfiltered list of items as it has no parent itself.

/// <summary>
/// Gets the property value.
/// </summary>
/// <param name="sourceObject">The source object.</param>
/// <param name="propertyName">Name of the property.</param>
/// <returns>The named properties value.</returns>
public static Object GetPropertyValue(this Object sourceObject, 
    string propertyName)
{
    if (sourceObject != null)
        return sourceObject.GetType()
            .GetProperty(propertyName)
            .GetValue(sourceObject, null);
    else
        return null;
}

Listing 6 – GetPropertyValue

The two extension methods used to get the list of items are called GetQueryOrdered Listing 7 or GetQueryFilteredFkColumn Listing 8.

/// <summary>
/// Gets the query ordered.
/// </summary>
/// <param name="sourceQuery">The source query.</param>
/// <param name="table">The table.</param>
/// <returns></returns>
public static IQueryable GetQueryOrdered(this IQueryable sourceQuery, MetaTable table)
{
    // get display column attribute
    var displayColumnAttribute = table.GetAttribute<DisplayColumnAttribute>();

    // check to see if sort is assigned
    if (displayColumnAttribute == null || displayColumnAttribute.SortColumn == null)
        return sourceQuery;

    // {row.OrderBy(row => row.Name)}
    var orderByCall = GetOrderByCallExpression(
        sourceQuery, 
        table, 
        displayColumnAttribute.SortColumn, false);

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

Listing 7 – GetQueryOrdered

/// <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 GetQueryFilteredFkColumn(this IQueryable sourceQuery, 
    MetaForeignKeyColumn fkColumn, String fkSelectedValue)
{
    // if no filter value return the query
    if (String.IsNullOrEmpty(fkSelectedValue))
        return sourceQuery;

    // order query
    sourceQuery = GetQueryOrdered(sourceQuery, fkColumn.Table);

    // {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 whereLambda = Expression.Lambda(body, parameterExpression);

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

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

Listing 8 – GetQueryFilteredFkColumn

Listings 7 & 8 are form the IQueriableExtensionMethods.cs class file in the NotAClue.Web.DynamicData class library, and are a part of my dynamic Linq Expression extension methods that originally came from the ASP.NET July 2007 Futures Source Code sample on ASP.Net on CodePlex.com, I continue to expand and modify them as I need to; so I wont go into detail here as there better posts on Linq Expression than any I could write.

Note: There is a useful sample in the C# samples see Dynamic LINQ (Part 1: Using the LINQ Dynamic Query Library on Scott Guthrie's blog, “Dynamic Linq” allows you to easily build Linq queries on the fly.
/// <summary>
/// Handles the SelectedIndexChanged event of the parentListControls 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>
/// <summary>
/// Setups the parent list control.
/// </summary>
/// <param name="table">The table.</param>
/// <param name="filterIndex">The parent id.</param>
/// <param name="items">The items.</param>
public void PopulateListControl(int filterIndex, IQueryable items)
{
    // clear the list controls list property
    filters[filterIndex].ListControl.Items.Clear();
    // enable list control
    filters[filterIndex].ListControl.Enabled = true;

    // add unselected value showing the column name
    // [Styles]
    filters[filterIndex].ListControl.Items.Add(
        new ListItem(String.Format("[{0}]", 
            filters[filterIndex].Column.DisplayName), ""));

    foreach (var row in items)
    {
        // populate each item with the display string and key value
        filters[filterIndex].ListControl.Items.Add(
            new ListItem(filters[filterIndex].Column.ParentTable.GetDisplayString(row),
            filters[filterIndex].Column.ParentTable.GetPrimaryKeyString(row)));
    }
}

Listing 9 – PopulateListControl

Once we have the query filtered and ordered we then set the list control’s items up by calling PopulateListControl (Listing 9), then finally we set the initial value in of the list control.

/// <summary>
/// Handles the SelectedIndexChanged event for each List control, 
/// and populates the next list control in the hierarchy.
/// </summary>
/// <param name="sender">The source of the event.</param>
/// <param name="e">
/// The <see cref="System.EventArgs"/> instance containing the event data.
/// </param>
void ListControls_SelectedIndexChanged(object sender, EventArgs e)
{
    // get list control
    var listControl = (ListControl)sender;

    // get the sending list controls id as an int
    var id = ((Control)sender).ID;

    // use regular expression to find list control index
    var regEx = new Regex(@"\d+");
    var parentIndex = int.Parse(regEx.Match(id).Value);

    if (!String.IsNullOrEmpty(listControl.SelectedValue))
    {
        if (parentIndex > 0)
        {
            // set child index
            var childIndex = parentIndex - 1;

            // get parent table
            var parentTable = filters[childIndex].Column.ParentTable;

            // get query from table
            var query = parentTable.GetQuery(context);

            // get items for list control
            var itemQuery = query.GetQueryFilteredFkColumn(
                filters[parentIndex].Column,
                listControl.SelectedValue);

            // populate list control
            PopulateListControl(childIndex, itemQuery);

            // reset all descendant list controls
            ResetAllDescendantListControls(childIndex);
        }
    }
    else
    {
        // reset all descendant list controls
        ResetAllDescendantListControls(parentIndex);
    }
}

Listing 10 – parentListControls_SelectedIndexChanged

The next thing we have to deal with is SelectedIndexChanged event on each dynamically created filter, we do this in the ListControls_SelectedIndexChanged handler (Listing 10) we first get the posting list control and then extract the filter index from it’s name. Remember all the filters are named ListControl{N} where N is the index into the filters list for the filter.

Note: We use a regular expression “\d+” to match one or more digits in the list control’s ID

Then we check to see if the list control has a SelectedValue if not we reset it and all it’s children to the default of "----" and Enabled to false. If it has a SelectedValue then we get a query on the child column filtered by the current SelectedValue and populate the next list control in line.

We then call SetupParentListControl to populate the control and finally ResetAllDescendantListControls to set any descendant controls to their default value of "----" and disabled.

The last two methods as two of the standard field template methods just slightly change to avoid exceptions seen here in Listings 11.

protected override void ExtractValues(IOrderedDictionary dictionary)
{
    // If it's an empty string, change it to null
    string value;
    if (filters[0] != null && filters[0].ListControl != null)
        value = filters[0].ListControl.SelectedValue;
    else
        value = String.Empty;

    if (String.IsNullOrEmpty(value))
        value = null;

    ExtractForeignKey(dictionary, value);
}

public override Control DataControl
{
    get
    {
        if (filters[0] != null && filters[0].ListControl != null)
            return filters[0].ListControl;
        else
            return null;
    }
}

Listing 11 – ExtractValues method and DataControl property.

These two methods merely check to see if there are nay controls before processing.

Download

The sample is a Visual Studio 2010 and .Net 4.0 sample but the code and field template should work with Visual Studio 2008 SP1 and .Net 3.5 SP1 DD Web Application.

Note: The script for creating the Vehicles database is in the zip file.

Sunday, 30 May 2010

Neat Routing Technique for Restricting Access to Tables

In answering this question “Table specific routing for insert not working” on the Dynamic Data Forum I came across a  way to restrict which tables the catch all routes in Dynamic Data processes, allowing you to specify routes that only allows List or Edit etc.

// Customers route
routes.Add("Customers", new DynamicDataRoute("{table}/{action}.aspx")
{
    Constraints = new RouteValueDictionary(new
    {
        action = "List",
        table = "Customers"
    }),
    Model = DefaultModel,
});

// Employees route
routes.Add("Employees", new DynamicDataRoute("{table}/{action}.aspx")
{
    Constraints = new RouteValueDictionary(new
    {
        action = "List|Edit",
        table = "Employees"
    }),
    Model = DefaultModel,
});

// Categories route
routes.Add("Categories", new DynamicDataRoute("{table}/{action}.aspx")
{
    Constraints = new RouteValueDictionary(new
    {
        action = "List|Details|Insert",
        table = "Categories"
    }),
    Model = DefaultModel,
});

// catch all route
routes.Add("CatchAll", new DynamicDataRoute("{table}/{action}.aspx")
{
    Constraints = new RouteValueDictionary(new
    {
        action = "List|Details|Edit|Insert",
        table = "^((?!Customers|Employees|Categories).)*$"
    }),
    Model = DefaultModel
});

Listing 1 – Filtering tables routes

In this sample I am specifying three custom routes for:

  1. “Customer” table to allow List action.
  2. “Employees” table to allow List and Edit actions;
  3. “Categories” table to allow List, Details and Insert actions;

To show that you can restrict any individual action or combination of actins.

Note: If you do not have a List action in the actions constraint then the table will not appear on the Default.aspx as there will be no list action and it will appear to be unscaffolded, this is not the case if you try a route that you have allowed i.e. Details or Insert then the specified routes will work, it will just not appear on the Default.aspx page.

But without filtering these tables out of the the “Catch All” route (Dynamic Data default route) the missing function will be available via through the “Catch All” route.

The solution is to add a Constraint that says don’t generate a route for these tables, we do this with a regular expression:

      "^((?!Customers|Employees|Categories).)*$"

the route value dictionary actually puts the ^ and $ at the beginning and end of the expression but I have left them there for clarity of what the expression does (for those who really know what regular expressions mean Winking)

Disabled Edit and Details links

Figure 1 - Disabled Edit and Details links

In Figure 1 we see the Edit and Details links are disabled, but the Delete is still enabled this is because there is no route for Delete and so it is not controlled by routing.

!Important: In Visual Studio 2010 and .Net 4 the links still appear to be there i.e. they are not disabled but there is no URL assigned so the links do not work. so there will be a little more work to make the links disabled.

Happy coding Happy Wizzard

Tuesday, 25 May 2010

Part 1 – A Cascading Hierarchical Field Template & Filter for Dynamic Data

In the past I have done several articles on Cascading field templates and filters, these articles will cover a single cascading hierarchical field template that filters by it's parent fields without having to have those tables related directly related to the columns table.

ScreenShot258

Figure 1 – Old Cascading Relationships

So in the diagram above to have cascade from Builder->Developer->HouseType on the plot you must have an foreign key relationship with each of the Parent tables on the Plots tables. In the above Requires Plots table we have an FK relationship with Builder, Developer and HouseType to get the cascade to work.

There was however a Cascade filter in the old Dynamic Data Futures VS2008 SP1 RTM project on Codeplex. This did offered the hope of something better, which in this article we will build. First the field template then with a few changes a Filter.

So the aim of this sample if to have a single foreign key field on the table and have the selection cascade over each parent field.

The Attribute

We will need a simple attribute for this to pass in the list of parent columns

Cascade Relationship EF

Figure 2 – New Cascading Relationships

So we will need to pass the field template a list of parent foreign key navigation properties like this:

Cascade Relationship EF attributes

Figure 3 – Foreign Key columns

In Figure 3 the normal foreign key column Product already in known to use but we will need to supply the name of the next foreign key navigation property which will be  Category.

[UIHint("CascadeHierarchical")]
[CascadeHierarchical("Category")]
public Product Product { get; set; }

Listing 1 – Attribute applied

Note in Listing 1 we only need to supply the next navigation property and if there was another level up we could supply that also and so on.

[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)]
public class CascadeHierarchicalAttribute : Attribute
{
    public String[] Parameters { get; private set; }

    /// <summary>
    /// Initializes a new instance of the <see cref="CascadeHierarchicalAttribute"/> class.
    /// </summary>
    /// <param name="parameters">
    /// The parameters are the parent columns in
    /// order of  hierarchy and must be Foreign Key
    /// navigation columns for the hierarchy for car manufacturers
    /// e.g. Manufacturer, VehicleType, Model and Style on the 
    /// Styles table the parameters would be:
    /// [CascadeHierarchical("Manufacturer", "VehicleType", "Model")]
    /// as Style would already be known via foreign key navigation.
    /// </param>
    public CascadeHierarchicalAttribute(params String[] parameters)
    {
        Parameters = parameters;
    }
}

Listing 2 – CascadeHierarchicalAttribute

The only parameter takes the param attribute and takes an array of type String.

Read Only Field Template

So first thing is to create the read only version of the field template, so what will be required, if we look at the standard foreign key field template the only methods we will need to replace is the GetDisplayString() method. In here we will return a string of the form:

Manufacturer > Vehicle Type > Model > Style

this would gives us with cars something like:

“Ford > Car > Mondeo > 5 Door Hatch”

// value to use to separate values in the Hierarchy
private const string DISPLAY_SEPERATOR = " > ";

protected String GetDisplayString()
{
    // make sure this is a navigation property (ForeignKey column navigation property)
    if (!(Column is MetaForeignKeyColumn))
        throw new InvalidOperationException(String.Format("Column {0} must be a foreign key column navigation property", Column.Name));

    // get cascade hierarchical attribute
    var cascadeHierarchicalAttribute = Column.GetAttribute<CascadeHierarchicalAttribute>();
    if (cascadeHierarchicalAttribute == null)
        throw new InvalidOperationException("Was expecting a CascadeFilterAttribute.");

    // check for null value
    if (FieldValue != null)
    {
        //get parent table
        var parentTable = ForeignKeyColumn.ParentTable;

        // add this foreign key navigation property to the soreted list
        var fieldValues = new SortedList<int, String>();
        fieldValues.Add(0, ForeignKeyColumn.ParentTable.GetDisplayString(FieldValue));

        // get current value as an object
        Object currentValue = FieldValue;

        // loop through each navigation property to build values
        for (int i = 0; i < cascadeHierarchicalAttribute.Parameters.Length; i++)
        {
            // set name of parent column
            var parentColumnName = cascadeHierarchicalAttribute.Parameters[i];

            // get parent column from name
            var parentColumn = (MetaForeignKeyColumn)parentTable.GetColumn(parentColumnName);

            // check for valid column
            if (parentColumn == null && !(parentColumn is MetaForeignKeyColumn))
                throw new InvalidOperationException(String.Format("Invalid parent column {0}", parentColumnName));

            // get next value
            currentValue = currentValue.GetPropertyValue(parentColumnName);

            // extract current value as string using the parent table's display string
            var currentValueString = FormatFieldValue(parentColumn.ParentTable.GetDisplayString(currentValue));

            // add current value as string to the sorted field values list
            fieldValues.Add(i + 1, currentValueString);

            // set next parent table
            parentTable = parentColumn.ParentTable;
        }

        // build display string
        var displayString = new StringBuilder();
        for (int i = cascadeHierarchicalAttribute.Parameters.Length; i >= 0; i--)
            displayString.Append(fieldValues[i] + DISPLAY_SEPERATOR);

        // return the built string
        return FormatFieldValue(displayString.ToString().Substring(0, displayString.ToString().Length - 2));
    }
    else
        // same as standard foreign key field
        return FormatFieldValue(ForeignKeyColumn.ParentTable.GetDisplayString(FieldValue));
}

Listing 2 – new GetDisplayString method

now in Details and List pages we will get output like this:

25-05-2010 15-06-46

Figure 4 – new output from read only CascadeHierarchical field template.

The only clever piece of code in here is the GetPropValue() method which gets the actual value from the parent column using some basic reflection.

/// <summary>
/// Gets the property value.
/// </summary>
/// <param name="sourceObject">The source object.</param>
/// <param name="propertyName">Name of the property.</param>
/// <returns>The named properties value</returns>
public static Object GetPropertyValue(this Object sourceObject, string propertyName)
{
    if (sourceObject != null)
        return sourceObject.GetType().GetProperty(propertyName).GetValue(sourceObject, null);
    else
        return null;
}

Listing 3 -  GetPropertyValue

So in the next part we will create the CascadeHierarchical_Edit field template that has multiple dropdown lists populated one for each parent with the furthest grandparent filtering the next and so on to the child.

Note: I will add a project download in the next article.

Friday, 12 March 2010

Great Offer on Pre-Orders of Visual Studio 2010

Microsoft Visual Studio 2010 Professional will launch on April 12 but you can beat the rush and secure your copy today by pre-ordering at the affordable estimated retail price of £484.99.

If you use a previous version of Visual Studio or any other development tool then you are eligible for this upgrade. Along with all the great new features in Visual Studio 2010 (see Visual Studio) Visual Studio 2010 Professional includes a 12-month MSDN Essentials subscription which gives you access to core Microsoft platforms: Windows 7 Ultimate, Windows Server 2008 R2 Enterprise, and Microsoft SQL Server 2008 R2 Data Centre.

So visit

Pre-Order Visual Studio 2010
Pre-Order Visual Studio 2010

to check out all the new features and sign up for this great offer.

Sunday, 28 February 2010

SiteMapProvider for Dynamic Data (.Net 3.5 SP1 and .Net 4.0) (UPDATED)

There was a post on the DD Forum a little while ago (Last week when I wrote this article) and it’s the issue of menus and site navigation in Dynamic Data, I’ve had several go’s at this on several projects and although I have had workable solutions I felt I was reinventing the wheel each time. So after the post politely asking for me to go into it a little deeper I made some suggestions on the forum that afterwards I felt were still going about this in the wrong way. What I felt we needed was our own SiteMapDataSourceBlushingand of course I was wrong, I discovered after a little research was I needed a SiteMapProvider which would plug into the SiteMapDataSource via the web.config allowing me to provide my own solution to navigation and menus. This is very similar the MembersipProvider and RolesProvider which makes this really coolBig Grin

So now we will have a solution that is easy to deploy and maintain, so in good Blue Peter style here are the things we are going to need:


    The Menu attribute
    The SiteMapProvider
    Updates to the Web.Config
    Metadata
    And a Menu and SiteMapPath (bread crumb) controls
    web.sitemap file for custom paths (non Model pages)

The MenuAttribute

At first I hade some code like Listing 1 here I generated the code in the page (site.master) and bound it to an XmlDataSource this worked after a fashion and produced the menus I wanted but they were organised by the DB structure not really what I wanted.

protected void Page_Load(object sender, EventArgs e)
{
    //System.Collections.IList visibleTables = MetaModel.Default.VisibleTables;
    System.Collections.IList visibleTables = MetaModel.Default.VisibleTables;
    if (visibleTables.Count == 0)
    {
        throw new InvalidOperationException("There are no accessible tables. Make..");
    }

    var root = new XElement("home");

    foreach (var table in MetaModel.Default.VisibleTables)
    {
        root.Add(GetChildren(table));
    }
    XmlDataSource1.Data = root.ToString();
    Menu1.DataSource = XmlDataSource1;
    Menu1.Orientation = Orientation.Horizontal;
    Menu1.DataBind();
}

private XElement GetChildren(MetaTable parent)
{
    XElement children =
        new XElement("Menu",
            new XAttribute("title", parent.DisplayName),
            new XAttribute("url", parent.ListActionPath),
                    from c in parent.Columns
                    where c.GetType() == typeof(MetaChildrenColumn) &&
                        ((MetaChildrenColumn)c).ChildTable.Name != parent.Name
                    orderby c.DisplayName
                    select GetChildren(((MetaChildrenColumn)c).ChildTable));

    return children;
}

Listing 1 – old Menu generating code

I suppose I could have used a sitemap file (“Web.sitemap” by default) I decided after a few stunted attempts with client sites that although this gave you the control it was tedious and boring especially with an constantly changing site.

So my final design was to have an attribute to apply to the metadata that would allow me to impose the site structure I wanted in the same place I was applying the all my other attributes (keep it all in one place is what I say then when you go looking it’s easy to find.)

/// <summary>
/// Attribute to identify which column to use as a 
/// parent column for the child column to depend upon
/// </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class MenuAttribute : Attribute, IComparable
{
    public static MenuAttribute Default = new MenuAttribute();

    /// <summary>
    /// Gets or sets the name of the menu.
    /// </summary>
    /// <value>The name of the menu.</value>
    public String Name { get; private set; }

    /// <summary>
    /// Gets or sets the parent.
    /// </summary>
    /// <value>The parent.</value>
    public String Parent { get; set; }

    /// <summary>
    /// Gets or sets a value indicating whether this <see cref="MenuAttribute"/> is show.
    /// </summary>
    /// <value><c>true</c> if show; otherwise, <c>false</c>.</value>
    public Boolean Show { get; set; }

    /// <summary>
    /// Gets or sets the order.
    /// </summary>
    /// <value>The order.</value>
    public int Order { get; set; }

    /// <summary>
    /// Gets or sets the image path.
    /// </summary>
    /// <value>The image path.</value>
    public String ImagePath { get; set; }

    /// <summary>
    /// Initializes a new instance of the <see cref="MenuAttribute"/> class.
    /// </summary>
    public MenuAttribute()
    {
        Name = String.Empty;
        Parent = String.Empty;
        ImagePath = String.Empty;
        Show = false;
        Order = 0;
    }
    /// <summary>
    /// Initializes a new instance of the <see cref="MenuAttribute"/> class.
    /// </summary>
    /// <param name="menuName">Name of the menu.</param>
    public MenuAttribute(String menuName)
    {
        Name = menuName;
    }

    #region IComparable Members
    public int CompareTo(object obj)
    {
        return Order - ((MenuAttribute)obj).Order;
    }
    #endregion
}

Listing 2 – the Menu attribute

I decided I wanted to be able to specify which table are in the root of the menu which are children and of which table and also if a table is shown or not, see Listing 2. to make a table appear in the root of the menu structure you need to Parent to be empty and Show must be true*  and Children must Have the Parent set to the menu Name if specified or Table Name and be set to show.

* This is because I will be using my GetAttributeOrDefault extension method see Listing 3
/// <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 3 GetAttributeOrDefault extension method.

I also threw in an Order property so the menus could be sorted on both the root and sub menus and an ImagePath property so that you can specify an image. Name is optional and in not specified it will user the DisplayName of the table.

The SiteMapProvider

This will go away and read the MetaModel and produce the site map from that instead of a web.sitemap file.

#region Constants
private const String PROVIDER_NAME = "MetaDataSiteMapProvider";
private const string CACHE_DEPENDENCY_NAME = "MetaDataSiteMapCacheDependency";
private const String DESCRIPTION = "description";
private const String SITE_MAP_FILE = "SiteMapFile";
private const String DEFAULT_SITE_MAP_FILE = "DynamicData.sitemap";
#endregion

#region Fields
private readonly object objLock = new object();
private SiteMapNode rootNode;
private String _siteMapFile;
#endregion

#region Properties
private MetaModel _model;
public MetaModel Model
{
    get
    {
        if (_model == null)
            _model = MetaModel.Default;
        return this._model;
    }
    set { this._model = value; }
}
#endregion

#region Initializartion
public override void Initialize(string name, NameValueCollection config)
{
    if (config == null)
        throw new ArgumentNullException("config");

    // get provider name
    if (String.IsNullOrEmpty(name))
        name = "MetaDataSiteMapProvider";

    // get custom mappings
    if (!string.IsNullOrEmpty(config[SITE_MAP_FILE]))
        _siteMapFile = config[SITE_MAP_FILE];
    else
        _siteMapFile = DEFAULT_SITE_MAP_FILE;

    // get description
    if (string.IsNullOrEmpty(config[DESCRIPTION]))
    {
        config.Remove(DESCRIPTION);
        config.Add(DESCRIPTION, "MetaData Site Map Provider for Dynamic Data");
    }

    base.Initialize(name, config);
}
#endregion

/// <summary>
/// Loads the site map information from 
/// persistent storage and builds it in memory.
/// </summary>
/// <returns>
/// The root <see cref="T:System.Web.SiteMapNode"/> 
/// of the site map navigation structure.
/// </returns>
public override SiteMapNode BuildSiteMap()
{
    // need to make sure we start with a clean slate
    if (rootNode != null)
        return rootNode;

    lock (objLock)
    {
        rootNode = new SiteMapNode(this, "root");

        // get tables for
        var tables = from t in Model.Tables
                     where String.IsNullOrEmpty(t.GetAttributeOrDefault<MenuAttribute>().Parent) &&
                        t.GetAttributeOrDefault<MenuAttribute>().Show
                     // sort first by menu order then by name.
                     orderby t.GetAttributeOrDefault<MenuAttribute>(), t.DisplayName
                     select t;

        // get external content.
        var sitemapFile = HttpContext.Current.Server.MapPath("~/" + _siteMapFile);
        if (File.Exists(sitemapFile))
        {
            var sitemap = XDocument.Load(sitemapFile);
            var elements = sitemap.Descendants().Descendants().Descendants();
            foreach (var element in elements)
            {
                var provider = element.Attributes().FirstOrDefault(a => a.Name == "provider");
                if (provider != null &&
                    !String.IsNullOrEmpty(provider.Value) &&
                    provider.Value == PROVIDER_NAME)
                {
                    foreach (var table in tables)
                    {
                        SetChildren(table, rootNode);
                    }
                }
                else
                {
                    SetXmlChildren(element, rootNode);
                }
            }
        }
        else
        {
            foreach (var table in tables)
            {
                SetChildren(table, rootNode);
            }
        }
    }

    // not sure if this is needed no real explination
    // was given in the samples I've seen.
    HttpRuntime.Cache.Insert(CACHE_DEPENDENCY_NAME, new object());

    return rootNode;
}

private void SetXmlChildren(XElement element, SiteMapNode parentNode)
{
    var url = element.Attributes().First(a => a.Name == "url");
    var node = new SiteMapNode(this, url.Value, url.Value);

    foreach (var attribute in element.Attributes())
    {
        switch (attribute.Name.ToString())
        {
            case "description":
                node.Description = attribute.Value;
                break;
            case "resourceKey":
                node.ResourceKey = attribute.Value;
                break;
            case "roles":
                node.Roles = attribute.Value.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
                break;
            case "title":
                node.Title = attribute.Value;
                break;
            case "siteMapFile":
            case "securityTrimmingEnabled":
            case "provider":
            case "url":
            default:
                break;
        }
    }

    AddNode(node, parentNode);

    if (element.HasElements)
    {
        foreach (var childElement in element.Descendants())
        {
            SetXmlChildren(childElement, node);
        }
    }
}

/// <summary>
/// Sets the children nodes of the current node.
/// </summary>
/// <param name="parentTable">The parent table.</param>
/// <param name="parentNode">The parent node.</param>
private void SetChildren(MetaTable parentTable, SiteMapNode parentNode)
{
    String imageUrl = String.Empty;
    var description = parentTable.GetAttribute<DescriptionAttribute>();
    var menuAttribute = parentTable.GetAttribute<MenuAttribute>();
    if (menuAttribute != null)
        imageUrl = menuAttribute.ImagePath;

    // get extra attributes, I'm just going to 
    // use the ImageUrl for use in menus etc.
    NameValueCollection attributes = null;
    if (String.IsNullOrEmpty(imageUrl))
    {
        attributes = new NameValueCollection();
        attributes.Add("ImageUrl", imageUrl);
    }

    // get the title
    var menuTitle = !String.IsNullOrEmpty(menuAttribute.Name)
        ? menuAttribute.Name : parentTable.DisplayName;

    // get the description if attribute has 
    // been applied or DisplayName if not.
    var menuDescription = description != null
        ? description.Description : parentTable.DisplayName;

    // note resource keys are not used I'm 
    // not bothering with localization.
    var node = new SiteMapNode(
        this,
        // note Key and Url must match
        // for slected menus to showup
        parentTable.ListActionPath,
        parentTable.ListActionPath,
        menuTitle,
        menuDescription,
        null,
        attributes,
        null,
        "");

    // we can't add two nodes with same URL an 
    // InvalidOperationException with be thrown if we do
    AddNode(node, parentNode);

    // get the children nodes of this node.
    var tables = from t in Model.Tables
                 where !String.IsNullOrEmpty(t.GetAttributeOrDefault<MenuAttribute>().Parent) &&
                   t.GetAttributeOrDefault<MenuAttribute>().Parent == menuTitle &&
                   t.GetAttributeOrDefault<MenuAttribute>().Show
                 orderby t.GetAttributeOrDefault<MenuAttribute>(), t.DisplayName
                 select t;

    // add children of current node
    foreach (var t in tables)
    {
        // call this method recursively.
        SetChildren(t, node);
    }
}

/// <summary>
/// Retrieves the root node of all the nodes that 
/// are currently managed by the current provider.
/// </summary>
/// <returns>
/// A <see cref="T:System.Web.SiteMapNode"/> that 
/// represents the root node of the set of nodes 
/// that the current provider manages.
/// </returns>
protected override SiteMapNode GetRootNodeCore()
{
    BuildSiteMap();
    return rootNode;
}

/// <summary>
/// Removes all elements in the collections of 
/// child and parent site map nodes that the 
/// <see cref="T:System.Web.StaticSiteMapProvider"/> 
/// tracks as part of its state.
/// </summary>
protected override void Clear()
{
    lock (objLock)
    {
        this.rootNode = null;
        base.Clear();
    }
}

Listing – 4 the SiteMapProvider

Updated: I have made a few changes in Listing 4 I found that I was still missing something mainly an easy way to add custom page mappings (SiteMapNodes) so I have added the facility to use a web.sitemap my default file name is DyanmicData.sitemap.

With this added and an entry for the provider set to "MetaDataSiteMapProvider" the standard nodes are added and when the node (and “there can be only one”) the Metadata nodes are added. See Figure 1.

In the SiteMapProvider Listing 4 the two main methods are BuildSiteMap and SetChildren; BuildSiteMap  starts by building the root menu and then calls SetChildren for each menu entry in the root to create the sub menus entries (and their sub menus as it calls it’s self recursively).

<?xml version="1.0" encoding="utf-8" ?>
<siteMap xmlns="http://schemas.microsoft.com/AspNet/SiteMap-File-1.0" >
    <siteMapNode url="" title=""  description="">
      <siteMapNode url="~/Default.aspx" title="Home"  description="Home page" roles="*" />
      <siteMapNode provider="MetaDataSiteMapProvider"  />
      <siteMapNode url="~/Admin.aspx" title="Admin"  description="Site Administration" roles="*" />
    </siteMapNode>
</siteMap>

Listing 5 – the DynamicData.sitemap file

To make this work we need an entry in the web.config file see Listing 5 this entry live inside the <system.web> tag.

<siteMap defaultProvider="MetaDataSiteMapProvider">
	<providers>
		<add name="MetaDataSiteMapProvider" 
		    Description="MetaData Site Map Provider for Dynamic Data" 
		    type="NotAClue.Web.DynamicData.MetaDataSiteMapProvider, NotAClue.Web.DynamicData"/>
	</providers>
</siteMap>

Listing 6 – SiteMap entry in web.config.

Adding Menu and SiteMapPath controls to Site.Master

Now we need to add a Menu or TreeView control to the site.master page (I am also going to add a SiteMapPath control as a bread crumb)

<div>
    <asp:Menu ID="Menu1" runat="server" BackColor="#E3EAEB" 
        DataSourceID="SiteMapDataSource1" DynamicHorizontalOffset="2" 
        Font-Names="Verdana" Font-Size="8pt" ForeColor="#666666" 
        Orientation="Horizontal" StaticSubMenuIndent="10px" 
        StaticDisplayLevels="2">
        <StaticSelectedStyle BackColor="#1C5E55" />
        <StaticMenuItemStyle HorizontalPadding="5px" VerticalPadding="2px" />
        <DynamicHoverStyle BackColor="#666666" ForeColor="White" />
        <DynamicMenuStyle BackColor="#E3EAEB" />
        <DynamicSelectedStyle BackColor="#1C5E55" />
        <DynamicMenuItemStyle HorizontalPadding="5px" VerticalPadding="2px" />
        <StaticHoverStyle BackColor="#666666" ForeColor="White" />
    </asp:Menu>
    <asp:SiteMapDataSource ID="SiteMapDataSource1" runat="server" ShowStartingNode="False" />
</div>
<div>
    <asp:SiteMapPath ID="SiteMapPath1" runat="server" Font-Names="Verdana" 
        Font-Size="8pt" PathSeparator=" > ">
        <PathSeparatorStyle Font-Bold="True" ForeColor="#1C5E55" />
        <CurrentNodeStyle ForeColor="#333333" />
        <NodeStyle Font-Bold="True" ForeColor="#666666" />
        <RootNodeStyle Font-Bold="True" ForeColor="#1C5E55" />
    </asp:SiteMapPath>
</div>

Listing 7 – adding Menu and SiteMapPath controls to site.master.

In Listing 6 I’ve just dragged and dropped ASP.Net Menu and SiteMapPath web controls to the page an configured the Menu by adding a new SiteMapDataSource control.

Note: You could also set the SiteMapProvider to our MetaDataSiteMapProvider this is not really required as we have set as default in the web.config.

Then I have used the Auto Format… option to give both the Menu and SiteMapPath some styling.

Note: In ASP.Net 4.0 the Menu Control like many other controls now boasts cleaner mark-up; in the Menus case it now uses un-ordered lists to produce the menu.

Sample Metadata using Northwind

[Menu(Show = true, Parent = "Products", Order = 0)]
partial class Category { }

[Menu(Show = true, Parent = "Employees", Order = 0)]
partial class Territory { }

partial class CustomerCustomerDemo { }

partial class CustomerDemographic { }

[Menu("Customers", Show = true, Order = 0)]
partial class Customer { }

[Menu("Employees", Show = true, Order = 2)]
partial class Employee { }

partial class EmployeeTerritory { }

[Menu("Order Details", Show = true, Parent = "Orders", Order = 0)]
partial class Order_Detail { }

[Menu("Orders", Show = true, Order = 1)]
partial class Order { }

[Menu("Products", Show = true, Order = 3)]
partial class Product { }

[Menu("Regions", Show = true, Parent = "Territories", Order = 0)]
partial class Region { }

[Menu("Shippers", Show = true, Parent = "Orders", Order = 0)]
partial class Shipper { }

[Menu("Suppliers", Show = true, Parent = "Products", Order = 1)]
partial class Supplier { }

Listing 8 – sample metadata.

Note: I’ve trimmed the metadata right down removing the metadata classes to keep it brief.

The Result

Dynamic Data Menus and bread crumb (SiteMapPath) Asp.Net 4.0 Tabbed Menu

Figure 1 – Dynamic Data Menus and bread crumb (SiteMapPath)

Downloads

Left is the ASP.Net 3.5 SP1 and right is the .Net 4.0 version & now with the new Tabbed Menu from the new ASP.Net project template.