Monday, 15 February 2010

Grouping Fields on Details, Edit and Insert with Dynamic Data 4, VS2010 and .Net 4.0 RC1

Whilst writing my last article over the week end I noticed the new Display attribute see Figure 1 the first one that intrigued me was the GroupName parameter, so the first thing I did was add some GroupName to some metadata on a Northwind table.

Display attribute parameters

Figure 1 – Display attribute parameters

[MetadataType(typeof(OrderMetadata))]
public partial class Order
{
    internal partial class OrderMetadata
    {
        public Object OrderID { get; set; }
        public Object CustomerID { get; set; }
        public Object EmployeeID { get; set; }
        [Display(Order = 0,GroupName = "Dates")]
        public Object OrderDate { get; set; }
        [Display(Order = 1,GroupName = "Dates")]
        public Object RequiredDate { get; set; }
        [Display(Order = 2,GroupName = "Dates")]
        public Object ShippedDate { get; set; }
        [Display(Order = 4,GroupName = "Ship Info")]
        public Object ShipVia { get; set; }
        [Display(Order = 5,GroupName = "Ship Info")]
        public Object Freight { get; set; }
        [Display(Order = 3,GroupName = "Ship Info")]
        public Object ShipName { get; set; }
        [Display(Order = 6,GroupName = "Ship Info")]
        public Object ShipAddress { get; set; }
        [Display(Order = 7,GroupName = "Ship Info")]
        public Object ShipCity { get; set; }
        [Display(Order = 8,GroupName = "Ship Info")]
        public Object ShipRegion { get; set; }
        [Display(Order = 9,GroupName = "Ship Info")]
        public Object ShipPostalCode { get; set; }
        [Display(Order = 10,GroupName = "Ship Info")]
        public Object ShipCountry { get; set; }
        // Entity Ref 
        [Display(Order = 12,GroupName = "Other Info")]
        public Object Customer { get; set; }
        // Entity Ref 
        [Display(Order = 13,GroupName = "Other Info")]
        public Object Employee { get; set; }
        // Entity Set 
        [Display(Order = 14,GroupName = "Other Info")]
        public Object Order_Details { get; set; }
        // Entity Ref 
        [Display(Order = 11,GroupName = "Ship Info")]
        public Object Shipper { get; set; }
    }
}

Listing 1 – GroupName metadata.

Well when I ran the app and get Figure 2 I was a little disappointed, I’d expected that at least the field would be grouped together by group name (maybe this was not the intended use but I was determined to make it work) but better still would have been with a separator containing the group name.

GroupingBefore

Figure 2 – Orders table with GroupName metadata

So I set about “making it so” (to quote Captain Picard) the first step was to group the fields so I looked at the new EntityTemplates.

<asp:EntityTemplate runat="server" ID="EntityTemplate1">
    <ItemTemplate>
        <tr class="td">
            <td class="DDLightHeader">
                <asp:Label 
                    runat="server"
                    OnInit="Label_Init" />
            </td>
            <td>
                <asp:DynamicControl 
                    runat="server"
                    OnInit="DynamicControl_Init" />
            </td>
        </tr>
    </ItemTemplate>
</asp:EntityTemplate>

Listing 2 – Default.ascx entity template

public partial class DefaultEntityTemplate : EntityTemplateUserControl
{
    private MetaColumn currentColumn;

    protected override void OnLoad(EventArgs e)
    {
        foreach (MetaColumn column in Table.GetScaffoldColumns(Mode, ContainerType))
        {
            currentColumn = column;
            Control item = new _NamingContainer();
            EntityTemplate1.ItemTemplate.InstantiateIn(item);
            EntityTemplate1.Controls.Add(item);
        }
    }

    protected void Label_Init(object sender, EventArgs e)
    {
        Label label = (Label)sender;
        label.Text = currentColumn.DisplayName;
    }

    protected void DynamicControl_Init(object sender, EventArgs e)
    {
        DynamicControl dynamicControl = (DynamicControl)sender;
        dynamicControl.DataField = currentColumn.Name;
    }

    public class _NamingContainer : Control, INamingContainer { }
}

Listing 3 – Default.ascx.cs entity template code behind.

If you look at my old Custom PageTemplates Part 4 - Dynamic/Templated FromView sample, I implemented the ITemplate interface for generating FormView and also ListView templates dynamically, and I remember at the time David Ebbo commenting on this and how he was working on something a little more flexible and then Entity Templates were unveiled in one of the early previews; but this is considerably more flexible than my sample was. I think we will be able to extend this greatly in the future but for now I’ll be happy with making grouping work.

So the first thing I did was tweak the default entity template to order by groups.

