Monday, 20 October 2008

Dynamic Data - Hiding Columns in selected PageTemplates

This is a return to IAutoFieldGenrators, what we are trying to do is specify what page template to hide columns on.

First the Attribute class:

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

    public HideColumnInAttribute()
    {
        PageTemplates = new PageTemplate[0];
    }

    public HideColumnInAttribute(params PageTemplate[] lookupTable)
    {
        PageTemplates = lookupTable;
    }

    public static HideColumnInAttribute Default = new HideColumnInAttribute();
}

public enum PageTemplate
{
    Details,
    Edit,
    Insert,
    List,
    ListDetails,
    // add any custom page templates here
}

Listing 1 - HideColumnIn attribute

here you pass an array of pages that the column should be hidden in.

[MetadataType(typeof(OrderMetadata))]
public partial class Order
{
    public class OrderMetadata
    {
        [DisplayName("Order Date")]
        [HideColumnIn
            (
                PageTemplate.Details, 
                PageTemplate.Edit, 
                PageTemplate.Insert
            )]
        public Object OrderDate { get; set; }
    }
}

Listing 2 - sample metadata for Northwind's Orders table

In Listing 2 we are saying that the OrderDate column should NOT be shown on the Details, Edit and Insert pages.

Next we need an IAutoFieldGenerator class for this feature.

public class HideColumnFieldsManager : IAutoFieldGenerator
{
    protected MetaTable _table;
    protected PageTemplate _currentPage;

    public HideColumnFieldsManager(MetaTable table, PageTemplate currentPage)
    {
        _table = table;
        _currentPage = currentPage;
    }

    public ICollection GenerateFields(Control control)
    {
        var oFields = new List<DynamicField>();

        foreach (var column in _table.Columns)
        {
            // carry on the loop at the next column  
            // if scaffold table is set to false or DenyRead
            if (!column.Scaffold ||
                column.IsLongString ||
                column.IsHidden(_currentPage))
                continue;

            var f = new DynamicField();

            f.DataField = column.Name;
            oFields.Add(f);
        }
        return oFields;
    }
}

public static class ExtensionMethods
{
    public static Boolean IsHidden(this MetaColumn column, PageTemplate currentPage)
    {
        var hideIn = column.Attributes.OfType<HideColumnInAttribute>().DefaultIfEmpty(new HideColumnInAttribute()).First() as HideColumnInAttribute;
        return hideIn.PageTemplates.Contains(currentPage);
    }
}

Listing 3 - the IAutoFieldGenerator class

!Important: It should be noted that all that is required if you already have an IAutoFieldGenerator class is to add the column.Hidden(_currentPage) test in the if statement that skips columns and also have the Extension methods class.

This IAutoFieldGenerator class and Extension methods class in Listing 3 simply test to see if the column is NOT to be shown on the current page passed in the constructor.

A bit more of an explanation is required for what is going on in the Extension method IsHidden. I tried using the DefaultIfEmpty with the Default method but could not get it to work smile_sad so I added the if null statement and set the hideIn to the defalut value if null.

protected void Page_Init(object sender, EventArgs e)
{
    DynamicDataManager1.RegisterControl(DetailsView1);
    table = DetailsDataSource.GetTable();
    DetailsView1.RowsGenerator = new HideColumnFieldsManager(table, PageTemplate.Details);
}

Listing 4 - sample implementation on the Details page

protected void Page_Init(object sender, EventArgs e)
{
    DynamicDataManager1.RegisterControl(GridView1, true /*setSelectionFromUrl*/);
    DynamicDataManager1.RegisterControl(DetailsView1);
    MetaTable table = GridDataSource.GetTable();
    GridView1.ColumnsGenerator = new HideColumnFieldsManager(table, PageTemplate.ListDetails);
    DetailsView1.RowsGenerator = new HideColumnFieldsManager(table, PageTemplate.ListDetails);
}

Listing 5 - sample implementation in the ListDetails page

Now each page needs to have a RowsGenerator or ColumnGenerator added as in Listings 4 & 5.

Download Project

Hope this helps smile_teeth

Steve

47 comments:

Ed said...

