Sunday, 14 February 2010

A New Way To Do Column Generation in Dynamic Data 4 (UPDATED)

There have been several questions on the Dynamic Data Forum saying things like IAutoFieldGenerator does not work with Details, Edit and Insert pages. This is because these page template have now moved to FormView which allows for us to have the nice new Entity Templates and this is cool; but leaves us with the issue of having to do custom column generation in two ways one for form view and one for GridView in List and ListDetails pages. So harking back to this post A Great Buried Sample in Dynamic Data Preview 4 – Dynamic Data Futures long ago in a year far far awayBig Grin

So what I am planning to do is add our own MetaModel that we can pass in a delegate to produce a custom list of columns. I am going to implement the Hide column based on page template (HideColumnInAttribute) for now.

The Custom MetaModel


So first things first lets build out custom MetaModel, the only two classes we will need to implement for our custom MetaModel are:


  MetaModel
  MetaTable

We need MetaModel because it goes away and get the other classes.

public class CustomMetaModel : MetaModel
{
    /// <summary>
    /// Delegate to allow custom column generator to be passed in.
    /// </summary>
    public delegate IEnumerable<MetaColumn> GetVisibleColumns(IEnumerable<MetaColumn> columns);

    private GetVisibleColumns _getVisdibleColumns;

    public CustomMetaModel() { }

    public CustomMetaModel(GetVisibleColumns getVisdibleColumns)
    {
        _getVisdibleColumns = getVisdibleColumns;
    }

    protected override MetaTable CreateTable(TableProvider provider)
    {
        if (_getVisdibleColumns == null)
            return new CustomMetaTable(this, provider);
        else
            return new CustomMetaTable(this, provider, _getVisdibleColumns);
    }
}

Listing 1 – Custom MetaModel class

So what are we doing here, firstly we have a delegate so we can pass in a methods to do the column generation and we are passing this in through our custom constructor. Then in the only method we are overriding we are returning the CustomMetaTable class, and passing in the delegate if it has been set.

public class CustomMetaTable : MetaTable
{
    private  CustomMetaModel.GetVisibleColumns _getVisdibleColumns;

    /// <summary>
    /// Initializes a new instance of the <see cref="CustomMetaTable"/> class.
    /// </summary>
    /// <param name="metaModel">The entity meta model.</param>
    /// <param name="tableProvider">The entity model provider.</param>
    public CustomMetaTable(MetaModel metaModel, TableProvider tableProvider) :
        base(metaModel, tableProvider) { }

    /// <summary>
    /// Initializes a new instance of the <see cref="CustomMetaTable"/> class.
    /// </summary>
    /// <param name="metaModel">The meta model.</param>
    /// <param name="tableProvider">The table provider.</param>
    /// <param name="getVisibleColumns">Delegate to get the visible columns.</param>
    public CustomMetaTable(
        MetaModel metaModel, 
        TableProvider tableProvider, 
        CustomMetaModel.GetVisibleColumns getVisibleColumns) :
        base(metaModel, tableProvider)
    {
        _getVisdibleColumns = getVisibleColumns;
    }

    protected override void Initialize()
    {
        base.Initialize();
    }

    public override IEnumerable<MetaColumn> GetScaffoldColumns(
        DataBoundControlMode mode, 
        ContainerType containerType)
    {
        if (_getVisdibleColumns == null)
            return base.GetScaffoldColumns(mode, containerType);
        else
            return _getVisdibleColumns(base.GetScaffoldColumns(mode, containerType));
    }
}

Listing 2 – Custom MetaTable class

In CustomMetaTable we have the default constructor and a custom constructor again we passing the delegate into the custom constructor. Now in the only method we are overriding we either call the base GetScaffoldColumns or our delegate if is has been set. And that’s it as far as the Meta Classes are concerned.

The Attribute