protected override void OnLoad(EventArgs e)
{
    // get a list of groups ordered by group name
    var groupings = from t in Table.GetScaffoldColumns(Mode, ContainerType)
                    group t by t.GetAttributeOrDefault<DisplayAttribute>().GroupName into menu
                    orderby menu.Key
                    select menu.Key;

    // loop through the groups
    foreach (var groupId in groupings)
    {
        // get columns for this group
        var columns = from c in Table.GetScaffoldColumns(Mode, ContainerType)
                      where c.GetAttributeOrDefault<DisplayAttribute>().GroupName == groupId
                      orderby c.GetAttributeOrDefault<DisplayAttribute>().GetOrder()
                      select c;

        // add fields
        foreach (MetaColumn column in columns)
        {
            currentColumn = column;
            Control item = new _NamingContainer();
            EntityTemplate1.ItemTemplate.InstantiateIn(item);
            EntityTemplate1.Controls.Add(item);
        }
    }
}

Listing 4 – extended default entity template stage 1

So what I did in listing 4 was get a list of all the groups sorted by group name, and then loop through the groups getting the column for each group; then generate the groups fields. Visually this does not produce much of a difference than the initial display.

Grouping with Sort

Figure 3 – Grouping with Sort.

Now we can see the groups coming together, next we need to add the visual aspect.

A little surgery on the ascx part of the entity template is required to get this to work. In Listing 5 you can see that I have added some runat=”server” properties to the TD’s of the template.

<asp:EntityTemplate runat="server" ID="EntityTemplate1">
    <ItemTemplate>
        <tr class="td">
            <td class="DDLightHeader" runat="server">
                <asp:Label 
                    runat="server" 
                    OnInit="Label_Init" />
            </td>
            <td runat="server">
                <asp:DynamicControl 
                    runat="server" 
                    OnInit="DynamicControl_Init" />
            </td>
        </tr>
    </ItemTemplate>
</asp:EntityTemplate>

Listing 5 – modified default.ascx Entity Template.

Moving to the Default entity templates code behind in Listing 6 I have added the code to add a separator, but it will need some modification as at the moment is just a repeat of one of the columns.

protected override void OnLoad(EventArgs e)
{
    // get a list of groups ordered by group name
    var groupings = from t in Table.GetScaffoldColumns(Mode, ContainerType)
                    group t by t.GetAttributeOrDefault<DisplayAttribute>().GroupName into menu
                    orderby menu.Key
                    select menu.Key;

    // loop through the groups
    foreach (var groupId in groupings)
    {
        // get columns for this group
        var columns = from c in Table.GetScaffoldColumns(Mode, ContainerType)
                      where c.GetAttributeOrDefault<DisplayAttribute>().GroupName == groupId
                      orderby c.GetAttributeOrDefault<DisplayAttribute>().GetOrder()
                      select c;

        // add group separator
        if (!String.IsNullOrEmpty(groupId))
        {
            groupHeading = true;
            currentColumn = columns.First();
            groupName = groupId;
            Control item = new _NamingContainer();
            EntityTemplate1.ItemTemplate.InstantiateIn(item);
            EntityTemplate1.Controls.Add(item);
        }

        // add fields
        foreach (MetaColumn column in columns)
        {
            groupHeading = false;
            currentColumn = column;
            Control item = new _NamingContainer();
            EntityTemplate1.ItemTemplate.InstantiateIn(item);
            EntityTemplate1.Controls.Add(item);
        }
    }
}

Listing 6 – final version of the OnLoad handler.

For the final tweaks of the visual of the separator we will done some extra manipulation in the two Init handlers of the Label and the DynamicControl.

For the Label wee need to change the text so I have added a class field groupHeading as a Boolean which if you look in Listing 6 I’m setting to true when it is a group and false when a field.

protected void Label_Init(object sender, EventArgs e)
{
    if (!groupHeading)
    {
        Label label = (Label)sender;
        label.Text = currentColumn.DisplayName;
    }
    else
    {
        Label label = (Label)sender;
        label.Text = groupName;
        var parentCell = label.GetParentControl<HtmlTableCell>();
        parentCell.ColSpan = 2;
        parentCell.Attributes.Add("class", "DDGroupHeader");
    }
}

Listing 7 – Label_Init handler

So in Listing 7 you can see that we do the standard thing if it is a filed, but do some custom stuff if it is a group heading. I first get the parent control (see Listing 8 for source) of type HtmlTableCell (we can get this because we set it to runat=”server”). Once we have the parent cell we can manipulate it; first of all we set it’s colspan attribute to 2 and change the CSS class to "DDGroupHeader" to make it stand out.

/// <summary>
/// Gets the parent control.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="control">The control.</param>
/// <returns></returns>
public static T GetParentControl<T>(this Control control) where T : Control
{
    var parentControl = control.Parent;
    // step up through the parents till you find a control of type T
    while (parentControl != null)
    {
        var p = parentControl as T;
        if (p != null)
            return p;
        else
            parentControl = parentControl.Parent;
    }
    return null;
}

Listing 8 – GetParentControl extension method.

.DDGroupHeader
{
	font-weight: bold;
	font-style: italic;
	background-color: Silver;
}

Listing 9 – DDGroupHeader CSS snippet