Great series of articles for getting started with EF - thanks for your efforts! I used the HideColumnIn feature - that is perfect for what I am trying to accomplish. However there is one thing that from hours of reading various web pages I don't understand: given that I have a series of tables where I track the creator / last modifier of the row and that all have a FK pointer back to a users table, how can I set the Created_By / Modified_By value - which I store as an int id in the session data - when a row is added / updated so I can mark the record correctly and the user cannot edit / change it? Thanks for any advice you can offer.

Ed said...

Also should have mentioned in last post: I want to do the update generically, not in each individual class. I want to do it in the SavingChanges method as I currently handle setting the create / modified date using code similar to this:
bool isNew = stateEntryEntity.State == EntityState.Added ? true : false;
int colOrdinal = entry.CurrentValues.GetOrdinal (isNew ? "Date_Created" : "Date_Modified");
if (colOrdinal > 0)
entry.CurrentValues.SetDateTime (colOrdinal, dateLastMaint);

Thanks.

Ed

Steve said...

Have a look at this article here "DynamicData - Automatic Column Update For Entity Framework" http://csharpbits.notaclue.net/2008/11/dynamicdata-automatic-column-update-for.html with a little work you could make it generic.

Steve :D

Randy said...

I've got this working on my site and it is great. Hid a bunch of columns in list but they show up in details, just as I want them to. However, some of them are bit columns and they still show up as filters in List view. How can I modify this to get rid of those?

Steve said...

Hi Randy, I'd add DD Futures project to my website have a look at the articles on my site for getting DD Fufture Filters working on a file based website. The start here: http://csharpbits.notaclue.net/2008/09/dynamic-data-futures-advanced-filters.html

with this you can choose the Filter you want on a column and you can also disable them.

Steve :D

Randy said...

Exellent! I actually just got done adding DDFutures so I could do column ordering anyway so this looks like the next step. Your site is FANTASTIC by the way. I'm a DBA who knows just enough C# to be dangerous so I'd be lost without your articles. :)

Steve said...

Hi Randy, I'm a C# who knows just enough about DB's to be dangerous.

Glad you like the site.

Steve :D

bruno grandjean said...

Hi,
I am using PK with identity increment in my model.
In using Templates & insertion mode user have to fill the PK which doesn't make sense.
How can I automatically increment my PK? A solution could be to set a default value to zero & hide the field but I hope a better solution exists.
all the best
bruno

Steve said...

The best place to post this question would be the Dynamic Data forum http://forums.asp.net/1145.aspx as this conversation could become quite protracted here.

Steve

P.S. when posting on the forum pleade mention the provider Linq to SQL / Entity Framework, try to use a well know Database such as Northwin and post a detailed description of the issue and I will be glad to help.

Merritt said...

Thx for the code. I am using Dynamic Data for a project I am currently working on for some peeps with absurdly unrealistic expectations on the delivery time frame and your posts constantly save me hours of work and provide inspiration and insight into how to fully realize and extend the potential of this slick new framework.

Merritt said...

Unfortunately, I had some difficulty implementing this solution withASP.NET Dynamic Data 4.0 Preview 2. It sort of breaks the functionality of some of the added attributes, like the new DisplayAttribute: you lose the ability to order your columns.

Maybe I am doing things wrong or missed something.

Anyways, my solution, was to include their source code for the DefaultAutoFieldGenerator (and all System.Web.DynamicData.Util) and add to their loop over Metatable.GetScaffoldColumns() a check for MetaColumn.IsHidden (your extension method):

