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

    14 comments:

    jyoo said...

    Thanks for all the great examples! The problem I'm running into following your example as well as running your example is when you edit the Employees table, the default values is always the first Employee in the table. No matter which Employee I choose to edit, it grabs the Title, First Name, and Last Name of the first record. But all children records are for the correct record. I followed your example and having the same issue with my website. Do you have any solutions for this? Thanks, James

    Steve said...

    Hi Jyoo, I'll look into that tomorrow and get back to you :D

    WoundedEgo said...

    I have the first in this series working (thanks for supplying the SS2000 Adventure Works). Are these 4 projects distinct? Or is the 4th the latest and the greatest, that builds on the other 3?

    Thanks,

    Steve said...

    Yes the last will contain all the previous and will be the final.

    Steve

    Steve said...

    There code will also be tidyed up and sometimes made a bit more elegant and I may also have fixed some minor bugs in the final version.

    Steve

    jwize said...

    Can someone send me the code or the reference to the assembly that contains
    ShowColumnsAttribute I can't seem to find it.

    Thanks, Jaime

    Steve said...

    Hi Jwize, I don't have your e-mail address or I'd send it to you. But its in App
    _Code folder in the ParentChildrenTemplateAttribute.cs file, sorry about the mixup I think I refactored the Attribute but forgot to rename the file :(. Hope this helps you :D

    Steve

    Rabin said...

    Is it possible to enable inserting childrens from a given parent?

    Rabin said...

    Is it possible to enable inserting a child when using these tabpanels for a given parent?

    Steve said...

    Sorry Rabin not sure what you mean the idea of inserting a new parent seems odd to me. However insert is possible in the gridviews if you combine the subGrid control described in this series with the GridView (ListView) with Insert in a later series.

    Hope thsi helps :D
    Steve

    Rabin said...

    Hi Steve I added the listview control (adding a detailsview would also be sufficient) below the gridview control of the children tables for a given parent table. However when I want to insert a new child and hit the insert button on a given tabpage from this child it causes the validation of the children. I was thinking of putting a updatepanel in the children_edit.ascx, but inserting still seems to postback all the tabpages. Do you have any suggestions?

    Steve said...

    You need to add a validation group on for each i.e. ListViewValidationGroup and GridViewValidationGRoup

    Steve :D

    Delly said...

    I always get a null exception in ChildrenGrid_Edit at the following line:

    var thisKeys = association.ThisKey.Split(seperator);

    association.ThisKey is always null for all tables/fk's.

    I have tried using the data futures northwind, rebuilding dc, using the 2000 northwind etc. but its always null.

    Any ideas?

    Steve said...

    Could you repost this to the page the code you refer to is on as I done have the time to go hunting for it. Sorry.

    Have you tried the sample project at the end of the series?

    Also all the code on this site is only tested with Linq to SQL at the moment (I'll try Linq to EF a bit more when v2 come out)

    Steve