[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public class HideColumnInAttribute : Attribute
{
    public PageTemplate PageTemplate { get; private set; }

    public HideColumnInAttribute() { }

    public HideColumnInAttribute(PageTemplate lookupTable)
    {
        PageTemplate = lookupTable;
    }
}

Listing 3 – HideColumnInAttribute

Listing 3 is the HideColumnIn attribute see Dynamic Data - Hiding Columns in selected PageTemplates for details on this attribute.

public static class ControlExtensionMethods
{
    // "~/DynamicData/PageTemplates/List.aspx"
    private const String extension = ".aspx";

    /// <summary>
    /// Gets the page template from the page.
    /// </summary>
    /// <param name="page">The page.</param>
    /// <returns></returns>
    public static PageTemplate GetPageTemplate(this Page page)
    {
        try
        {
            return (PageTemplate)Enum.Parse(typeof(PageTemplate),
                page.RouteData.Values["action"].ToString());
        }
        catch (ArgumentException)
        {
            return PageTemplate.Unknown;
        }
    }
}

Listing 4 – GetPageTemplate extension method.

Updated: Here the GetPageTemplate extension method has been updated thanks to beeps4848 from the DD forum see this thread How to cast integer values as an array of enum values? where he makes this cool suggestion of using the RoutData to get the action name, so maybe the attribute should now be HideColumnInAction ot the Enum ActionName (PageActions has been used).
[Flags]
public enum PageTemplate
{
    // standard page templates
    Details         = 0x01,
    Edit            = 0x02,
    Insert          = 0x04,
    List            = 0x08,
    ListDetails     = 0x10,
    // unknown page templates
    Unknown         = 0xff,
}

Listing 5 – PageTemplate enum.

In Listing 4 we have the new improved GetPageTemplate extension method now you don’t have to change each page to inherit DynamicPage you can just call the Page.GetPageTemplate() to find out which page you are on. it required the PageTemplate enum in listing 5.

The Delegate Methods

public static IEnumerable<MetaColumn> GetVisibleColumns(IEnumerable<MetaColumn> columns)
{
    var visibleColumns = from c in columns
                         where IsShown(c)
                         select c;
    return visibleColumns;
}

public static Boolean IsShown(MetaColumn column)
{
    // need to get the current page template
    var page = (System.Web.UI.Page)System.Web.HttpContext.Current.CurrentHandler;
    var pageTemplate = page.GetPageTemplate();

    var hideIn = column.GetAttribute<HideColumnInAttribute>();
    if (hideIn != null)
        return !((hideIn.PageTemplate & pageTemplate) == pageTemplate);

    return true;
} 

Listing 6 – Column generator methods

Now we need to supply our own column generator methods, in Listing 6 we have two methods the first GetVisibleColumns (and the name does not need to be the same as the Delegate) is the one we pass into the MetaModel, and the second IsHidden is where we test to see if the column should be hidden or not.

Adding To Web Application

Now we need to put these into our sample web application.

 public class Global : System.Web.HttpApplication
 {
     private static MetaModel s_defaultModel = new CustomMetaModel(GetVisibleColumns);
     public static MetaModel DefaultModel
     {
         get { return s_defaultModel; }
     }
    // other code ...
}

Listing 7 – Adding to Global.asax

So all we have to do is change the default value in Global.asax from

private static MetaModel s_defaultModel = new MetaModel();

to

private static MetaModel s_defaultModel = new CustomMetaModel(GetVisibleColumns);

Now all column generation throughout the site is handled by the GetVisibleColumns method from Listing 6.

[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; }
        public Object OrderDate { get; set; }
        [HideColumnIn(PageTemplate.List)]
        public Object RequiredDate { get; set; }
        public Object ShippedDate { get; set; }
        public Object ShipVia { get; set; }
        [HideColumnIn(PageTemplate.List)]
        public Object Freight { get; set; }
        public Object ShipName { get; set; }
        [HideColumnIn(PageTemplate.List)]
        public Object ShipAddress { get; set; }
        [HideColumnIn(PageTemplate.List)]
        public Object ShipCity { get; set; }
        [HideColumnIn(PageTemplate.List)]
        public Object ShipRegion { get; set; }
        [HideColumnIn(PageTemplate.List)]
        public Object ShipPostalCode { get; set; }
        [HideColumnIn(PageTemplate.List)]
        public Object ShipCountry { get; set; }
        // Entity Ref 
        public Object Customer { get; set; }
        // Entity Ref 
        public Object Employee { get; set; }
        // Entity Set 
        public Object Order_Details { get; set; }
        // Entity Ref 
        public Object Shipper { get; set; }
    }
}

