Monday, 29 September 2008

Dynamic Data Custom PageTemplates - A variation of Part 1 with the Details and SubGrid in Tabs

These articles are now under the title of Custom PageTemplates:

  • Custom PageTemplates Part 1 - Custom PageTemplates with Ajax Control Toolkit Tabs
  • Custom PageTemplates Part 2 - A variation of Part 1 with the Details and SubGrid in Tabs
  • Custom PageTemplates Part 3 - Dynamic/Templated Grid with Insert (Using ListView)
  • Custom PageTemplates Part 4 - Dynamic/Templated FromView
  • This article is the same as Part 1 in that it’s all the same code but with just a few changes. Here I’m going to include the DetailsView from Part 1 as the first tab of the tabbed sub grids.

    The main changes are:

    • The Details and the SubGrids are all generated in the ITemplate class DetailsSubGridsTemplate
    • A new PageTemplate TabbedEditWithSubGrids.aspx now only has a FormView
    • New FieldTemplates have been added, TableDetail.ascx and TableForm.ascx
    • A new Template folder structure has been added to separate all the different template types

    The Design

    I thought I'd explain what I wanted to achieve and then show the code, so here goes.

    What I wanted was the ability to have a compact form for dealing with related entities (i.e. a customer with all it’s related entities Orders, Addresses, Contacts etc) but I also wanted the main Form/Details to be customisable change the column and also be able to change the layout (to which end I’ve added two new FieldTemplates TableDetail.ascx and TableForm.ascx. TableDetail.ascx is just what you’d expect and is the same in appearance as the Edit.aspx page, TableForm.ascx on the other hand is fully customisable via templates (User Controls as mentioned in the previous article Part 1).

    So we should end up with something like this:

    TableDetail.ascx TableForm.ascx
    Figure 1 - TableDetail.ascx Figure 2 - TableForm.ascx

    As you can see Figure 1 is based on the standard Edit.aspx page template and Figure 2 is based on a custom template defined for each table. At runtime the ITemplate class DetailsSubGridsTemplate checks to see if a template exists for the current table and then gives the DynamicControl a UIHint for either TableDetail of TableForm.

    Hope that makes sense.

    The Implementation of ITemplate

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Web;
    using System.Web.UI;
    using System.Web.DynamicData;
    using System.Web.UI.WebControls;
    using AjaxControlToolkit;
    using System.IO;
    
    /// <summary>
    /// Creates an item template that renders any children columns in the passed in table as GridViews
    /// </summary>
    public class DetailsSubGridsTemplate : ITemplate
    {
        private MetaTable _table;
        private Page _page;
    
        public DetailsSubGridsTemplate(MetaTable table, Page page)
        {
            _table = table;
            _page = page;
        }
    
        public void InstantiateIn(Control container)
        {
            IParserAccessor acessor = container;
            // get all the children columns
            var subGridTables = from c in _table.Columns.OfType<MetaChildrenColumn>()
                                select new SubDetails()
                                {
                                    Column = c,
                                    SubGridMetaData = c.Attributes.OfType<SubGridViewsAttribute>().FirstOrDefault(),
                                    Order = c.Attributes.OfType<SubGridViewsAttribute>().FirstOrDefault() != null
                                    && c.Attributes.OfType<SubGridViewsAttribute>().FirstOrDefault().Order > 0
                                    ? c.Attributes.OfType<SubGridViewsAttribute>().FirstOrDefault().Order
                                    : int.MaxValue,
                                };
    
            // sort the according to Order first and column name second
            // note if SubGridViewsAttribute is not allied or the attrivute
            // has no value for Order then just sort but column name
            subGridTables = from sg in subGridTables
                            orderby sg.Order, sg.Column.Name
                            select sg;
    
            // make sure there are some children columns
            if (subGridTables.Count() > 0)
            {
                // create tab container to hold each children column
                var tabContainer = new TabContainer();
                tabContainer.ID = "tabContainer";
    
                // enable auto poastback so selected tab is remembered
                //tabContainer.AutoPostBack = true;
                tabContainer.EnableViewState = true;
    
                // add the tab container to the page
                acessor.AddParsedSubObject(tabContainer);
    
                // Add DetailsView =======================================================================
                // add tab pannel to hold the parent table
                var tabPanelParent = new TabPanel();
                tabPanelParent.ID = "tpParent" + _table.Name;
    
                // add the tab panel
                tabContainer.Tabs.Add(tabPanelParent);
    
                // set the tab header maybe later add an attribute?
                tabPanelParent.HeaderText = _table.DisplayName;
    
                //Instantiate a DynamicControl for this Children Column
                var parentDetails = new DynamicControl(DataBoundControlMode.Edit)
                {
                    ID = "tpParent",
    
                    // set data field to the first column name
                    // any column will do not used
                    DataField = _table.Columns[0].Name
                };
    
                // if a template exists then use the TableForm control
                String itemTemplate = _table.Model.DynamicDataFolderVirtualPath + "Templates/FormViewEdit/" + _table.Name + ".ascx";
                String path = _page.Server.MapPath(itemTemplate);
                if (File.Exists(path))
                    parentDetails.UIHint = "TableForm";
                else
                    // else use TableDetail
                    parentDetails.UIHint = "TableDetail";
    
                // add the DynamicControl to the tab panel
                tabPanelParent.Controls.Add(parentDetails);
                // End DetailsView =======================================================================
    
                // add a tab panel for each children table
                foreach (SubDetails SubGridDetails in subGridTables)
                {
                    var tabPanel = new AjaxControlToolkit.TabPanel();
                    tabPanel.ID = "tp" + SubGridDetails.Column.Name;
    
                    // add the tab panel
                    tabContainer.Tabs.Add(tabPanel);
    
                    var subGridAttributes = SubGridDetails.Column.Attributes.OfType<SubGridViewsAttribute>().SingleOrDefault();
                    // set the Tab's name to be the tables display name 
                    // or table Name if no attribute is present
                    if (subGridAttributes != null && subGridAttributes.TabName.Length > 0)
                        tabPanel.HeaderText = subGridAttributes.TabName;
                    else
                        tabPanel.HeaderText = SubGridDetails.Column.ChildTable.DisplayName;
    
                    //Instantiate a DynamicControl for this Children Column
                    var childrenGrid = new DynamicControl(DataBoundControlMode.Edit)
                    {
                        ID = SubGridDetails.Column.Name,
    
                        // set UIHint
                        UIHint = "ChildrenGrid",
    
                        // set data field to column name
                        DataField = SubGridDetails.Column.Name
                    };
    
                    // add the DynamicControl to the tab panel
                    tabPanel.Controls.Add(childrenGrid);
                }
                // set the tab pannels index to 0 which
                // forces the first tab to be selected
                if (!_page.IsPostBack)
                    tabContainer.ActiveTabIndex = 0;
            }
            else
            {
                // Add DetailsView =======================================================================
                //Instantiate a DynamicControl for this Children Column
                var parentDetails = new DynamicControl(DataBoundControlMode.Edit)
                {
                    ID = "tpParent",
    
                    // set data field to the first column name
                    // any column will do not used
                    DataField = _table.Columns[0].Name
                };
    
                // if a template exists then use the TableForm control
                String itemTemplate = _table.Model.DynamicDataFolderVirtualPath + "Templates/FormViewEdit/" + _table.Name + ".ascx";
                String path = _page.Server.MapPath(itemTemplate);
                if (File.Exists(path))
                    parentDetails.UIHint = "TableForm";
                else
                    // else use TableDetail
                    parentDetails.UIHint = "TableDetail";
    
                // add the DynamicControl to the tab panel
                acessor.AddParsedSubObject(parentDetails);
                // End DetailsView =======================================================================
    
                // if no children columns
                // add label to show no grids
                var label = new Label();
                label.Text = "There are no SubGrids";
                label.CssClass = "droplist";
    
                // add the label to the page
                acessor.AddParsedSubObject(label);
            }
        }
    
        private class SubDetails
        {
            /// <summary>
            /// Column to display
            /// </summary>
            public MetaChildrenColumn Column { get; set; }
    
            /// <summary>
            /// MetaData if any from the original column
            /// </summary>
            public SubGridViewsAttribute SubGridMetaData { get; set; }
    
            /// <summary>
            /// Holds the sort order value
            /// </summary>
            public int Order { get; set; }
        }
    }

    Listing 1 – DetailsSubGridsTemplate ITemplate class

    As you can see the bulk of the class is the same as the ITemplate class from the previous article (Part 6) the two main changes are, one is the tests for the number of records in the DetailsSubGridsTemplate, this is because there are always tabs if there is one or more children tables. And the second change is the addition of the code to add the Details/Form of the parent table to the first tab or if not children tables are present to the page.

    Field Templates

    <%@ Control 
        Language="C#" 
        CodeFile="TableDetail.ascx.cs" 
        Inherits="TableDetailField" %>
    
    <asp:DynamicDataManager 
        ID="DynamicDataManager1" 
        runat="server" 
        AutoLoadForeignKeys="true" />
        
    <asp:ValidationSummary ID="ValidationSummary1" 
        D="ValidationSummary1" 
        runat="server" 
        EnableClientScript="true"
        HeaderText="List of validation errors" />
        
    <asp:DynamicValidator 
        runat="server" 
        ID="DetailsViewValidator" 
        ControlToValidate="DetailsView1"
        Display="None" />
        
    <asp:DetailsView 
        ID="DetailsView1" 
        runat="server" 
        DataSourceID="DetailsDataSource" 
        DefaultMode="Edit"
        AutoGenerateEditButton="True" 
        OnItemCommand="DetailsView1_ItemCommand" 
        OnItemUpdated="DetailsView1_ItemUpdated"
        CssClass="detailstable" 
        FieldHeaderStyle-CssClass="bold">
    </asp:DetailsView>
    
    <asp:LinqDataSource 
        ID="DetailsDataSource" 
        runat="server" 
        EnableUpdate="true">
        <WhereParameters>
            <asp:DynamicQueryStringParameter />
        </WhereParameters>
    </asp:LinqDataSource>

    Listing 2 - TableDetail.ascx

    using System;
    using System.Linq;
    using System.Web.DynamicData;
    using System.Web.UI.WebControls;
    
    public partial class TableDetailField : FieldTemplateUserControl
    {
        protected MetaTable table;
    
        public String[] DisplayColumns { get; set; }
    
        protected void Page_Init(object sender, EventArgs e)
        {
            DynamicDataManager1.RegisterControl(DetailsView1);
            table = DetailsDataSource.GetTable();
    
            var attribute = Table.Attributes.OfType<ShowColumnsAttribute>().SingleOrDefault();
    
            if (attribute != null)
                DisplayColumns = attribute.DisplayColumns;
    
            DetailsView1.RowsGenerator = new FieldTemplateRowGenerator(table, DisplayColumns);
        }
    
        protected void DetailsView1_ItemCommand(object sender, DetailsViewCommandEventArgs e)
        {
            if (e.CommandName == DataControlCommands.CancelCommandName)
            {
                Response.Redirect(table.ListActionPath);
            }
        }
    
        protected void DetailsView1_ItemUpdated(object sender, DetailsViewUpdatedEventArgs e)
        {
            if (e.Exception == null || e.ExceptionHandled)
            {
                Response.Redirect(table.ListActionPath);
            }
        }
    }

    Listing 3 - TableDetail.ascx.cs

    Listings 2 and 3 comprise the TableDetail FieltTempalte which uses the DisplayColumns property of the ShowColumnsAttribute to determin which columns to show.

    <%@ Control 
        Language="C#" 
        CodeFile="TableForm.ascx.cs" 
        Inherits="TableDetailField" %>
    
    <asp:DynamicDataManager 
        ID="DynamicDataManager1" 
        runat="server" 
        AutoLoadForeignKeys="true" />
        
    <asp:ValidationSummary 
        ID="ValidationSummary1" 
        runat="server" 
        EnableClientScript="true"
        HeaderText="List of validation errors" />
        
    <asp:DynamicValidator 
        runat="server" 
        ID="DetailsViewValidator" 
        ControlToValidate="FormView1"
        Display="None" />
    
    <asp:FormView 
        ID="FormView1" 
        DefaultMode="Edit" 
        DataSourceID="FormDataSource"
        OnItemCommand="Edit_ItemCommand" 
        OnItemUpdated="Edit_ItemUpdated" 
        runat="server">
        <ItemTemplate>
        </ItemTemplate>
        <EditItemTemplate>
        </EditItemTemplate>
    </asp:FormView>
    
    <asp:LinqDataSource 
        ID="FormDataSource" 
        runat="server" 
        EnableDelete="true">
    </asp:LinqDataSource>

    Listing 4 – TableForm.ascx

    using System;
    using System.IO;
    using System.Linq;
    using System.Web.DynamicData;
    using System.Web.UI.WebControls;
    
    public partial class TableDetailField : FieldTemplateUserControl
    {
        protected MetaTable table;
    
        public String[] DisplayColumns { get; set; }
    
        protected void Page_Init(object sender, EventArgs e)
        {
            DynamicDataManager1.RegisterControl(FormView1);
    
            table = FormDataSource.GetTable();
    
            var attribute = Column.Attributes.OfType<ShowColumnsAttribute>().SingleOrDefault();
    
            if (attribute != null)
                DisplayColumns = attribute.DisplayColumns;
    
            // load item template
            table = FormDataSource.GetTable();
            String itemTemplate = table.Model.DynamicDataFolderVirtualPath + "Templates/FormViewEdit/" + table.Name + ".ascx";
            if (File.Exists(Server.MapPath(itemTemplate)))
            {
                FormView1.EditItemTemplate = LoadTemplate(itemTemplate);
            }
            else
            {
                throw new InvalidOperationException("The TableForm FieldTemplate requires an EditItemTamplate in the " + table.Model.DynamicDataFolderVirtualPath + "Template/FormViewEdit folder");
            }
        }
    
        protected void Edit_ItemUpdated(object sender, FormViewUpdatedEventArgs e)
        {
            if ((e.Exception == null))
            {
                Edit_RedirectToList();
            }
        }
    
        protected void Edit_ItemCommand(object sender, FormViewCommandEventArgs e)
        {
            if ((e.CommandName == DataControlCommands.CancelCommandName))
            {
                Edit_RedirectToList();
            }
        }
    
        protected void Edit_RedirectToList()
        {
            MetaTable table = FormDataSource.GetTable();
            String returnUrl = Request.QueryString["returnUrl"];
    
            if ((returnUrl == null))
            {
                Response.Redirect(table.ListActionPath);
            }
            else
            {
                Response.Redirect(returnUrl);
            }
        }
    }

    Listing 5 – TableForm.ascx.cs

    Listings 4 and 5 makeup the TableForm FieldTemplate which requires a template in the ~/DynamicData/Template/FormViewEdit folder otherwise it throws an InvalidOperationException.

    Both the above FieldTemplates only require access to the MetaTable table to generate their output. The most notable feature is that they are both configured using the DynamicDataManager just like a PageTemplate.

    A couple of TableForm template examples

    <%@ Control 
        Language="C#" 
        AutoEventWireup="true" 
        CodeFile="Employees.ascx.cs" 
        Inherits="Employees" %>
        
    <div style="width: 400px;">
        <p class="droplist" style="border: dotted 1px cyan;">
            <span style="display:inline-block; height: 16px; text-align: right; vertical-align: middle; width: 80px;">
                Title:&nbsp;
            </span>
            <span style="height: 16px; text-align: left; vertical-align: middle;">
                <asp:DynamicControl 
                    ID="Title" 
                    DataField="Title" 
                    Mode="Edit" 
                    runat="server">
                </asp:DynamicControl>
            </span>
        </p>
        <p class="droplist" style="border: dotted 1px cyan;">
            <span style="display:inline-block; height: 16px; text-align: right; vertical-align: middle; width: 80px;">
                First Name:&nbsp;
            </span>
            <span style="height: 16px; text-align: left; vertical-align: middle;">
                <asp:DynamicControl 
                    ID="FirstName" 
                    DataField="FirstName" 
                    Mode="Edit" 
                    runat="server">
                </asp:DynamicControl>
            </span>
        </p>
        <p class="droplist" style="border: dotted 1px cyan;">
            <span style="display:inline-block; height: 16px; text-align: right; vertical-align: middle; width: 80px;">
                Last Name:&nbsp;
            </span>
            <span style="height: 16px; text-align: left; vertical-align: middle;">
                <asp:DynamicControl 
                    ID="LastName" 
                    DataField="LastName" 
                    Mode="Edit" 
                    runat="server">
                </asp:DynamicControl>
            </span>
        </p>
        <div class="droplist">
        <asp:LinkButton 
            ID="UpdateLinkButton" 
            CommandName="Update" 
            runat="server">
            Update
        </asp:LinkButton>&nbsp;
        <asp:LinkButton 
            ID="CancelLinkButton" 
            CausesValidation="false" 
            CommandName="Cancel" 
            runat="server">
            Cancel
        </asp:LinkButton>
        </div>
    </div>

    Listing 6 – Example 1 of a template for the Employees table

    <%@ Control 
        Language="C#" 
        AutoEventWireup="true" 
        CodeFile="Copy of Employees.ascx.cs" 
        Inherits="Employees" %>
    
    <table class="detailstable">
        <tr>
            <th>Title</th>
            <td><asp:DynamicControl 
                    ID="Title" 
                    DataField="Title" 
                    Mode="Edit" 
                    runat="server">
                </asp:DynamicControl></td>
        </tr>
        <tr>
            <th>First Name</th>
            <td><asp:DynamicControl 
                    ID="FirstName" 
                    DataField="FirstName" 
                    Mode="Edit" 
                    runat="server">
                </asp:DynamicControl></td>
        </tr>
        <tr>
            <th>Last Name</th>
            <td><asp:DynamicControl 
                ID="LastName" 
                DataField="LastName" 
                Mode="Edit" 
                runat="server">
            </asp:DynamicControl></td>
        </tr>
        <tr>
            <td colspan="2">
                <asp:LinkButton 
                    ID="UpdateLinkButton" 
                    CommandName="Update" 
                    runat="server">
                    Update
                </asp:LinkButton>
                <asp:LinkButton 
                    ID="CancelLinkButton" 
                    CausesValidation="false" 
                    CommandName="Cancel" 
                    runat="server">
                    Cancel
                </asp:LinkButton>
            </td>
        </tr>
    </table>
    

    Listing 7 – Example 2 of a template for the Employees table

    Listing 6 and 7 are the FieldTemplates used in Figures 1 and 2.

    Download Project

    I know there are no big explanations here but Part 6 should cover most things passed over here.

    Part 8 will cover Dynamic/Templated Grid/Details with Insert (Using ListView), this will replace the List PageTemplate and allow insert in the grid but also allow you to supply a template for each mode; Item, Edit, Insert, Footer and Header.

    Back soon smile_teeth

    Thursday, 25 September 2008

    Dynamic Data Custom PageTemplates with Ajax Control Toolkit Tabs UPDATED: 2008/09/27

    These articles are now under the title of Custom PageTemplates:

    Custom PageTemplates Part 1 - Custom PageTemplates with Ajax Control Toolkit Tabs Custom PageTemplates Part 2 - A variation of Part 1 with the Details and SubGrid in Tabs Custom PageTemplates Part 3 - Dynamic/Templated Grid with Insert (Using ListView) Custom PageTemplates Part 4 - Dynamic/Templated FromView

    I’ve been working for a little while now to get a generic page with a DetailsView showing the parent record and a set of tabs showing all the child records similar to Figure 1.

    A Detail/Edit page with all ChildrenColumn as sub grids in a tab control 

    Figure 1 – A Detail/Edit page with all ChildrenColumn as sub grids in a tab control

    I wanted something that was easy to implement on your own custom page and reused my existing FieldTemplate ParentGrid, I tried several methods to achieve this and I’m going to show the two most successful methods.

    Both methods use a FormView as the control to embed the tabs and grids:

    • Creating an external ItemTemplate and load it into the FormView at runtime
    • Have a dynamic ItemTemplate that implements ITemplate.

    Making the Custom Page with a FormView

    For this sample I’m going to make a custom generic Edit page, so first we need to make a copy of the Edit.aspx page and rename it to EditSubGridViews.aspx rename the class as well to EditSubGridViews in both the aspx file and the code behind.

    <asp:UpdatePanel ID="UpdatePanel2" runat="server">
        <ContentTemplate>
        
            <h3>Sub GridViews</h3>
            
            <asp:FormView 
                ID="FormView1" 
                runat="server" 
                DataSourceID="DetailsDataSource">
                <ItemTemplate>
                </ItemTemplate>
            </asp:FormView>
            
        </ContentTemplate>
    </asp:UpdatePanel>

    Listing 1 – The added the FormView and UpdatePanel

    Add Listing 1’s code after the end of the current UpdatePanel.

    Note: The DataSourceID="DetailsDataSource" is set to the same data source as the DetailsView1

    In the code behind add the following line to the Page_Load event handler.

    DynamicDataManager1.RegisterControl(FormView1);

    This just registers the FormView with the DynamicDataManager no real magic here.

    // load item template
    table = DetailsDataSource.GetTable();
    String itemTemplate = table.Model.DynamicDataFolderVirtualPath + "Templates/" + table.Name + ".ascx";
    if (File.Exists(Server.MapPath(itemTemplate)))
        FormView1.ItemTemplate = LoadTemplate(itemTemplate);
    // note if no template is loaded then the FormView will show nothing
    

    Listing 2 – code to load the ItemTemplate

    This is the magic, you can create your own custom template as a UserControl to include just the ChildrenTables you want. Here we first of all get a reference to the table so that we reference the Model.DynamicDataFolderVirtualPath (which hold the virtual path to the Dynamic Data folder normally ~/DynamicData/) to build the path to our template. And if the template exists then load it into the FormView’s ItemTemplate. Obviously you could load any of the other FormView templates this way.

    Modifying the Route for Edit

    To use our new PageTemplate EditSubGridViews we could just have edited the current Edit page, but I thought it would be good to leave that as is and change the route.

    // The following statement supports separate-page mode, where the List, Detail, Insert, and 
    // Update tasks are performed by using separate pages. To enable this mode, uncomment the following 
    // route definition, and comment out the route definitions in the combined-page mode section that follows.
    routes.Add(new DynamicDataRoute("{table}/{action}.aspx") {
        Constraints = new RouteValueDictionary(new { action = "List|Details|Insert" }),
        Model = model
    });
    
    // add a route to EditSubGridViews
    routes.Add(new DynamicDataRoute("{table}/EditSubGridViews.aspx")
    {
        Action = PageAction.Edit,
        ViewName = "EditSubGridViews",
        Model = model
    });

    Listing 3 – Route to EditSubGridViews.aspx added to Global.asax 

    In the default root in the Global.asax file remove Edit from action = "List|Details|Edit|Insert" and add the route to EditSubGridViews from Listing 3.

    Create the FormView ItemTemplate

    To create a template file for FormView all you need to do is create a new UserControl in the folder. The first thing to do is create the Templates folder under the DynamicData folder.

    To create the folder from Visual Studio 2008 right click the DynamicData folder and click New Folder

    Select New Folder from the context menu

    Figure 2 – Select New Folder from the context menu

    Now right click the new folder and click Add New Item…

    Add New Item

    Figure 3 – Add New Item

    From the Add New Item dialogue box choose Web User Control and name it Employees.

    Create the Web User Control

    Figure 4 – Create the Web User Control

    Templates folder under the DynamicData folder with the Employees template

    Figure 5 – Templates folder under the DynamicData folder with the Employees template

    Add the following mark-up to the new template:

    <ajaxToolkit:TabContainer ID="TabContainer1" runat="server">
        <ajaxToolkit:TabPanel ID="TabPanel1" HeaderText="My Employees" runat="server">
            <ContentTemplate>
                <asp:DynamicControl ID="DynamicControl1" DataField="Employees" UIHint="ChildrenGrid" Mode="Edit" runat="server">
                </asp:DynamicControl>
            </ContentTemplate>
        </ajaxToolkit:TabPanel>
        <ajaxToolkit:TabPanel ID="TabPanel3" HeaderText="My Orders" runat="server">
            <ContentTemplate>
                <asp:DynamicControl ID="DynamicControl3" DataField="Orders" UIHint="ChildrenGrid" Mode="Edit" runat="server">
                </asp:DynamicControl>
            </ContentTemplate>
        </ajaxToolkit:TabPanel>
    </ajaxToolkit:TabContainer>
    

    Listing 4 – ItemTemplate showing Employees and Orders ChildrenGrids

    Note: See this post series Dynamic Data and Advanced Field Templates for the ChildrenGrid details.

    The important setting here in the DynamicControls are UIHint and DataField, (ChildrenGrid is a FieldTemplate from a previous series of articles on FieldTemplates on my blog) in these parameters we are setting:

    • UIHint setting the FieldTemplate to be used.
    • DataField which field from the table to bind the FieldTemplate to.

    When you run the sample you will see something like Figure 6.

    Example output from EditSubGridViews.aspx

    Figure 6 – Example output from EditSubGridViews.aspx

    Creating a Dynamic ItemTemplate that Implements ITemplate

    ITemplate documentation can be found on the MSDN website here Creating Web Server Control Templates Programmatically.

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Web;
    using System.Web.UI;
    using System.Web.DynamicData;
    using System.Web.UI.WebControls;
    using AjaxControlToolkit;
    
    /// <summary>
    /// Creates an item template that renders any children columns in the passed in table as GridViews
    /// </summary>
    public class SubGridViewItemTemplate : ITemplate
    {
        private MetaTable _table;
        private Page _page;
    
        public SubGridViewItemTemplate(MetaTable table, Page page)
        {
            _table = table;
            _page = page;
        }
    
        public void InstantiateIn(Control container)
        {
            IParserAccessor acessor = container;
            // get all the children columns
            var subGridTables = from c in _table.Columns.OfType<MetaChildrenColumn>()
                                select new SubDetails()
                                {
                                    Column = c,
                                    SubGridMetaData = c.Attributes.OfType<SubGridViewsAttribute>().FirstOrDefault(),
                                    Order = c.Attributes.OfType<SubGridViewsAttribute>().FirstOrDefault() != null
                                    && c.Attributes.OfType<SubGridViewsAttribute>().FirstOrDefault().Order > 0
                                    ? c.Attributes.OfType<SubGridViewsAttribute>().FirstOrDefault().Order
                                    : int.MaxValue,
                                };
    
            // sort the according to Order first and column name second
            // note if SubGridViewsAttribute is not allied or the attrivute
            // has no value for Order then just sort but column name
            subGridTables = from sg in subGridTables
                            orderby sg.Order, sg.Column.Name
                            select sg;
    
            // make sure there are some children columns
            if (subGridTables.Count() > 0)
            {
                // check if more than one children column present
                if (subGridTables.Count() > 1)
                {
                    // create tab container to hold each children column
                    var tabContainer = new TabContainer();
                    tabContainer.ID = "tabContainer";
    
                    // add event handler
                    tabContainer.EnableViewState = true; // ***UPDATED 2008/09/27***
    
                    // add the tab container to the page
                    acessor.AddParsedSubObject(tabContainer);
    
                    // add a tab panel for each children table
                    foreach (SubDetails SubGridDetails in subGridTables)
                    {
                        var tabPanel = new AjaxControlToolkit.TabPanel();
                        tabPanel.ID = "tp" + SubGridDetails.Column.Name;
    
                        // add the tab panel
                        tabContainer.Tabs.Add(tabPanel);
    
                        var subGridAttributes = SubGridDetails.Column.Attributes.OfType<SubGridViewsAttribute>().SingleOrDefault();
                        // set the Tab's name to be the tables display name 
                        // or table Name if no attribute is present
                        if (subGridAttributes != null && subGridAttributes.TabName.Length > 0)
                            tabPanel.HeaderText = subGridAttributes.TabName;
                        else
                            tabPanel.HeaderText = SubGridDetails.Column.ChildTable.DisplayName;
    
                        //Instantiate a DynamicControl for this Children Column
                        var childrenGrid = new DynamicControl(DataBoundControlMode.Edit)
                        {
                            ID = SubGridDetails.Column.Name,
    
                            // set UIHint
                            UIHint = "ChildrenGrid",
    
                            // set data field to column name
                            DataField = SubGridDetails.Column.Name
                        };
    
                        // add the DynamicControl to the tab panel
                        tabPanel.Controls.Add(childrenGrid);
                    }
                    // set the tab pannels index to 0 which
                    // forces the first tab to be selected
                    if (!_page.IsPostBack)
                        tabContainer.ActiveTabIndex = 0;
                }
                else
                {
                    // if only one sub grid then don't bother with tabs
                    SubDetails SubGridDetails = subGridTables.FirstOrDefault();
                    var childrenGrid = new DynamicControl(DataBoundControlMode.Edit)
                    {
                        ID = SubGridDetails.Column.Name,
                        UIHint = "ChildrenGrid",
                        DataField = SubGridDetails.Column.Name
                    };
    
                    // add the grid to the page
                    acessor.AddParsedSubObject(childrenGrid);
                }
            }
            else
            {
                // if no children columns
                // add label to show no grids
                var label = new Label();
                label.Text = "There are no SubGrids";
                label.CssClass = "droplist";
    
                // add the label to the page
                acessor.AddParsedSubObject(label);
            }
        }
    
        private class SubDetails
        {
            /// <summary>
            /// Column to display
            /// </summary>
            public MetaChildrenColumn Column { get; set; }
    
            /// <summary>
            /// MetaData if any from the original column
            /// </summary>
            public SubGridViewsAttribute SubGridMetaData { get; set; }
    
            /// <summary>
            /// Holds the sort order value
            /// </summary>
            public int Order { get; set; }
        }
    }

    Listing 5 - SubGridViewItemTemplate

    Listing 5 is the class that implements ITemplate – SubGridViewItemTemplate, to summarise this generates a template in code at runtime. In this case the template consists of a TabContainer from the Ajax Control Toolkit with a TabPanel for each ChildrenColumn found in the _table passed in to the class in the constructor. The TabPanel in turn contains a DynamicControl with it’s UIHint set to ChildrenGrid.

    Personal Note: For me the key thing that Dynamic Data gives us is the DynamicField and DynamicControl which use FieldTemplates, these FielTemplates encapsulate all our field functionality in a central place. This is apart from all the other Dynamic Data goodness that we get from scaffolding etc. For this ITemplate class it remove ALL the complexity from the class and also makes it future proof, because who knows what fields my ChildrenGrid will face in the future? but I don’t need to worry be cause DynamicField and DynamicControl will handle it all.

    So an explanation of the code seems appropriate here.

    var subGridTables = from c in _table.Columns.OfType<MetaChildrenColumn>()
                        select new SubDetails()
                        {
                            Column = c,
                            SubGridMetaData = c.Attributes.OfType<SubGridViewsAttribute>().FirstOrDefault(),
                            Order = c.Attributes.OfType<SubGridViewsAttribute>().FirstOrDefault() != null
                            && c.Attributes.OfType<SubGridViewsAttribute>().FirstOrDefault().Order > 0 
                            ? c.Attributes.OfType<SubGridViewsAttribute>().FirstOrDefault().Order 
                            : int.MaxValue,
                        };
    
    // sort the according to Order first and column name second
    // note if SubGridViewsAttribute is not allied or the attrivute
    // has no value for Order then just sort but column name
    subGridTables = from sg in subGridTables
                    orderby sg.Order, sg.Column.Name
                    select sg;

    Listing 6 – Getting the ChildrenColumns

    These two Linq to Objects statements here firstly get a list of new objects and secondly sorts this list. It’s clear what the first two properties are in the SubDetails class but Order looks a little complex. What’s happening in setting Order is; if the SubGridViewsAttribute is not present or the Order property of SubGridViewsAttribute not set then the Order property of the new SubDetails object is set to int.MaxValue (this is because int can’t be null and by default is initialised to zero).

    The next chunk of code is pretty straight forward it just loops over the subGridTables collection and create the appropriate TabContainer and fills it with TabPanels each with it’s own DynamicControl. If however there is only one ChilcrenColumn in the _table then it just adds a single DynamicControl and finally if there are no ChildrenColumns in the _table then it adds a Label that says There are no SubGrids.

    Finally at the end of the class is the definition of the SubDetails class.

    Adding the SubGridViewItemTemplate a FormView

    The code to SubGridViewItemTemplate to a FormView is straight forward:

    FormView1.ItemTemplate = new SubGridViewItemTemplate(table);
    So to finish off the EditSubGridViews.aspx page we need to add a little more code:
    protected void Page_Init(object sender, EventArgs e)
    {
        DynamicDataManager1.RegisterControl(DetailsView1);
        DynamicDataManager1.RegisterControl(FormView1);
    
        // load item template
        table = DetailsDataSource.GetTable();
        String itemTemplate = table.Model.DynamicDataFolderVirtualPath + "Templates/" + table.Name + ".ascx";
        if (File.Exists(Server.MapPath(itemTemplate)))
        {
            FormView1.ItemTemplate = LoadTemplate(itemTemplate);
        }
        else
        {
            // generate the sub grid views if no template available
            table = DetailsDataSource.GetTable();
            FormView1.ItemTemplate = new SubGridViewItemTemplate(table);
        }
    }

    Listing 7 – adding the SubGridViewItemTemplate to EditSubGridViews.aspx

    Now if you supply a custom ItemTemplate for a table then that will be used to format the FormView otherwise it will auto generate the ItemTemplate for the table.

    I seem to have missed out the attribute class SubGridViewsAttribute so I'll list it here:

    [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
    public class SubGridViewsAttribute : Attribute
    {
        public int Order { get; set; }
        public String TabName { get; set; }
        public SubGridViewsAttribute()
        {
        }
    }

    Listing 7 - SubGridViewsAttribute

    And now some sample metadata:

    [MetadataType(typeof(EmployeeMD))]
    public partial class Employee 
    { public class EmployeeMD { [SubGridViews( Order = 2, TabName = "My Employees")] public object Employees { get; set; } [SubGridViews( Order = 3, TabName = "My Territories")] public object EmployeeTerritories { get; set; } [SubGridViews( Order = 1, TabName = "My Orders")] public object Orders { get; set; } public object Employee1 { get; set; } } }

    Listing 8 - sample metadata showing order and tab name

    Now I think that’s neat smile_teeth

    Project Download

    *** UPDATES ***

    UPDATED 2008/09/26 :I’ve just discovered an issue which shows itself when you click on a tab other than the first tab and then click edit on a grid item. When you do this after postback the tab index has been reset back to 0, if you click the tab you had previously selected the row you clicked edit on is in edit mode.
    This is just to let you know I know about this BUG and am working on it.
    I’ll be back soon with an update smile_teeth
    UPDATED 2008/09/26 : Added tabContainer.AutoPostBack = true; to fix issue with tabs smile_teeth 
    UPDATED 2008/09/27 : A better fix is to add tabContainer.EnableViewState = true; to fix issue with tabs smile_teeth 

    Tuesday, 23 September 2008

    Dynamic Data and Advanced Field Templates - ParentFormView with Templates

    ParentFormView with Templates

    This is a variant on the DetailsView/ParentDetails FieldTemplate the main difference is that here the FieldTemplate is based on a FormView not a DetailsView.

    <asp:FormView 
        ID="FormView1" 
        runat="server" 
        DataSourceID="FormDataSource"
        CssClass="detailstable"
        FieldHeaderStyle-CssClass="bold">
        
    </asp:FormView>
    
    <asp:LinqDataSource 
        ID="FormDataSource" 
        runat="server" 
        EnableDelete="true">
    </asp:LinqDataSource>

    Listing 1 – changes to the FieldTemplate

    In Listing 1 the DetilsView has been changed to a FormView. All references to DetailsView1 and DetailsDataSource have been changed to FormView1 and FormDataSource in the ascx page and the code behind.

    if (metaForeignKeyColumn != null)
    {
        // load Field Template
        String ddFolderVirtualPath = metaForeignKeyColumn.ParentTable.Model.DynamicDataFolderVirtualPath;
        String tableName = metaForeignKeyColumn.ParentTable.Name;
    
        String itemTemplate = ddFolderVirtualPath + "Templates/" + tableName + "Item.ascx";
        String editItemTemplate = ddFolderVirtualPath + "Templates/" + tableName + "Edit.ascx";
        //String insertItemTemplate = ddFolderVirtualPath + "Templates/" + tableName + "Insert.ascx";
        String footerTemplate = ddFolderVirtualPath + "Templates/" + tableName + "Footer.ascx";
        String headerTemplate = ddFolderVirtualPath + "Templates/" + tableName + "Header.ascx";
        String pagerTemplate = ddFolderVirtualPath + "Templates/" + tableName + "Pager.ascx";
    
        if (File.Exists(Server.MapPath(itemTemplate)))
        {
            FormView1.ItemTemplate = LoadTemplate(itemTemplate);
    
            // load any other field templates
            //TODO: if an operation is required but no template throw error
            if (File.Exists(Server.MapPath(editItemTemplate)))
                FormView1.EditItemTemplate = LoadTemplate(editItemTemplate);
    
            // because this is a parent no insert will be available
            //if (File.Exists(Server.MapPath(insertItemTemplate))) 
            //    FormView1.InsertItemTemplate = LoadTemplate(insertItemTemplate);
    
            if (File.Exists(Server.MapPath(footerTemplate)))
                FormView1.FooterTemplate = LoadTemplate(footerTemplate);
            if (File.Exists(Server.MapPath(headerTemplate)))
                FormView1.HeaderTemplate = LoadTemplate(headerTemplate);
            if (File.Exists(Server.MapPath(pagerTemplate)))
                FormView1.PagerTemplate = LoadTemplate(pagerTemplate);
    
            ...//CODE OMITTED FOR CLARITY
        }
        else
        {
            throw new InvalidOperationException("ParentFormView_Edit requires an item template.");
        }
    }
    else
    {
        // throw an error if set on column other than MetaChildrenColumns
        throw new InvalidOperationException("The GridView FieldTemplate can only be used with MetaChildrenColumns");
    }

    Listing 2 – changes to the code behind

    protected override void OnDataBinding(EventArgs e)
    {
        base.OnDataBinding(e);
    
        // get the FK column
        var metaForeignKeyColumn = Column as MetaForeignKeyColumn;
    
        // get the association attributes associated with MetaChildrenColumns
        var association = metaForeignKeyColumn.Attributes.
            OfType<System.Data.Linq.Mapping.AssociationAttribute>().FirstOrDefault();
    
        if (metaForeignKeyColumn != null && association != null)
        {
            // get keys ThisKey and OtherKey into dictionary
            var keys = new Dictionary<String, String>();
            var seperator = new char[] { ',' };
            var thisKeys = association.ThisKey.Split(seperator);
            var otherKeys = association.OtherKey.Split(seperator);
            for (int i = 0; i < thisKeys.Length; i++)
            {
                keys.Add(thisKeys[i], otherKeys[i]);
            }
    
            // setup the where clause 
            // support composite foreign keys
            foreach (String fkName in metaForeignKeyColumn.ForeignKeyNames)
            {
                // get the current FK column
                var fkColumn = metaForeignKeyColumn.Table.GetColumn(fkName);
                // get the current PK column
                var pkColumn = metaForeignKeyColumn.ParentTable.GetColumn(keys[fkName]);
    
                // setup parameter
                var param = new Parameter();
                param.Name = pkColumn.Name;
                param.Type = pkColumn.TypeCode;
    
                // get the value for this FK column
                param.DefaultValue = GetColumnValue(fkColumn).ToString();
    
                // add the where clause
                FormDataSource.WhereParameters.Add(param);
            }
        }
    }

    Listing 3 – OnDataBinding now handles multiple PK-FK relationships ***UPDATED 2008/09/24***

    As you can see what has been done here add the facility to load templates for the FormView to allow this I’ve added a new folder to the DynamicData folder named Templates see Figure 1.

    Folder structure changes

    Figure 1 - Folder structure changes

    In Listing 2 you can see that Model.DynamicDataFolderVirtualPath is accessed to retrieve the Dynamic Data virtual path (the default is ~/DynamicData/) so that the path to each Template can be constructed. The path to each of the possible Templates is constructed, but only the itemTemplate is required.

    Note: Code to test for other Templates could be added if that option was enabled via properties set via the constructor.

    All you need to create a Template file is to add a new UserControl to the Templates folder and then customise it.

    <p class="droplist">
    <strong>Name: </strong><asp:DynamicControl ID="DynamicControl1" runat="server" DataField="CompanyName" ></asp:DynamicControl>&nbsp;
    <strong>Contact Name: </strong><asp:DynamicControl ID="DynamicControl2" runat="server" DataField="ContactName" ></asp:DynamicControl>
    </p>
    <p class="droplist">
    <strong>Phone: </strong><asp:DynamicControl ID="DynamicControl3" runat="server" DataField="Phone" ></asp:DynamicControl>&nbsp;
    <strong>Fax: </strong><asp:DynamicControl ID="DynamicControl4" runat="server" DataField="Fax" ></asp:DynamicControl>
    </p>

    Listing 4 – a very simple ItemTemplate

    As you can see Listing 4 is a very simple field template but looks how you would expect the mark-up to look in Figure 2.

    ScreenShot119

    Figure 2 – the FieldTemplate at work

    Project Download