The next thing is to hide the DynamicControl and it’s HtmlTableCell so the label can span both columns.  Now if we are in a group header then we hide the DynamicControl, get the parent cell and also hide it, letting the label’s cell span both rows.

protected void DynamicControl_Init(object sender, EventArgs e)
{
    DynamicControl dynamicControl = (DynamicControl)sender;
    dynamicControl.DataField = currentColumn.Name;
    if (groupHeading)
    {
        // hide Dynamic Control maybe overkill
        dynamicControl.Visible = false;
        // get the parent cell
        var parentCell = dynamicControl.GetParentControl<HtmlTableCell>();
        // hide the cell
        parentCell.Visible = false;
    }
}

Listing 10 – DynamicControl_Init handler

Note: The DynamicControl must have it’s DataField set otherwise it will throw an error.

Grouping with visual separators

Figure 4 – Grouping with visual separators.

Now we have separators working, “made so” I would think, well only partially with this version you have to repeat all the above with a few minor changed for Edit and Insert EntityTemplates, but that is in the sample.

Note: Remember this sample is with Visual Studio 2010 and .Net 4.0 RC1

Download

As always have fun coding

11 comments:

John Jolly said...

Thanks for this, a very useful guide that has helped me get a bit more into dynamic data. When I implemented this I noticed that the grouped groups were not in the order I intended, rather in alphabet order. I wanted the order of the groups to reflect an arbitrary order so I implemented a minor update that places the groups in an order defined by the smallest DisplayAttribute.Order value from each grouping. In the OnLoad I revised the order expression on the initial groupings select, so this becomes

orderby menu.Min(column => column.GetAttributeOrDefault().GetOrder() ?? 0)

John Jolly said...

Thanks for this, a very useful guide that has helped me get a bit more into dynamic data.
The order of the groups as things stand is aplhabetic but I wanted it controlled more precisely. I implemented a minor update that places the groups in an order defined by the smallest DisplayAttribute.Order value within each grouping. In the OnLoad I revised the order clause in the initial groupings select expression to become

orderby menu.Min(column => column.GetAttributeOrDefault().GetOrder() ?? 0)

Stephen J. Naughton said...

Hi John, good point, however I would add , menu.Key at the end so if order has not been used then some sensible order will be provided.

orderby menu.Min(column => column.GetAttributeOrDefault().GetOrder() ?? 0)
, menu.Key

Steve :)

Mae said...

Hi, Steve!

I really love all the articles you've posted in regards to Dynamic Data Controls/Websites! I am new to this and very overwhelmed :) with information.

I'm only using .NET 3.5 and have just started to play with it. So there are new things that .NET 4 that are not available to me, like the code you posted here.

In .NET 3.5 Dynamic Data, is it possible to dynamically create multiple formview controls and create its fields on 1 page? In addtion, be able to control how the fields are laid out using configuration tables?

There are so many fields and sections that I need to display on a grid that gridview/details view doesn't really work for me. And I'm trying to squish as much information on one page so that there's a wholistic view to the data, instead of clicking tabs back and forth or going to other pages.

To describe the page that I'm designing:
- On a page, you have 5 or more sections/groupings(formview inside panels). 1 section can have 28 fields and others can have 5 fields. The formviews do not have to be dynamically generated at this time.
- Each formview is pulling from one table or view.
- within the formview, the fields and its layout must be configurable. I'm thinking of laying the fields out horizontally in 1 or more columns. If it's just 1 column then it'll be like details view layout.
- the fields that can be viewed or edited should be configurable based on user's roles. Some users has less priveleges than others and can only view/edit certain fields.

Any thoughts and resources that you can suggest to help me accomplish this task?

Do you think performance will be greatly affected when doing dynamic pages this way?

Thanks for your help in advance!

FMM

Stephen J. Naughton said...

Hi FMM, email me direct and I will send you some links to articles

Steve (email in top right)

Gillykid said...

Just to expand on John Jolly's point, the code should actually be:

menu.Min(column => column.GetAttributeOrDefault ().GetOrder() ?? 0), menu.Key

Stephen J. Naughton said...

Thanks Gillykid, I will certanly use that in the next version :)

Steve

Unknown said...

Hi Steve, I would like to adopt the grouping but I am little concerned on loosing multicolumn break up implementation from your another example. Could you please confirm if these two can stay together and the UI could be rendered groups with multi columns arranged?

Regards,
Sudhakar

Stephen J. Naughton said...

Hi Sudhakara, sorry they are mutually exclusive. you could however adapt them to work together by adding some of the code from the Entity Template in the Grouping to the Multicolumn.

Steve

Unknown said...

Hi Steve, Could you please help me out in the combined implementation of multi column and groupings if you have some time this weekend? Also, I am looking for some help in creating tabs for each table.

Best Regards,
Sudhakar

Stephen J. Naughton said...

sorry I wont have time to do that but will look into it ad I do have one that does Tabs will post the example soon

Steve