Monday, June 11, 2012

Dynamic Data Custom PageTemplates with Ajax Control Toolkit Tabs

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.

 

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

Figure 2 – Select New Folder from the context menu

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

Figure 3 – Add New Item

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

Figure 4 – Create the Web User Control

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.

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

 

No comments:

Post a Comment