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 FromViewI’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.
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
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.
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
Project Download
*** UPDATES ***
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
32 comments:
I'm trying to implement your custom page template and am getting the following compiler errors:
Error 1 The type or namespace name 'ShowColumnsAttribute' could not be found (are you missing a using directive or an assembly reference?) c:\Projects\CTSWeb1\DynamicData\FieldTemplates\ChildrenGrid_Edit.ascx.cs 27
Error 2 The type arguments for method 'System.Linq.Enumerable.SingleOrDefault[less than symbol]TSource[greater than symbol](System.Collections.Generic.IEnumerable[less than symbol]TSource[greater than symbol])' cannot be inferred from the usage. Try specifying the type arguments explicitly. c:\Projects\CTSWeb1\DynamicData\FieldTemplates\ChildrenGrid_Edit.ascx.cs 27
Error 3 The type or namespace name 'FieldTemplateRowGenerator' could not be found (are you missing a using directive or an assembly reference?) c:\Projects\CTSWeb1\DynamicData\FieldTemplates\ChildrenGrid_Edit.ascx.cs 58
Do these errors ring a bell? They don't with me...
Thanks,
Bill Ross
Hi Bill have you tried running the download as is, you just need to alter your connection string so it will connect to Northwind.
Steve :D
Hi Bill, just a thought you are using Dynamic Data From VS2008 SP1?
Steve :D
I just recently downloaded the DynamicData - I think it is the latest and the greatest, though I'm not sure exactly how to tell.
My version of Northwind does not have the tables you refer to, so it doesn't work.
I guess I'll scour Microsoft for the older versions of NW and see if I can get your version running. That would help.
Do you have a demo site where I could view the operation?
The Latest Version of DD is availble in the VS2008 SP1.
The version of Northwind I use is the standard northwind from SQL2000 downloads here:http://www.microsoft.com/downloads/details.aspx?FamilyID=06616212-0356-46A0-8DA2-EEBC53A68034&displaylang=en
The best thing for you to try is download my web project file point the connection string at your Northwind and then run it. The project should just work. All the error messages sugest you are missing some of the classes in my project.
Hope this helps :D
Steve
Hi Steve,
Thanks for all the great work!
I do have a few questions...
How would I go about controlling the tabs? Which table shows up and also display name?
How would you add a details and an insert link to the tabbed view for the children tables? I tried to add a insert link, but it was wanting to insert for the parent table, not the child table.
Also, how would I control which fields get edited when edit link is clicked in the children tables? Currently I'm using custom edit pages for my tables, which I'm selecting which columns get edited and which are readonly. How do I get that same control?
Thanks so much in advance! All your work has been very helpful!
James
Hi jyoo, I think we could change the subGrid attribute to have hide property and fiulter them. In looking back I seem to have missed out the attribute and will get around to updating it.
Steve :D
I also will get on to that other issue you mentioned :-/
Steve :D
Steve, in listing 8 you have these lines:
[SubGridViews(
Order = 2,
TabName = "My Employees")]
public object Employees { get; set; }
Is the "public object Employeees" indicating a child table name of "Employees"? If so, what do I do if my child table has a space in the name? What if the table name were [Employee Territories]?
Thanks.
Bill Ross
Hi Bill, it is the children table referance in Linq To SQL it should be similar in Entity Framework.
Steve :D
I don't quite get Routes. Currently my application gets a "Resource not found" error looking for:
/MyWeb/MyTable/List.aspx
I would have thought it should be looking for:
/MyWeb/DynamicData/PageTemplates/MyTable/List.aspx
?
Hi Steve,
So I got the first question worked out I asked regarding which tabs and names show up by going through your steps again:).
I'm wondering if you had a chance to follow up with the other questions?
I'm still trying to figure out how to add a details and an insert link to the children table in the Subgrid View. I noticed in the ChildrenGrid template, the AutoGenerateDeleteButton and AutoGenerateEditButton are set, but there is no AutoGenerateInsertButton.
Also is there anyway to control the inline edit on the children tables? Is there anyway to use the field templates like how you would use them with a list, edit, or insert page? Basically I'm trying to get the Subgrid View to look like the standard list page.
Thanks in advance:)
James
Steve, in your code, I notice that you have an App_code/NW.Metadata.cs and you have a DynamicData/Templates directory. I'm getting a "Resource not found" error on my list.aspx page. I'm wondering...
What refers to the Templates directory? Do I need that?
In your NW.Metadata.cs file, it looks like you have the metadata and partial classes for three tables. I heard that the convention was to create a separate class file for each table, with the table name as the name of the file. Thoughts?
If I click on "Categories" in your example project and then "View Products" I get "Invalid Column Name 'Calculated Column'" from line 34 of FilterUserControl.ascx.cs
Hi Jhyoo, you will just need to make some changes in the ChildrenGrid FieldTemplate and add a link for the insert. I chose to do it this was as I don't see a need for calling a seperate page for editing the lines. Also I plan to replace the GridView control with the ListView version created in a later article so the inline edit and insert are available.
Steve :D
Hi Bill, this version does not need the templates folder but in a later article it will. As for the Metadata class the convertion is to use a nested class for the metadata and I've also seen a private interface used as well, but I've not tried that yet :D
Steve :D
Hi Jyoo, you said "Also, how would I control which fields get edited when edit link is clicked in the children tables? Currently I'm using custom edit pages for my tables, which I'm selecting which columns get edited and which are readonly. How do I get that same control?"
I would add some new attribute specifically for the SubGrid and then drop those column in the Linq query in the column generator.
Steve :D
Hi Bill, you said "If I click on "Categories" in your example project and then "View Products" I get "Invalid Column Name 'Calculated Column'" from line 34 of FilterUserControl.ascx.cs"
I think you are using Entity Framework and it does not support Calculated Columns this only Works at the moment with Linq to SQL David Ebbo said in a post on the ASP.Net Dynamic Data Forum that the issue is with the EntityDataSource and there has just been released a new Futures entitled PDC preview release here: http://www.codeplex.com/aspnet/Release/ProjectReleases.aspx?ReleaseId=18764 this may have a workaround.
Steve :D
Hi Bill, another thought on that Categories thing I think you may be talking about a different article than this one as the error seems to refer to Filtering and Filtering on any column, is that correct?
Steve :D
I just ran the first of your four projects. And yes, I'm not using Linq so your first response sounds correct.
ChildrenGrid_Edit can only get the binding parameter from QueryString. (OnDataBinding)
How can I apply the DynamicControl binding ???
(e.g. provide the Binding parameter from other control)
You can use the ExtractForeignKey:
IDictionary dict = new OrderedDictionary();
fkColumn.ExtractForeignKey(dict, selectedValue);
to extract the key value pairs.
I have a new articles soon which has this in it and may be worth a read.
Steve
First of all, I want to say thanks--you've been a fantastic resource. You probably get this a lot, but here it goes anyway...
I was trying to convert your project to VB and was having a little difficulty with the class SubGridViewItemTemplate.
In the InstantiateIn subroutine, you declare the subGridTable and sort it.
Would you be able to help me with the VB equivilent to the declaring the subGridTables and calling the SubDetails() class?
Thanks.
--John
Hi John, if you send me an e-mail direct we could chat via msn or skype and see what it is you are having difficulty with.
Steve :D
Mr. Stephen Naughton,
I have done in my project accordingly to your instructions (in the global.asax.cs) but even tough URL routing seems to work ok while debugging, it seems that some code generated by dynamic data itself overrides my routing and it uses the standard one so I am unable to use my version "mstList" of the List template.
Any idea that might be helpful?
Carlos Porras (El Salvador)
Hi Análisis Políticos Guanacos, have you tried the sample? And if so does it act the same way?
If you would like I can build a sample and send it to you for you to try.
Steve :D
Mr. Stephen Naughton,
I greatly appreciate your effort and time in trying to contribute with me. Thank you very much.
What I'm trying to do is the following:
As I am creating a system forr controlling inventory (stock) and sales (billing) for a small company, I have elaborated an ERD which is composed of 140 tables. 52 of them are "reference tables as to keep referential integrity sustained by means of using DropDownLists.
On the other hand, one "holding" is composed of different "companies" and each company is divided into "locals".
It happens that what I call a "holding" is really a small company which is dedicated to different commercial activities; for instance: a company (let's say Orion)can have three different kind of business (let's say: a hardware store, a pet store and a small pharmacy)
I will have registered in my system each one of them as different companies (hardware store = Orion Hardware, Pet = Orion's Pet Shop and Pharmacy = "Orion's Drugstore)
Even tough they will reside in the same database, I need that each one of them to look only the part of the database that belongs to each one of them, even tough the administrator or manager or CEO will be able to see and parameterize any company.
What I will need to do then is to keep the company code from the login screen and pass it trough all different screens of the system but only using the company code for that specific user that has been logged on.
In order to do this, I need to redirect all 52 "mstxxxx" tables to use a screen in which I could pass the value of the company code as to automatically fill the DropDownList with that single value as to show the user that he is using and working data from that specific company. (He wouldn't be able to see any other data from other company)
I can achieve this if I were to create in the Custom Templates folder 52 different carpets with the name of the table and of course I'd have a copy of my List.aspx file in every one of the different 52 carpets.
I think that I could achieve this by using proper redirecting URL in the global.asax.cs file.
Is that possible?
Carlos Porras (El Salvador)
Hi Carlos, it is eminantly possible, I have done this sort of thing using some custom code in the standard page template (List) I would add code that checked to see the table had the filter column using code like this:
MetaColumn column;
table.TryGetColumn("foo", out column);
with this extension method you can if the column is not null you can then apply your filter to the data source by adding a whereParameter.
Steve :D
This is a bit clearer :)
MetaColumn column;
if (table.TryGetColumn("foo", out column))
{
//TODO: add where parameter
}
Steve :D
Steve,
First of all thanks for all your work and I love your articles. I am just getting into Dynamic Data and it looks good. I have done a complete form in an AJAX Tabcontainer in a FormView and using DynamicControl wherever possible. The kick is that I want a two column layout. I have set up all the controls in an HTML table, and when I had finished it all I went to change the TabIndex property on the DyamicControl fields to get the 2 column effect (tab key takes the user down columns, not across rows) when I realised there IS no TabIndex property. Disaster. What do you do when you want a two column layout in a FormView? Am I missing something or will I have to lay it out all over again from scratch?
Also, there doesn't seem to be any way to set the width of a DynamicCOntrol field? I tried setting it in the CssClass but it gets overridden by the width of the actual control that is used (i.e. the control in, for example, the Text_Edit template.
Hi Steve,
Thank you very much!
I just want to know if this article is still applicable to 4.0 Framework Dynamic Data Entities.
Do you have a sample or a new version?
Thanks!
Where can I download the project?
Hi Numb, I see the project has dissapeared from my SkyDrive I will fix this ASAP.
Steve
P.S. I will be adding this to NuGet Soon.
Post a Comment