foreach (MetaColumn column in this._metaTable
.GetScaffoldColumns(
mode,
controlContainerType
))
{
if (column.IsHidden(_currentPage)) break;

They could have made life so much easier if they just exposed all the classes in Util, or by putting some hooks in for DefaultAutoFieldGenerator for additional filtering, or by at least exposing their static methods as protected instead of private and GenerateFields as virtual... or something.

Sorry for the rant, but I feel it's absurd that I had to include so much of their source code just to add one line to their default auto field generator.

Steve said...

Hi Merritt, I've not done any work with ASP.NET Dynamic Data 4.0 Preview 2 as yet, but I do know that some of the extra fuctionality you fine here is already built in to the preview, such as column ordering. I'm very busy with a paying project at the moment and so have not had the time to play with the preview but pretty soon I'm going to if I hope to keep the edge.

Steve :D

Richard said...

Hey Steve-

Quick question: in GenerateFields where you say...

if (!column.Scaffold ||
column.IsLongString ||
column.IsHidden(_currentPage))
continue;

What is the purpose of checking column.IsLongString there? When I implement this it fails to render nvarchar(max) fields in any view, which DD would typically render as multi-line text edit. I want these fields, just never in list view, so I changed mine to:

if (!column.Scaffold ||
(column.IsLongString && _currentPage == PageTemplate.List ) ||
column.IsHidden(_currentPage))
continue;

But wouldn't this typically be something we'd handle in the model (meaning you could remove the IsLongString check entirely)? Was just curious if you had a specific reason for excluding that field type in all views.

Steve said...

column.IsLongString is equivalent to the check that the default column generator does for List pages and suppresses large text fields being displayed.

Steve :D

Anonymous said...

Yes, that I need ! Tks.

Anonymous said...

This is a great post. Not only useful as is, but shows how to go about extending dynamic data as well. Kudos and thanks.

I have a question though, because I am using Preview 4 and the Page Templates and Page_Inits under Preview 4 are a bit different. For example, the ListDetails.aspx actually uses a FormView instead of a DetailsView, and as far as I can tell so far there is no ColumnsGenerator or RowsGenerator or anything similar on the FormView. Do you have an example of using the custom IAutoFieldGenerator under Preview 4?

Steve said...

No see my latest post here http://csharpbits.notaclue.net/2009/06/great-buried-sample-in-dynamic-data.html this is a great hidden gem in Preview 4.

Steve :D

Anonymous said...

Hi Steve,

I tried using your code and it worked but then I realized it was conflicting with my rowOrder attribute that I created to order the rows in the detailsview.
In my ListDetails.aspx, when I put the HiddenColumnManager code after the code for ordering rows, it hides the row but doesn't order the rows and if I put it before it doesn't hide the row, but the order is working correctly...

Have you got any ideas?

Steve said...

If you send me an e-mail I have a sample with Column Order and Hide combined.

Steve :D

Anonymous said...

You are great. Thank you

Anonymous said...

Really great. Really. ^^

David said...

Really impressive blog. thx !
Just a question : I have about 100 tables with their own columns and also 10 identical columns.
I would like to hide the 10 identical columns in each table, but I try to be generic. I don't want to hide my 100*10 columns one by one.
I don't use the code of this tutorial (but I could), I just use the [ScaffoldColumn("false")] property (because I need to hide columns in each page).

Have you got an idea ? Thx
David.
PS : excuse my bad english.

Steve said...

Hi David, drop me an e-mail and I think I can give you an idea or two to help streamline this.

Steve :D

David said...

Is there exist a similar method to hide data rows ?
For example is I want to display only rows whose "isActive" attribute is set to True.

Thx.

Steve said...

I usually create a custom filter either using the filtering provided by the Old Futures project which had all the source, of using Preview 4 DD v2 bits. I hide the filter but still return the filter value.

Steve :D

Mona said...

Thank you for this article (and all the others!).

I have implemented Dynamic Data across my DAL and my presentation layer. I had to add the Attribute class in my DAL so I could refer to it in my classes that represent each tables. But I had to insert the IAutoFieldGenerator in my presentation layer ortherwise the line for ColumnsGenerator wouldn't compile ("Cannot implicitly convert HideColumnFieldsManager to IAutoFieldGenerator"). Do you think it is possible to add this class in the DAL as well? Because now I have dependency between my presentation and my DAL. Thanks!

Steve said...

I can't see why you can't and I don't understand the error you are getting is sounds like there is a namespace issue because HideColumnFieldsManager implements IAutoFieldGenerator.

But again I thinks this should be doable.

Steve :D

Mona said...

Just to let you know, I was just missing a System.Web reference in my DAL project....

Steve said...

Goo glad you sorted it.

Steve :D

Anonymous said...

Hi

I have used your code for to hide the columns only in insert.aspx page......but there is a problem....don´t save the new data (it´s a datetime)

Steve said...

Sorry but without understanding your schema I can't tell why this would be.

Steve :D

Anonymous said...

Hi Steve,

I tried this and it works perfect, but it's messing up the paging on the List.aspx page. Any ideas?

Steve said...

Hi I've got dosens of projects using HideColumnIn. I can send you aworking sample that uses Northwind for you to test, just send an e-mail to the e-mail address on my blog.

Steve :D

Anonymous said...

You rock - thank you for this solution, it worked great.

Philip said...

what would be the current one to expand the things over to experience?

Philip
Web Based Application Development

Steve said...

Sorry Philip I don't quite understand what it is you want...

Steve :)

