Monday, 26 May 2008

DynamicData - Generate Columns/Rows (using IAutoFieldGenerator) - Part 5

Articles in this Series

IAutoFieldGenerator

This is the heart of controlling which fields are shown in the List, Details, Edit views etc. Listing 1 is a skeleton of the Column/Row Generator, as you can see it is a class that is passed a table and has a loop which returns a ICollection of DynamicFields. The foreach loop is where the columns are processed similarly to the if (!column.Scaffold) which tests to see if the column to scaffolded.

using System.Collections.Generic;
using System.Web.DynamicData;

public class FilteredFieldsManager : IAutoFieldGenerator
{
    protected MetaTable _table;

    public FilteredFieldsManager(MetaTable table)
    {
        _table = table;
    }

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

        // where do I put this test I think in the page?
        foreach (MetaColumn column in _table.Columns)
        {
            // carry on the loop at the next column 
            // if scaffold column is set to false
            if (!column.Scaffold)
                continue;

            // create new DynamicField
            DynamicField newField = new DynamicField();

            // assign column name and add it to the collection
            newField.DataField = column.Name;
            oFields.Add(newField);
        }
        return oFields;
    }
}

Listing 1

The class is used like:

// code to add column level security
table = GridDataSource.GetTable();
GridView1.ColumnsGenerator = new FilteredFieldsManager(table);

Listing 2

The code in Listing 2 is added to the Page_Init event, this is all that is needed in the pages code behind to add column level security.

Customising the FieldGenerator class

The first change is to add a new member variable _roles and change the Constructor to take an array of Strings.

protected MetaTable _table;
protected String[] _usersRoles;

public FilteredFieldsManager(MetaTable table, params String[] roles)
{
    _table = table;
    _usersRoles = roles;
}

Listing 3

Then in the GenerateFields method the permission for the current column will be assigned a local variable and then add a test to the if (!column.Scaffold) so that if the column is DenyRead then the column will also be skipped see Listing 4.

// get permissions for current column for current user roles.
var fieldPermissions = column.GetFieldPermissions(Roles.GetRolesForUser());

// carry on the loop at the next column 
// if scaffold table is set to false or DenyRead
if (!column.Scaffold  fieldPermissions.Contains(FieldPermissionsAttribute.Permissions.DenyRead))
    continue;

Listing 4

The next step is to add a DynamicReadonlyField class so that id a field is marked DenyEdit then a read only field can be returned instead of the regular DynamicField see Listing 5.

public class DynamicReadonlyField : DynamicField
{
    public override void InitializeCell(
        DataControlFieldCell cell,
        DataControlCellType cellType,
        DataControlRowState rowState,
        int rowIndex)
    {
        if (cellType == DataControlCellType.DataCell)
        {
            var control = new DynamicControl() { DataField = DataField };

            // Copy various properties into the control
            control.UIHint = UIHint;
            control.HtmlEncode = HtmlEncode;
            control.NullDisplayText = NullDisplayText;

            // this the default for DynamicControl and has to be
            // manually changed you do not need this line of code
            // its there just to remind us what we are doing.
            control.Mode = DataBoundControlMode.ReadOnly;

            cell.Controls.Add(control);
        }
        else
        {
            base.InitializeCell(cell, cellType, rowState, rowIndex);
        }
    }
}

Listing 5 - Thanks to David Ebbo for this class and the explanation here on the DynamicData forum.

Now if we add a test for the column has a DenyEdit security attribute assigned then the field can now be set to an DynanicReadOnlyField. In Listing 6 the fieldPermissions variable can be tested to see if it contains a DenyEdit permission.

DynamicField f;
if (fieldPermissions.Contains(FieldPermissionsAttribute.Permissions.DenyEdit))
{
    f = new DynamicReadonlyField();
}
else
{
    f = new DynamicField();
}

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

Listing 6

Listing 7 shows the code to test for foreign Key tables parent and child table permissions and if DenyRead do not show the field for the column.

//if foreign key table is hidden then hide the column that references it in this table
if (column.GetType() == typeof(MetaChildrenColumn))
{
    // Get permissions for current columns child table
    var childTablePermissions = column.GetChildrenTablePermissions(Roles.GetRolesForUser());

    // carry on the loop at next column
    if (childTablePermissions.Contains(TablePermissionsAttribute.Permissions.DenyRead))
        continue;
}

//if foreign key table is hidden then hide the column that references it in this table
if (column.GetType() == typeof(MetaForeignKeyColumn))
{
    // Get permissions for current columns parent table
    var parentTablePermissions = column.GetFkTablePermissions(Roles.GetRolesForUser());

    // carry on the loop at next column
    if (parentTablePermissions.Contains(TablePermissionsAttribute.Permissions.DenyRead))
        continue;
}

Listing 7

Put it all together in Listing 8.

public class FilteredFieldsManager : IAutoFieldGenerator
{
    protected MetaTable _table;
    protected String[] _usersRoles;

    public FilteredFieldsManager(MetaTable table, params String[] roles)
    {
        _table = table;
        _usersRoles = roles;
    }

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

