Monday, 23 March 2009

Cascading or Dependant Field Templates for ASP.Net 4.0 Preview

Introduction

So Here’s the Idea (which is a continuation of Dynamic Data – Cascading FieldTemplates) you have a field that is a dropdown list of one thing and another that is a subset of the previous. So to have a real world example (I’ve been working in the House Building Industry in the UK for the last 5 years so I have a ready example.

A very simple House Sales system

Figure 1 – A very simple House Sales system

In Figure 1 we have five tables, each Builder would have a number of Developments and each Development would have a series of House Types. Now a Plot would exist on a Development of a single Builder and have a House Type, in my example I have two plots tables RequiredPlot and NotRequiredPlot the difference is simple in one all the FK fields are require i.e. not nullable and in the other they are nullable. This is to show both faces of the cascading FieldTemplate. When you insert a plot in Dynamic Data then you must choose the Builder the Development and the House Type.

Insert New Plot NotRequired  (Note the [Not Set] in the NotRequired) Insert New Plot Required

Figure 2 – Insert New Plot NotRequired and Required (Note the [Not Set] in the NotRequired)

Here in Figure 2 Inserting a new Plot we have a problem at the moment I can choose a development of a different builder than the plot and the same with house type.

So my idea here is to provide a generic way of saying this field depends upon that filed (i.e. House Type depends upon Development and Development depends upon Builder).

Bug Undocumented Feature

And also having done this before I have found a nasty bug (sorry) undocumented feature my software never has bugs only undocumented features. This err Feature shows it’s self when you have a list where one say in our case Builder has only one Development and you are not using the [not set] and only showing values if you select Builder A and her has two developments and you select development 2 this will filter the contents of the House Types dropdown list, if you then change the Builder to a builder that only have one development then you will not get the corrects house types showing in the House Types dropdown list, because you cannot change the Development because there is only one.

OK I hope that last paragraph made sense because it took me a while to see why the problem exists, which is the cascade does not ripple through the cascading controls. This version of cascading Field Template will rectify this problem.

So explanation over, to the code.

Building the Extension the the FieldTemplateUserControl

The two main requirements of this implementation are:

  1. Have a ripple down the chain of controls when a selection at the top is made.
  2. A generic means of cascade.

In the previous version I simply hooked up the SelectionChanged event of the DropDownList but that only fired when the dropdown list is changed manually. So this time we will implement our own event in the UserControl which we can fire on DropDownList SelectionChanged event and when we receive a SelectionChange event from the parent control.

Before we dive into the main body of code we need to understand the event structure so here’s a basic intro to events. I’m going to use some terminology so here are my definitions how I understand it.

Term Explanation Where
Delegate Think of a Delegate as the declaration of the method pattern you must use to implement or consume this event Publisher
Publisher This is the code that has the event and wants to let other code know about the event  
Client This is the code that wants to know when the event happens in the publisher  
X This is the event  
Publish Is saying I have an event that can inform you when X happens Publisher 
Raise The publisher announces that X has happened Publisher
Subscribe Tell me when X happens Client
Consume Is actually deal with the results of the event X Client
EventArgs information passed to the event from the Publisher relating to event X own class

Table 1 – Event Terms and Descriptions

I personally find most explanations of event handling confusing so I cobbled together the above from what I’ve read, to help stop me getting confused when need to work with events.

Cascade Attribute and Associated Extension Methods

We need an attribute

/// <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

Below are the standard extension methods I use to find attributes I use these so I don’t have to test for null as I know I will get an attribute back, for a detailed explanation of these see Writing Attributes and Extension Methods for Dynamic Data.

public static partial class HelperExtansionMethods
{
    /// <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();
    }

    /// <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 – Extension methods for finding attributes

Custom EventArgs Type

So the first thing we need is an event argument to pass the value of the selected item to the child field, since we are going to the trouble of having our own event we may as well do away with the whole looking for the control and extracting it’s value that we did before this will greatly simplify the code and make it more efficient as we will not be searching the control tree which takes time.

/// <summary>
/// Event Arguments for Category Changed Event
/// </summary>
public class SelectionChangedEventArgs : EventArgs
{
    /// <summary>
    /// Initializes a new category changed event
    /// </summary>
    /// <param name="categoryId">
    /// The categoryId of the currently selected category
    /// </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 – Selection Changes Event Arguments

CascadingFieldTemplate Class

This part of this article involves creating the cascade class to apply to the ForeignKey_Edit FieldTEmplate

Class diagram for CascadingFieldTemplate

Figutre 3 -  Class diagram for CascadingFieldTemplate

I will be using a few bits from the old Dynamic Data Futures project which you can find here Dynamic Data on Codeplex the file I will be using is the LinqExpressionHelper file as what the point of writing what already bee written. I’ll point out this code when we get to it but it’s always worth crediting the author of the code, so as usual all the credit goes to the ASP.Net team for the really clever bit of code here.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web.DynamicData;
using System.Web.UI.WebControls;
using System.Linq.Expressions;
using System.Web.UI;

/// <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; }
}

/// <summary>
/// Modifies the standard FieldTemplateUserControl 
/// to support cascading of selected values.
/// </summary>
public class CascadingFieldTemplate : FieldTemplateUserControl
{
    /// <summary>
    /// Data context
    /// </summary>
    private object DC;

    /// <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)
    {
        DC = Table.CreateContext();

        // 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;

        //TODO: find a way of determining which the parent control is DetailsView or FormView

        // get dependee 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 parent FiledTeamlate
            string[] parentColumnPKV = filterValue.Split(',');
            var parentFieldTemplate = GetSelectedParent(
                parentColumnPKV, 
                ParentColumn.ParentTable);

            // get list of values filteres by the parent's selected entity
            var itemlist = GetQueryFilteredByParent(
                childTable, 
                ParentColumn, 
                parentFieldTemplate);

            // 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>
    /// Get the entity value of the selected 
    /// value of the parent column
    /// </summary>
    /// <param name="primaryKeyValues">
    /// An array of primary key values
    /// </param>
    /// <param name="parentTable">
    /// Parent columns FK table
    /// </param>
    /// <returns>
    /// Returns the currently selected entity
    /// from the parent list as an object
    /// </returns>
    private object GetSelectedParent(
        string[] primaryKeyValues, 
        MetaTable parentTable)
    {
        var query = parentTable.GetQuery(DC);

        // Items.Where(row => row.ID == 1).Single()
// this is where I use that file from Dynamic Data Futures
var singleWhereCall = LinqExpressionHelper.BuildSingleItemQuery( query, parentTable, primaryKeyValues); return query.Provider.Execute(singleWhereCall); } /// <summary> /// Returns an IQueryable of the current FK table filtered by the /// currently selected value from the parent filed template /// </summary> /// <param name="childTable"> /// This columns FK table /// </param> /// <param name="parentColumn"> /// Column to filter this column by /// </param> /// <param name="selectedParent"> /// Value to filter this column by /// </param> /// <returns> /// An IQueryable result filtered by the parent columns current value /// </returns> 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); } /// <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 – SelectionChangedEventArgs Class

You will note this class inherits the FieldTemplateUserControl class so we get all the magic of the underlying class. We declare a delegate:

public delegate void SelectionChangedEventHandler(object sender, SelectionChangedEventArgs e);

Note the SelectionChangedEventArgs are not just empty standard EventArgs we now get the value of the parent passed in from the parent of the currently selected value.

And then the event:

public event SelectionChangedEventHandler SelectionChanged;

The we have the guts of the class PopulateListControl which takes the selected value pass in from the parent FieldTemplate where we get a list of items for this column filtered by the parent value if it exists and has a value.

Then we have GetQueryFilteredByParent where we actually get the list of items filtered by the parent FieldTemplate.

And I did say I’d point out where I was using that helper class from Dynamic Data Futures:

var singleWhereCall = LinqExpressionHelper.BuildSingleItemQuery(query, parentTable, primaryKeyValues);

Also here it’s worth talking about:

private CompositeDataBoundControl GetContainerControl()

and in particular this line of code:

var p = parentControl as CompositeDataBoundControl;

the CompositeDataBoundControl is the base type of the DetailsView and the FormView as will know if you’ve looked at the previews (I’ve only just got into it been too busy with work, but that’s stopped for a little while) to facilitate EntityTemplates Details, Edit and Insert now use the FormView control so I’ve written this modification to handle either version, the release with .Net 3.5 SP1 or the Preview. Note however that if you use ListView with inline editing as in my article:

Custom PageTemplates Part 3 - Dynamic/Templated Grid with Insert (Using ListView)

then you will need to modify the code to search for the ListView also.

Modifying the Standard ForeignKey_Edit FieldTemplate

Here we will wire up the SelectionChanged event so a change in FieldTemplate will ripple down the list

parent –> child(parent) –> child etc.

Here again is the code for the FieldTemplate as it has been modified:

using System;
using System.Collections.Specialized;
using System.ComponentModel.DataAnnotations;
using System.Web.DynamicData;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;

public partial class ForeignKey_EditField : CascadingFieldTemplate //System.Web.DynamicData.FieldTemplateUserControl
{
    protected override void Page_Init(object sender, EventArgs e)
    {
        // remember to call the base class
        base.Page_Init(sender, e);

        // add event handler if dependee exists
        if (ParentControl != null)
        {
            // subscribe to event
            ParentControl.SelectionChanged += SelectedIndexChanged;
        }
    }

    protected void Page_Load(object sender, EventArgs e)
    {
        if (DropDownList1.Items.Count == 0)
        {
            if (Mode == DataBoundControlMode.Insert || !Column.IsRequired)
                DropDownList1.Items.Add(new ListItem("[Not Set]", ""));

            PopulateListControl(DropDownList1);
        }

        SetUpValidator(RequiredFieldValidator1);
        SetUpValidator(DynamicValidator1);
    }

    #region Event
    // raise event
    protected void DropDownList1_SelectedIndexChanged(object sender, EventArgs e)
    {
        RaiseSelectedIndexChanged(DropDownList1.SelectedValue);
    }

    // consume event
    protected void SelectedIndexChanged(object sender, SelectionChangedEventArgs e)
    {
        PopulateListControl(DropDownList1, e.Value);

        if (Mode == DataBoundControlMode.Insert || !Column.IsRequired)
            RaiseSelectedIndexChanged("");
        else
        {
            RaiseSelectedIndexChanged(DropDownList1.Items[0].Value);
        }
    }
    #endregion

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

        if (Mode == DataBoundControlMode.Edit)
        {
            string selectedValueString = GetSelectedValueString();
            ListItem item = DropDownList1.Items.FindByValue(selectedValueString);
            if (item != null)
            {
                // if there is a default value cascade it
                RaiseSelectedIndexChanged(item.Value);
                DropDownList1.SelectedValue = selectedValueString;
             }
        }
        else if (Mode == DataBoundControlMode.Insert)
        {
            // if child field has hook up for cascade
            RaiseSelectedIndexChanged("");
        }
    }

    protected override void ExtractValues(IOrderedDictionary dictionary)
    {
        // If it's an empty string, change it to null
        string value = DropDownList1.SelectedValue;
        if (String.IsNullOrEmpty(value))
        {
            value = null;
        }

        ExtractForeignKey(dictionary, value);
    }

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

Listing 4 - ForeignKey_Edit FieldTemplate

I’ve highlighted all the changes in BOLD ITALIC to emphasise  the content the main thins to look for are:

In the Page_Init here wee hook up the event if there is a parent control, and then the section that is surrounded with a region called event here we react to the SelectedIndexChanged event of the DropDownList1 and also consume the event from the parent if it exists.

<asp:DropDownList 
    ID="DropDownList1" 
    runat="server" 
    CssClass="DDDropDown" 
    AutoPostBack="True" 
    onselectedindexchanged="DropDownList1_SelectedIndexChanged">
</asp:DropDownList>

Listing 5 – changes to the DropDownList control in the page

Here again in Listing 5 I’ve highlighted the changed content, firstly we have enabled AutoPostBack and wired up the OnSelectedIndexChanged event to the event handler DropDownList1_SelectedIndexChanged in the code behind inside the event region.

Sample Metadata

[MetadataType(typeof(RequiredPlotMD))]
public partial class RequiredPlot
{
    public partial class RequiredPlotMD
    {
        public object Id { get; set; }
        public object BuilderId { get; set; }
        public object DeveloperId { get; set; }
        public object HouseTypeId { get; set; }
        public object No { get; set; }

        public object Builder { get; set; }
        [Cascade("Builder")]
        public object Developer { get; set; }
        [Cascade("Developer")]
        public object HouseType { get; set; }
    }
}

[MetadataType(typeof(NotRequiredPlotMD))]
public partial class NotRequiredPlot
{
    public partial class NotRequiredPlotMD
    {
        public object Id { get; set; }
        public object BuilderId { get; set; }
        public object DeveloperId { get; set; }
        public object HouseTypeId { get; set; }
        public object No { get; set; }

        public object Builder { get; set; }
        [Cascade("Builder")]
        public object Developer { get; set; }
        [Cascade("Developer")]
        public object HouseType { get; set; }
    }
}

Listing 6 – Sample metadata

In the attached file is a copy of the website zipped a script for creating the database and an excel spreadsheet with all the data which you can import into the DB which is fiddly but it saves me having to have several different version of the DB

I would go into more detail breaking it down line by line but I like to have the full listing with lots of comments myself, but if you think I should be more detailed let me know.

Note: This also works with Dynamic Data from .Net 3.5 SP1

Happy coding 

Microsoft ASP.NET 4.0 Data Access and Dynamic Data at MIX09

See David Ebbo’s great session at MIX09 for the latest on ASP.Net and Dynamic Data in .Net 4.0

Microsoft ASP.NET 4.0 Data Access: Patterns for Success with Web Forms

In here David shows off some of the new features in ASP.Net 4.0 including DomainService and DomainDataSource which are great because it gives you a layer in between your model and your site making the code in you site data provider neutral. Simply by replacing the DomainService you could change you data provider with out changing the code in you PageTemplates or FiledTemplates.

This is a really great session well worth the watch for ASP.Net classic and Dynamic Data.

it gets 5 starts from me!

   5stars

There were 6 start there before? Are you sure?

Monday, 16 March 2009

Adding Dynamic Data Previews to a New or Exiting Dynamic Data Website

What I want to do here is create a template website I can copy and reuse to build web sites based on the Preview. Hope this is of some help.

Create a new Dynamic Data Web Site:

ScreenShot248

Once created empty the DynamicData folder (keeping the folder) remove all sub folders.

Copy the contents of the following folders to the Dynamic Data Folder for:

Linq to SQL Web Sites

Linq to SQL folders

Entity Framework Web Sites

Entity Framework folders

Once you’ve copied the files

Copied folders

It’s time to do some editing of the files, we’ll use the Ctrl+Shift+H to do a search and replace of files in folders.

Setting up Look in folder

In the Find and Replace dialogue click the ellipsis (…) button at the end of the Look in: drop down list.

  1. Find the root folder of your website.
  2. Select the DynamicData folder in your site.
  3. Add to the Selected folders.

Find and Replace dialogue

Using the Search Patterns list below fill in the Find and Replace dialogue box:

  1. Search for pattern.
  2. Replace with pattern.
  3. Use regular expression.
  4. Enter the file type to search in.

Search patterns:

step Search for: Replace with: Use Regular expressions file types
1. namespace DynamicData(EF)*Project \{\n   yes *.ascx.cs
2. \}\n\} \} yes *.ascx.cs
3. Inherits="DynamicData(EF)*Project.   Yes *.ascx
4. CodeBehind=" CodeFile=" No *.ascx

In step 1 & 3 you may encounter any of the following name spaces to match and remove:

  1. DynamicDataProject.DynamicData.Filters {
  2. DynamicDataProject.DynamicData.EntityTemplates {
  3. DynamicDataProject.DynamicDataL2S.EntityTemplates {
  4. DynamicDataProject {
  5. DynamicDataEFProject {

I would attempt to remove them in the order shown above (4 & 5 are covered by the pattern namespace DynamicData(EF)*Project \{\n

 Finally Add a Reference to the Preview DLL’s

  1. Right click the root of the website, and choose “Add Reference…”
    Add reference to web site
  2. In the “Add Reference” dialogue browse to the Common Files folder in the location where you unzipped the Preview. Then select all the dll’s and click OK to add a reference to them in your web site.
    Add reference dialogue

Finally copy the:

File Copy to Folder
AutocompleteFilter.asmx.cs App_Code folder (create if you have not already).
AutocompleteFilter.asmx ~/ (root of website)
AjaxToolkitFixes.css ~/ (root of website)
AutocompleteStyle.css ~/ (root of website)
Site.css ~/ (root of website)
Site.master ~/ (root of website)
Site.master.cs ~/ (root of website)
web.config ~/ (root of website)

 

Similar editing is now required of these files see the Search Patterns above.

Edit the web.config and remove the contents of the connection strings node in the web.config

remove connection strings from web.config

Finally tweak the GridView  on the Default.aspx page.

<asp:GridView ID="Menu1" 
    runat="server" 
    AutoGenerateColumns="false" 
    CssClass="DDGridView"
    RowStyle-CssClass="td" 
    HeaderStyle-CssClass="th" 
    CellPadding="6">

See the BOLD ITALIC text this is to take advantage of the friendlier CSS now supplied with the Preview it won’t stamp on other CSS you may want to use in your web site as the CSS in the .Net 3.5 SP1 version does.

Now you have a sample that you can use over an over like a template

Your next step is the same as if you had just done file new “Dynamic Data Web Site” but I would copy to a new folder first so I can use this as a template each time I want a Preview DD website.

Happy coding

I’m a an MVP and I’m a PC

Friday, 13 February 2009

AJAX History in a Dynamic Data Website

If you want to know a little more about AJAX History have a look at this video on MSDN Screencasts (used to be MSDN .Net Nuggets) Managing Browser History with ASP.NET AJAX and the ASP.NET 3.5 Extensions Preview it is slightly out of date in that some of the properties have been renamed but its essentially correct and a good foundation for AJAX History’s use. There is a second article Managing Browser History on the Client with ASP.NET AJAX and the ASP.NET 3.5 Extensions Preview which may be of interest for some.

The Problem

To appreciate the solution you need to understand the problem. So here’s what happens you have a standard ASP.Net Dynamic Data V1 website with several filters on each list page, there are two things that will irritate your users to do with AJAX partial post back:

1. You make several selections and click back you will go to the previous page not the last selection (try it with and without partial render enabled in site.master EnablePartialRendering="true" see Listing 1).

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title>Dynamic Data Site</title>
    <link href="~/Site.css" rel="stylesheet" type="text/css" />
</head>
<body class="template">
    <h1><span class="allcaps">Dynamic Data Site</span></h1>
    <div class="back">
        <a runat="server" href="~/"><img alt="Back to home page" runat="server" src="DynamicData/Content/Images/back.gif" />Back to home page</a>
    </div>

    <form id="form1" runat="server">
    <div>
        <asp:ScriptManager ID="ScriptManager1" runat="server" EnablePartialRendering="true"/>
        <asp:ContentPlaceHolder id="ContentPlaceHolder1" runat="server">
        </asp:ContentPlaceHolder>
    </div>
    </form>
</body>
</html>

Listing 1 – the default Site.master

2. You click a link from a List page on which you have made several filter selections view the sub page and then click back you reach the page with no filtering applied.

Both of these issues can be really irritating for the user, because A it’s not the expected behaviour and B it’s a pain to have to redo all those filters (after all users are really lazy smile_omg)

Have you watched the video? well if you have no prior knowledge of AJAX History I strongly suggest you have a look at the video after all it's only 13mins 27secs long.

The Solution

First of all this is only my first attempt at this I think once I get into the Dynamic Data V2.0 Preview/Release I will redo this as a class that inherits from UserFilterControlBase to easily give this functionality in filter templates.

<asp:ScriptManager 
    ID="ScriptManager1" 
    runat="server" 
    EnableHistory="true"
    EnableSecureHistoryState="false"
    EnablePartialRendering="true"/>

Listing 2 – the ScriptManager on Site.master alterations

As you can see from the Listing 2 enabling AJAX History is quite easy you just need to add the EnableHistory="true" property to the control. The other property is to disable encryption of the history information that is added to the URL EnableSecureHistoryState="false" this is one of the methods that have been renamed for the RTM version.

Now we need to edit the ~/DynamicData/Content/FilterUserControl.ascx.cs file see Listing 3 for the code all alteration are in BOLD italic.

using System;
using System.Collections.Specialized;
using System.Linq;
using System.Web.UI;
using System.Web.UI.WebControls;

public partial class FilterUserControl : System.Web.DynamicData.FilterUserControlBase
{
    // global variable for holding the History Point value
    private String HistoryPointValue;

    public event EventHandler SelectedIndexChanged
    {
        add
        {
            DropDownList1.SelectedIndexChanged += value;
        }
        remove
        {
            DropDownList1.SelectedIndexChanged -= value;
        }
    }

    public override string SelectedValue
    {
        get
        {
            return DropDownList1.SelectedValue;
        }
    }

    protected void Page_Init(object sender, EventArgs e)
    {
        if (!Page.IsPostBack)
        {
            PopulateListControl(DropDownList1);

            // Set the initial value if there is one
            if (!String.IsNullOrEmpty(InitialValue))
                DropDownList1.SelectedValue = InitialValue;
        }

        // add event handler to capture history point.
        this.SelectedIndexChanged += ThisSelectedIndexChanged;

        // Add OnNavigate handler to restore History points.
        var scriptManager = ScriptManager.GetCurrent(Page);
        if (scriptManager != null)
            scriptManager.Navigate += ScriptManagerOnNavigate;
    }

    protected void ThisSelectedIndexChanged(object sender, EventArgs e)
    {
        var filterRepeater = this.GetParentFilterRepeater();
        if (filterRepeater != null)
        {
            // add History point for the curret state of
            // all filters so we can restore them later.
            var scriptManager = ScriptManager.GetCurrent(Page);
            if (scriptManager != null && scriptManager.IsInAsyncPostBack)
            {
                var nvcHistory = new NameValueCollection();

                var filterControls = filterRepeater.GetFilterControls();
                if (filterControls.Count() > 0)
                {
                    foreach (var filter in filterControls)
                    {
                        var f = filter as FilterUserControl;
                        if (f != null)
                            nvcHistory.Add(f.DataField, f.SelectedValue);
                    }
                    // if we get some filter states add the history point.
                    if (nvcHistory.Count > 0)
                        scriptManager.AddHistoryPoint(nvcHistory, Page.Title);
                }
            }
        }
    }

    protected void ScriptManagerOnNavigate(object sender, HistoryEventArgs e)
    {
        HistoryPointValue = e.State[DataField];
        if (e.State[DataField] == null || e.State[DataField] == "")
            DropDownList1.SelectedIndex = 0;
        else if(HistoryPointValue != null)
        {
            // check if history point is in the DDL and set History Point value
            ListItem item = DropDownList1.Items.FindByValue(HistoryPointValue);
            if (item != null)
                DropDownList1.SelectedValue = HistoryPointValue;
        }
    }
}

Listing 3 – FilterUserControl.ascx.cs file

And finally here are the extension methods use in Listing 3.

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

/// <summary>
/// Summary description for ExtensionMethods
/// </summary>
public static class ExtensionMethods
{
    /// <summary>
    /// Get the parent FilterRepeater of the control it's used on.
    /// </summary>
    /// <param name="control">The control to find the parent FilterRepeater of.</param>
    /// <returns>The parent FilterRepeater.</returns>
    public static FilterRepeater GetParentFilterRepeater(this Control control)
    {
        var parentControl = control.Parent;
        while (parentControl != null)
        {
            var FilterRepeater = parentControl as FilterRepeater;
            if (FilterRepeater != null)
                return FilterRepeater;
            else
                parentControl = parentControl.Parent;
        }
        return null;
    }

    /// <summary>
    /// Get a List of FilterUserControlBase filters from the FilterRepeater.
    /// </summary>
    /// <param name="filterRepeater">The FilterRepeater to get a List of filter from</param>
    /// <returns>returns a List of type FilterUserControlBase.</returns>
    public static IEnumerable<UserControl> GetFilterControls(this FilterRepeater filterRepeater)
    {
        var filters = new List<UserControl>();

        foreach (RepeaterItem item in filterRepeater.Items)
        {
            var filter = item.Controls.OfType<UserControl>().FirstOrDefault();
            filters.Add(filter);
        }

        return filters.AsEnumerable();
    }
}

Listing 4 – Extension methods

Now when you skip back and forth through pages you should find it works just like you and your users were expecting.

Wednesday, 28 January 2009

Making Individual Tables Read Only – Dynamic Data

These are two thing you can do to make your table Read Only.

  1. Add an attribute for instance the ReadOnlyAttribute or your own custom attribute
  2. Using Routing to restrict access to the Edit or Insert pages

The first solution I covered in this article Writing Attributes and Extension Methods for Dynamic Data so I’ll just show the Routing example here.

// Setting a table to Read only
var visibleTables = from t in MetaModel.Default.Tables
                    where t.Scaffold == true
                    select t;

var readOnlyTables = new StringBuilder();
foreach (var table in visibleTables)
{
    var isReadOnly = table.Attributes.OfType<ReadOnlyAttribute>().
DefaultIfEmpty(new ReadOnlyAttribute(false)).
FirstOrDefault();
    if (isReadOnly.IsReadOnly)
        readOnlyTables.Append(table.Name + "|");
}

routes.Add(new DynamicDataRoute("{table}/{action}.aspx")
{
    Constraints = new RouteValueDictionary(new
    {
        action = "Edit|Insert",
        table = readOnlyTables.ToString().Substring(0, readOnlyTables.Length - 1)
    }),
    ViewName = "Details",
    Model = model
});

Listing 1 – Routing example.

[ReadOnly(true)]
public partial class Employee {}

Listing 2 – sample attribute.

In Listing 1 we see three things going on:

  1. We get a list of visible tables using the Scaffold attribute to find out if the table should be shown.
  2. Next we loop through the visible tables and if the table is set to ReadOnly then we add it to the readOnlyTables StringBuilder to build a list of read only tables (e.g. “Products|Customers”).
  3. Build a route that constrains the these tables to only get to the Details page.
Note: That this route must appear before the default routes.

Happy coding and remember your a PC HappyWizard but for us coders I prefer the short lived “Live to Code and Code to Live”

Important: The only caveat here is that the Delete button still works. :( But you could catch that in Business logic if you didn’t want to edit each page.

Monday, 26 January 2009

Disallow Navigation on ForeignKey FieldTemplate – Dynamic Data

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

[AttributeUsage(AttributeTargets.Property)]
public class AllowNavigationAttribute : Attribute
{
    public Boolean Show { get; private set; }

    public AllowNavigationAttribute(Boolean show)
    {
        Show = show;
    }
    // this will allow us to have a default set to false
    public static AllowNavigationAttribute Default = new 
AllowNavigationAttribute
(true); }

Listing 1 – AllowNavigation attribute

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

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.

protected string GetNavigateUrl()
{
    var allow = Column.Attributes.OfType<AllowNavigationAttribute>()
                .DefaultIfEmpty(new AllowNavigationAttribute(true))
                .FirstOrDefault().Show;
    if (!AllowNavigation || !allow)
    {
        return null;
    }

    if (String.IsNullOrEmpty(NavigateUrl))
    {
        return ForeignKeyPath;
    }
    else
    {
        return BuildForeignKeyPath(NavigateUrl);
    }
}

Listing 2 – ForeignKey FieldTemplate modifications

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

[MetadataType(typeof(ProductMD))]
public partial class Product
{
    public partial class ProductMD
    {
       //EntityRef
        [AllowNavigation(false)]
        public object Category {get;set;}
    }
}

Listing 3 – sample Metadata

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

Happy Coding and remember You’re a PC HappyWizard

Saturday, 24 January 2009

Setting the Initial Sort Order – Dynamic Data

This is just a quick note on a post I made on the Dynamic Data Forum to answer a question where setting the DisplayColumnAttribute should set the default sort order of the GridView on the List page but didn’t. Here’s how I solved it with a hint or two from Marcin Dobosz.

[MetadataType(typeof(Orders_Metadata ))]
[DisplayColumn("OrderID", "OrderID", true)]
public partial class Orders
{
    public class Orders_Metadata
    {
... }
}

Listing 1 – Example Metadata

Assuming you have a table with the DisplayColumnAttribute set you could put this on your List page: 

var displayColumn = table.GetAttribute<DisplayColumnAttribute>();
if (displayColumn != null && displayColumn.SortColumn != null)
{
    GridView1.Sort(displayColumn.SortColumn,
        displayColumn.SortDescending ? SortDirection.Descending : SortDirection.Ascending);
}

Listing 2 – Edit the Page_Load event handler and add the following code

public static T GetAttribute<T>(this MetaTable table) where T : Attribute
{
    return table.Attributes.OfType<T>().FirstOrDefault();
}

Listing 3 – you will also need this extension method see Writing Attributes and Extension Methods for Dynamic Data

Add your DisplayColumnAttribute (see Listing 1) to the table you want sorted (note: it must have the second string constant even if it’s the same name, as the second string it the one that the sort is done on, and the third value if true causes the sort to be descending). Then in Listing 2 you get the attribute using the extension method from Listing 3 and apply the relevant sort.

This is here mainly so I can find it again! HappyWizard

Friday, 23 January 2009

Making a Field Read-Only via the ReadOnlyAttribute – Dynamic Data

This article is as a result of this thread not editable attribute on the Dynamic Data Forum:

What you need to do is:

  1. Use the ReadOnlyAttribute in System.ComponentModel and attribute up the column you want as read only in edit mode

  2. Create an class that implements IAutoFieldGenrator.

  3. A custom DynamicField.

And here's the sample:

using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Web.DynamicData;
using System.Web.UI;
using System.Web.UI.WebControls;

public class FieldsManager : IAutoFieldGenerator
{
    protected MetaTable _table;

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

    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)
                continue;

            DynamicField f;

            // here we check to dee
            if (column.Attributes.OfType<ReadOnlyAttribute>().
DefaultIfEmpty(new ReadOnlyAttribute(false)).
FirstOrDefault().IsReadOnly) f = new DynamicReadonlyField(); else f = new DynamicField(); f.DataField = column.Name; oFields.Add(f); } return oFields; } } // special thanks to david Ebbo for this public class DynamicReadonlyField : DynamicField { public override void InitializeCell( DataControlFieldCell cell, DataControlCellType cellType, DataControlRowState rowState, int rowIndex) { if (cellType == DataControlCellType.DataCell) { var control = new DynamicControl() { DataField = DataField }; // Copy various properties into the control control.UIHint = UIHint; control.HtmlEncode = HtmlEncode; control.NullDisplayText = NullDisplayText; // this the default for DynamicControl and has to be // manually changed you do not need this line of code // its there just to remind us what we are doing. control.Mode = DataBoundControlMode.ReadOnly; cell.Controls.Add(control); } else { base.InitializeCell(cell, cellType, rowState, rowIndex); } } }

And in the Edit page add:

protected void Page_Init(object sender, EventArgs e)
{
    DynamicDataManager1.RegisterControl(DetailsView1);
    table = DetailsDataSource.GetTable();
    DetailsView1.RowsGenerator = new FieldsManager(table);
}

In the Page_Init add the BOLD ITALIC lines

This is a direct copy of the answer I gave in the thread HappyWizard