Drop me an e-mail, my address is at the top of my blog.

Brian said...

Hi,

I am trying to implement this in vb.net and having an issue. I know this is C# bits, but perhaps you can help. My problem is passing the array of PageTemplates in the partial class. VB doesnt allow the declaration of an Array the same way C# does and every other way i try to declare the array i get compile time error about "Contant expression is required" How can I pass this list of PageTemplates to the attribute class?

Steve said...

Hi Brian, I think it should look like this:
Public Sub New(ParamArray lookupTable As PageTemplate())
PageTemplates = lookupTable
End Sub

For that method.

Steve

Matthew Paul said...

Hi Steve, excellent blog. I'm experimenting with Dynamic data, and you're code's been very handy making the who scaffolding side of things at all useful.

One query: I've noticed in hiding columns, if the page for example is '/Companies/List.aspx', the columns hide, but '/Companies/list.aspx' does not. I've worked around this by adding the lower case version to the PageTemplate enum like so:

List = 0x08,
list = 0x08,

Is there a nicer way to make it not case sensitive?

Steve said...

You could use the Action from the route to find out which page it is, I'll look into it for the next version (DD4 versoin)

Steve :D

Anonymous said...

hi,
in VS 10 Page_init of DD need dictionary parameter.
table = DynamicDataRouteHandler.GetRequestMetaTable(Context);
GridView1.SetMetaTable(table,
table.GetColumnValuesFromRoute(Context));

I didn't succeed to work around it...

Steve said...

Hi Anonymous, this is not compatible with DD4 and VS2010 you need to look at A New Way To Do Column Generation in Dynamic Data 4 DD4 uses Entity Templates which rely on FormView which you can use FormView1.ColumnsGenerator =

Steve :D

Anonymous said...

Hi Steve,

I'm using DD4 in VS2010. FormView doesn't have ColumnsGenerator property. Do I have to extend the FormView?

Steve said...

have a look at this article here A New Way To Do Column Generation in Dynamic Data 4

Steve :D

Anonymous said...

Hi Steve,

Your code is cool as always - usually completed for me too, but mostly I understand what's going on here.

However, I'm having a problem - its vb.net (vs 2010) so I don't know if you care to deal with this, but here it goes:

I'm trying to implement this on a page List but the GenerateFields function in the HideColumnFieldsManager Class never fires. The constructor for the class does, though. I'm at a loss for ideas (I converted all your code to vb.net via a conversion utility on the web). I had to convert the extensionmethods class to a module because the converter added this above the IsHidden function: _, but ishidden is called from GenerateFields so I don't think that is the problem and I tried it as a clss w/o the compiler stmt & it didn't work either.

I'd appreciate any ideas
Thanks, so much!

John

Steve said...

hi this should still work but have a look at A New Way To Do Column Generation in Dynamic Data ... this is the better way as this will work for all pages.

Steve
P.S. sorry for the delay I am recovering from a heart attack and then a triple heart bypass surgery :(

Anonymous said...

Hi Steve,

I'm sorry to hear about your health - I hope you are well on the road to recovery! And no apologies ever needed for giving me all this free code and personalized help!

I have taken your advice and now I am trying to make the new column generation code work but I'm having a problem. I'm sure you'd rather see any questions about it posted on that page, so I will be posting it there shortly.

Thanks again, and get well soon!