        // Get table permissions
        var tablePermissions = _table.GetTablePermissions(this._usersRoles);

        // if table is DenyRead then do not output any fields
        if (!tablePermissions.Contains(TablePermissionsAttribute.Permissions.DenyRead))
        {
            foreach (MetaColumn column in _table.Columns)
            {
                // get permissions for current column for current user roles.
                var fieldPermissions
                    = column.GetFieldPermissions(Roles.GetRolesForUser());

                // carry on the loop at the next column 
                // if scaffold table is set to false or DenyRead
                if (!column.Scaffold  fieldPermissions
                    .Contains(FieldPermissionsAttribute.Permissions.DenyRead))
                    continue;

                // if foreign key table is hidden then hide
                // the column that references it in this table
                if (column.GetType() == typeof(MetaChildrenColumn))
                {
                    // Get permissions for current columns child table
                    var childTablePermissions
                        = column.GetChildrenTablePermissions(Roles.GetRolesForUser());

                    // carry on the loop at next column
                    if (childTablePermissions
                        .Contains(TablePermissionsAttribute.Permissions.DenyRead))
                        continue;
                }

                // if foreign key table is hidden then hide
                // the column that references it in this table
                if (column.GetType() == typeof(MetaForeignKeyColumn))
                {
                    // Get permissions for current columns parent table
                    var parentTablePermissions
                        = column.GetFkTablePermissions(Roles.GetRolesForUser());

                    // carry on the loop at next column
                    if (parentTablePermissions
                        .Contains(TablePermissionsAttribute.Permissions.DenyRead))
                        continue;
                }

                DynamicField f;
                if (fieldPermissions
                    .Contains(FieldPermissionsAttribute.Permissions.DenyEdit))
                {
                    f = new DynamicReadonlyField();
                }
                else
                {
                    f = new DynamicField();
                }

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

Listing 8

having done all this remember all you have to add to the page is:

// code to add column level security
table = GridDataSource.GetTable();
GridView1.ColumnsGenerator = new FilteredFieldsManager(table, Roles.GetRolesForUser());

SQL Server 2005

SQL Server 2008

the sample already has Login and Roles setup (Account details below).

Website users:

  • admin
  • fred
  • sue
  • pam

all passwords are: password

28 comments:

Cormac said...

Hi Steve,
I finally got around to looking into table and column permissions for my application. Your sample code is great. Thanks for that. One question though. I noticed when I was running up the sample app that although I logged in as Fred (which shouldn't have read permissions on the order_detail table) that if I manually manipulated the url then I could get access to the that table. It seems to me that marking metadata on tables and columns is a tool to aid in the display of database tables/columns and not to 100% lock out access. Is this correct?
Cormac

Steve said...

Hi Cormac, if you look at part 4 and the section entitled "Some Error Handling for Pages Reached with Tables that are DenyRead" you will see the code that need to be added to each page to stop what you said is happening. So it's a bug, I will review the download and update where nessacery.

Steve
P.S. have a look at this new series here DynamicData: Database Based Permissions - Part 1 on my blog :D

Anonymous said...

Steve,

You mentioned that you are using a Deny Pattern for your permissions, where you limit functionality as additional attributes are tagged.

I setup my application with users and roles as follows:


Roles: Admin, Consumer, Buyer

User A is an Admin, Consumer, and Buyer

User B is a Consumer and a Buyer

User C is a Consumer


... you can see where I'm going.


When I ran the application User A was being denied access to functionality, this is because one of the other roles this person had was denied functionality. To get everything working with role based permissions setup like I just described I had to refactor the HasAnyRole method in the FieldPermissionsAttribute class to HasAllRoles:

public Boolean HasAllRoles(String[] roles)
{
foreach (var role in roles)
{
if (!HasRole(role))
return false;
}

return true;
}


Thoughts on this?

Steve said...

Yep thats the way I made it, of course you could make the logic more complicated, but I was just showing how and that level of complexity will suit more peoples uses :D

Steve

Anonymous said...

Steve,

Thanks for your excellent work. Your explanations and code help me to understand DD a lot better :-)

Thanks again,

Sylvester

SuicideSquad said...

Steve,

This is the init of list.aspx.cs:
protected void Page_Init(object sender, EventArgs e) {
table = DynamicDataRouteHandler.GetRequestMetaTable(Context);
GridView1.SetMetaTable(table);
GridDataSource.EntityTypeFilter = table.EntityType.Name;

table = GridDataSource.GetTable();
GridView1.ColumnsGenerator = new FilteredFieldsManager(table);
}

I added the class to the project as well, But my app does not go into public ICollection GenerateFields(Control control).
What is going wrong?

Steve said...

Hi Suicidesquad, from the code you have posted it looks like you are using either Preview 4 or Beta 1 VS2010, I have not tested in this environment yet so I'm not sure what the reason is but I will look into it ASAP.

Steve :D

Steve said...

Aparently there IAutoFieldGenerator works the same as in V1 according to my contacts. So I'll have a quick look later and see what happens. I'll use Preview4 and Beta 1 to test this.

Steve :D

Anonymous said...

Hi Steve
Is there a way to add a further column to aps:DetailsView using IAutoFieldGenerator interface?
What I want to achieve is, that a new column with units or status information is generated on detailsview.

Steve said...

I think you could but there is probably a better way of doing this, can you be more specific.

Steve ;D

Anonymous said...

Okay

I have an Attribute like this

[AttributeUsage(AttributeTargets.Property)]
public class UnitAttribute : Attribute
{
public String Unit { get; private set; }

public UnitAttribute()
{
Unit = "-";
}

public UnitAttribute(String UnitExpression)
{
Unit = UnitExpression;
}

public static UnitAttribute Default = new UnitAttribute();
}

I can use this attribute to decorate my column with. What i now want to see on my detailsview on dynamic data is a third column containing this unit attribute.

I have now idea what happens in the background of "RowsGenerator" if i set

DetailsView1.RowsGenerator = new AutoFieldsManager(table, PageTemplate.Details, Roles.GetRolesForUser());

So, how can i achieve it?

Thanks for your help

Steve said...

Have a look at Listing 8 FilteredFieldManager for details of what goes on in side an IAutoFieldGenerator.

Steve :D

Anonymous said...

Hi Steve

Sorry for my late reply.

Hm, I'm sorry, but i don't get the answer from your last post? *confused

So let me ask in another way. Is it possible to return a 2-dimensional list of DynamicField from GenerateFields function (Listing 8) to force DetailsView.RowsGenerator to create a third column on detailsview?

Sorry, for my bad english. I hope it's now clearer.

Thanks for your help

Loopmaster

Steve said...

The IAutoFieldGenerator returns a list ot type DynamicFiled your problem is that there is no column so it may be possible to add an extra DynamicField that does not match a Column and map it to a FieldTemplate via UIhint.

Anonymous said...

I will give it a try. At least it could work for a gridview. But I don't think, that I can use this I idea for a detailsview, because there a table column becomes a row. That's not what I wanted a achieve.

Anyway. Thanks for your help
Loopmaster

Anonymous said...

Hi Steve,

How would you do the same without IAutoFieldGenerator (using FormView in Detail, Edig and Insert pages)...

Thanks!!

Steve said...

Hi there :) I have an article here Dynamic/Templated FromView this generates the Templates for a FormView. If you are using VS2010 then look at this article here
A New Way To Do Column Generation in Dynamic Data 4
Hope this helps.

Steve :D

Anonymous said...

I'm using vs2010 (also use CustomMetaTable and CustomMetaModel like you use in "A New Way To Do Column Generation in Dynamic Data 4"), but I don 't know how to asign DynamicReadonlyField/DynamicField in Fields of FormView

Sorry for my bad english.

Thanks again !!

Steve said...

Your English is better than my say French :) Have a look at this old post based on Preview 4 here Securing Dynamic Data Preview 4 Refresh I am working on a RTM Sample at the moment :)

