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

29 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

Stephen J. Naughton 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?

Stephen J. Naughton 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 :)

Anonymous 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

Stephen J. Naughton 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?

Stephen J. Naughton 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.

Stephen J. Naughton 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.

Stephen J. Naughton said...

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

Steve

Anonymous said...

Hi Steve,

Where and how to call the extension methods. My requirement is like I want to show a column in Insert.aspx but not in other pages.
Please provide in detail.

Stephen J. Naughton said...

Hi there, that is what this sample will do, there is no extension methods to call you just replce the metsmodel with this one and then add you attribute and it all just works.

Steve

Anonymous said...

But the problem is I am not able to pass getVisiblecolumns in CustomMetaModel. In Global.asax I am doing like this -
private static MetaModel s_defaultModel = new CustomMetaModel(); //MetaModel();

and in MetaData class -
public class EmployeeMetadata
{
[HideColumnIn(PageTemplate.List)] [UIHint("TextPassword")]
public object Pass{get; set;}
}

And how to pass one more PageTemplate.Edit in HideColumnIn

Please provide in detail.

Thanks

Stephen J. Naughton said...

You don't need to pass anything in in Global.asax you just add the attribute to you buddy/metadata classes.

Stephen J. Naughton said...

Try downloading and looking at the sameple.

Steve

Anonymous said...

Steve,

It is not working.
See my req. is I have to show a column in Insert and Edit Page
but not in List and Listdetails.

I am doing same as what you told but it's not working. May be I am wrong somewhere.
If you have any sample for same then please send to viv_bit at yahoo

Stephen J. Naughton said...

Can you send me a sample project that uses Northwind and I will test here?

Steve
P.S. my e-mail address is in the top right of the page.

Unknown said...

Hi,

I have implemented the solution you provided to hide the columns in specific template. I am getting an error at global.asax
Error "Could not find an implementation of the query pattern for source type 'System.Collections.Generic.IEnumerable'. 'Where' not found. Are you missing a reference to 'System.Core.dll' or a using directive for 'System.Linq'?
I am facing this error at method
_____________*****________________
public static IEnumerable GetVisibleColumns(IEnumerable columns)
{
var visibleColumns = from c in columns
where IsShown(c)
select c;
return visibleColumns;
}
_________******_____________
I have written this method in global.asax.cs file.
please help me.

Stephen J. Naughton said...

Hi Navneet, not sure where you are getting this error, have you downloaded the sample and tested that?

Steve

Unknown said...

Hi Steve,

I don't have northwind DB so that's why not able to run the project completely. I am getting an error at line:-
var visibleColumns = from c in columns. columns variable is providing the issue.

Stephen J. Naughton said...

you can get it from here
Northwind

Steve

Anonymous said...

Hi Steve,

This is the post I pointed to from your blog on the older method of hiding columns.

This code is converting to vb.net well, with one unfortunate exception.
In the global.asax file you have this line of code:

Private Shared s_defaultModel As New CustomMetaModel(GetVisibleColumns()

which is not passing any parameters, but the function in global.asax is expecting a collection of columns:

Public Shared Function GetVisibleColumns(columns As IEnumerable(Of MetaColumn)) As IEnumerable(Of MetaColumn)
Dim visibleColumns = From c In columns Where IsHidden(c) Select c
Return visibleColumns
End Function

- so it won't compile and I don't know what to pass into it. I'm thinking the problem has to do with the delegate, but I don't know how to solve it.

Thanks alot!

John (Don't push yourself to answer this in a hurry, I know you are on the mend!)

Anonymous said...

Hay, did you ever solved that, i have the same problem?

Stephen J. Naughton said...

Hi there I have not solved the issue as there has been no one to do the VB conversion for me I'm a C# coder. It need someone doing a lot of VB to do it and I don't know any experienced VB programmers sorry :(

Steve

julealgon said...

Hi there, awesome post here BTW.

I stumbled on it after searching for random things to solve my problem here:
http://stackoverflow.com/questions/20497450/asp-net-dynamic-data-full-scaffolding-without-datacontext-objectcontext

I wonder if you could take a look and suggest a workaround there, seeing as you are very knowledgeable on Dynamic Data :)

Cheers,
Juliano

Stephen J. Naughton said...

I posted a reply to the thread on StackOverflow

Steve