Listing 8 – sample metadata.

Download

Note: The sample is for Visual Studio 2010 RC

Happy Coding

13 comments:

Denis Brulic said...

Hello Steve,

in this solution we lost following:
[HideColumnIn(PageTemplate.Edit, PageTemplate.Insert)]

So, no more multiple restrictions. How can we add more template pages in HideColumIn attribute?!

Functionality we need with Dynamic Data is Hide/Show columns dynamically based on metadata (works perfect with list view but not with edit or insert with your older solution (http://csharpbits.notaclue.net/2008/10/dynamic-data-hiding-columns-in-selected.html)!)

Thanx for help,
Denis

Steve said...

Hi Denis, not sure exactly what you want, just drop me an e-mail and I'll see what I can do.

Steve :D

Anonymous said...

Is Microsoft trying to make this easier for people, or a lot more complex? Is there going to be any end to abandoning old methods and going in for new, unnecessary ones?

Anonymous said...

Hi,
I am trying to use your sample with my DD web application , but for some reason he is not using my
MetaDatType.cs...
Do you have an idea why?

Steve said...

Hi there yes the usual reason that metadata is not recognised is namespace issues, my e-mail is at the top of the site e-mail me and I'll see what I can do.

Steve :)

Grant said...

Hey Steve,
Thank you for the great post.

I had the same problem as Dennis Brulic (2 June). I wanted to hide columns from two or more actions (e.g. List and Insert) at the same time. Here is my solution but would appreciate any feedback on improving it:

1. Created a 2nd HideColumn Attribute (e.g. for Insert) from Listing 3

[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public class HideColumnInsertAttribute : Attribute
{
public PageTemplate PageTemplate { get; private set; }
public HideColumnInsertAttribute() { }
public HideColumnInsertAttribute(PageTemplate lookupTable)
{
PageTemplate = lookupTable;
}
}

2. Edited the code from Listing 6. This example handles two actions (List and Insert) but could be edited to handle more:
public static Boolean IsShown(MetaColumn column)
{
// need to get the current page template
var page = (System.Web.UI.Page)System.Web.HttpContext.Current.CurrentHandler;
var pageTemplate = page.GetPageTemplate();
var hideIn = column.GetAttribute<HideColumnInAttribute>();
var hideInsert = column.GetAttribute<HideColumnInsertAttribute>();

if (pageTemplate == PageTemplate.Insert)
{
if (hideInsert != null) return !((hideInsert.PageTemplate & pageTemplate) == pageTemplate);
}

if (pageTemplate == PageTemplate.List)
{
if (hideIn != null)
return !((hideIn.PageTemplate & pageTemplate) == pageTemplate);
}
return true;
}

3. Use in metadata as appropriate (see listing 8):

[DisplayName("Field 1")]
[HideColumnIn (PageTemplate.List)]
[HideColumnInsert (PageTemplate.Insert)]
public string field1 { get; set; }

Hope this helps.

Cheers,
Grant

Steve said...

I'll redo this with some ideas I've had since.

Steve :)

Sachin said...

How can this be implemented in ListDetails.aspx template which has got both edit and insert forms in it?

Steve said...

this just works ford any page template, as it works at the meta model level

Steve

Sachin said...

In the ListDetails.aspx page, I have both Insert FormView and Edit GridView. I want set of columns to be hidden only in the FormView but must be shown in the gridview. Since the code sample given here works against a page template, I'm not sure how this can be implemented in this case.

Steve said...

I'll send you that sample I have if you email me direct

Steve

Christiaan said...

Hi Steve,

Thank you so much for sharing this with us, it's great!

Like Sachin, I also need to hide certain columns in the FormView but show them in the GridView inside of the ListDetails.aspx page.

Can you please show us how to implement such functionality?

Thank you in advance.

Steve said...

Hi Christian, send me an e-mail and I will explain a method you could use.

Steve