Steve

Andrey Druz said...

How to add 2 IAutoFieldGenerator on Page_Init?

GridView1.ColumnsGenerator = new HideColumnFieldsManager(table, PageTemplate.List);
GridView1.ColumnsGenerator = new FilteredFieldsManager(table, Roles.GetRolesForUser());

Steve said...

You can't as they return a list of Fileds not columns.

If your using DD4 and VS2010 then I have a better way that can be chained see
A New Way To Do Column Generation in Dynamic Data 4
you couls use a delegate to generate columns and you could also allow then to be chained if you wanted.

Steve :D

Andrey Druz said...

Thanks.

vivek said...

Hi Steve,

Below is my code for Page_Init in List.aspx -
table = DynamicDataRouteHandler.GetRequestMetaTable(Context);
GridView1.SetMetaTable(table, table.GetColumnValuesFromRoute(Context));
GridDataSource.EntityTypeFilter = table.EntityType.Name;

table = GridDataSource.GetTable();
GridView1.ColumnsGenerator = new FilteredFieldsManager(table, Session["dept"].ToString());

But it is not calling GenerateFields() from FilteredFieldsManager.

Please suggest asap.

Steve said...

You may need to move the line to the Page_Load

GridView1.ColumnsGenerator = new FilteredFieldsManager(table, Session["dept"].ToString());

Steve

vivek said...

Thnks Steve. Its working.
But in Edit page fields are coming.

For that I am using same code with Formview1, but RowGenerator is not coming.
Please reply

Steve said...

Hi vivek, e-mail me direct not sure what your issue is, my e-mail is in the top right of my blog.

Steve

vivek said...

Steve,

My issue is how to implement same like what we did in list.apsx for Edit.aspx page. so that those columns should not come for particular role.
Sorry I can't mail you from here.

Steve said...

I think you are on .Net 4 so you will need to look at my new Column generation here A New Way To Do Column Generation in Dynamic Data 4 this gets rid of the requirement to use field generators and simplifies everything. Also you cant use column generators with the FormView.

Steve