- Marcin Dobosz blog on Extending the FilterRepeater Sample
- Marcin Dobosz blog on Custom metadata providers
- Maíra Wenzel's blog Dynamic Data Attributes
- Latest DynamicData bits .NET Framework 3.5 Service Pack 1 Beta & ASP.NET Dynamic Data Preview see David Ebbo's Blog for Clearing some confusion about Dynamic Data and VS SP1 Beta
- David Ebbo's Dynamic Data, AJAX and JavaScript errors and how to make the errors obscured by AJAX visible.
- Roger Jennings of OakLeaf Systems Blog has a post Eliminate Ambiguous Object References when Customizing ADO.NET Dynamic Data Pages in Web Apps which covers pitfalls creating a DynamicData Web Application as opposed to a file system based website.
ASP.Net, Dynamic Data and c# stuff: Focused on what new and cool, code tutorials and useful tricks and tips on getting the most out of ASP.Net. All in C#
Saturday 31 May 2008
Today's Interesting Links
Friday 30 May 2008
DynamicData - Updating the ListDetails Page - Part 7
Articles in this Series
- Introduction - A DynamicData Attribute Based Permission Solution using User Roles.
- Part 1 - Permissions Attribute (Metadata) Classes.
- Part 2 - Sample Metadata for project.
- Part 3 - The Helper Extension Methods.
- Part 4 - Limit Tables shown on Default page and List, Edit & Details etc.
- Part 5 - Generate Columns/Rows (using IAutoFieldGenerator)
- Part 6 - Miscellaneous bits
- Part 7 - Updating the ListDetails Page
- DynamicData - Limit the Filter Fields
- DynamicData - Automatic Column Update
Updating the ListDetails Page
Similarly to the List page Listing 1 show the class variables.
protected Boolean denyEdit = false; protected Boolean denyDelete = false; protected Boolean denyDetails = false; protected Boolean denyInsert = false;
Next is the setting of the class variables based on the permissions set returned.
// Get permissions for current table var tablePermissions = table.GetTablePermissions(Roles.GetRolesForUser()); // get status of links for column 0 from table permissions if (tablePermissions.Contains(TablePermissionsAttribute.Permissions.DenyEdit)) denyEdit = true; if (tablePermissions.Contains(TablePermissionsAttribute.Permissions.DenyDelete)) denyDelete = true; if (tablePermissions.Contains(TablePermissionsAttribute.Permissions.DenyDetails)) denyDetails = true; if (tablePermissions.Contains(TablePermissionsAttribute.Permissions.DenyInserts)) denyInsert = true;
This is the error if user got to this by invalid URL for them (e.g. old link or a hack)
// if table is denied read throw error if (tablePermissions.Contains(TablePermissionsAttribute.Permissions.DenyRead)) { Response.Redirect("~/Default.aspx?error=No access to " + table.Name); }
Here the links and DetailsPanel are set to display or hide, also if the DetailsPanel is hidden the don't bother to turn off the buttons.
// set visibility of Details DetailsPanel.Visible = !denyDetails; GridView1.AutoGenerateSelectButton = !denyDetails; GridView1.AutoGenerateEditButton = !denyEdit; GridView1.AutoGenerateDeleteButton = !denyDelete; if (!denyDetails) { DetailsView1.AutoGenerateInsertButton = !denyInsert; DetailsView1.AutoGenerateEditButton = !denyEdit; DetailsView1.AutoGenerateDeleteButton = !denyDelete; }
I think that about wraps it up for this...
But may be not I think next I may override some of the controls then there will be less code to add per page.
NOTE: you will need to change the routing to see ListDetails pages instead of List, Details, Edit, Insert.
Wednesday 28 May 2008
DynamicData - Miscellaneous bits - Part 6
Articles in this Series
- Introduction - A DynamicData Attribute Based Permission Solution using User Roles.
- Part 1 - Permissions Attribute (Metadata) Classes.
- Part 2 - Sample Metadata for project.
- Part 3 - The Helper Extension Methods.
- Part 4 - Limit Tables shown on Default page and List, Edit & Details etc.
- Part 5 - Generate Columns/Rows (using IAutoFieldGenerator)
- Part 6 - Miscellaneous bits
- Part 7 - Updating the ListDetails Page
- DynamicData - Limit the Filter Fields
- DynamicData - Automatic Column Update
Disabling Insert hyperlink on List and Details etc
Listing 1 shows the code to add to pages to disable the insert button.
// get user permissions var tablePermissions = table.GetTablePermissions(Roles.GetRolesForUser()); // Disable insert hyperlink in inserts denied. if (tablePermissions.Contains(TablePermissionsAttribute.Permissions.DenyInserts)) { InsertHyperLink.Visible = false; }
Listing 1
Removing; Edit, Delete, Details hyperlinks from Lists
Listing 2 is added to the List page as class variables and Listing 3 is added to the Page_Load, it will remove the whole column if all three class variables are true.
protected Boolean denyEdit = false; protected Boolean denyDelete = false; protected Boolean denyDetails = false;
// get user permissions var tablePermissions = table.GetTablePermissions(Roles.GetRolesForUser()); // get status of links for column 0 from table permissions if (tablePermissions.Contains(TablePermissionsAttribute.Permissions.DenyEdit)) denyEdit = true; if (tablePermissions.Contains(TablePermissionsAttribute.Permissions.DenyDelete)) denyDelete = true; if (tablePermissions.Contains(TablePermissionsAttribute.Permissions.DenyDetails)) denyDetails = true;
// if all buttons are hidden the hide the column if (denyDelete && denyDetails && denyEdit) { GridView1.Columns.RemoveAt(0); // remove the event handler if not needed GridView1.RowDataBound -= GridView1_RowDataBound }
To remove individual hyperlinks an OnRowDataBound event handler needs to be added see Listing 4.
<asp:GridView ID="GridView1" runat="server" DataSourceID="GridDataSource" AllowPaging="True" AllowSorting="True" CssClass="gridview" OnRowDataBound="GridView1_RowDataBound">
The same class variables are used here to turn off individual hyperlinks (
// manage security on Edit Delete links protected void GridView1_RowDataBound(object sender, GridViewRowEventArgs e) { // don't bother to hide individual links if they are all hidden if (e.Row.RowType == DataControlRowType.DataRow)// && !(!denyDelete && !denyDetails && !denyEdit)) { var controls = e.Row.Cells[0].Controls; if (controls.Count > 0) { foreach (var c in controls) { if (c.GetType() == typeof(HyperLink)) { var h = c as HyperLink; if ((h.Text.ToLower() == "edit" && denyEdit) || (h.Text.ToLower() == "details" && denyDetails)) { ((HyperLink)c).Visible = false; } } else if (c.GetType() == typeof(LinkButton)) { var h = c as LinkButton; if (h.Text.ToLower() == "delete" && denyDelete) { ((LinkButton)c).Visible = false; } } } } } }
Note: if all three class variable are true you could add the && !(!denyDelete && !denyDetails && !denyEdit) to the if statement and remover the GridView1.RowDataBound -= GridView1_RowDataBound line from Page_Load if statement, if you you still need to process the rows for some other reason.
Next the ListDetailsPage.
Monday 26 May 2008
DynamicData - Generate Columns/Rows (using IAutoFieldGenerator) - Part 5
Articles in this Series
- Introduction - A DynamicData Attribute Based Permission Solution using User Roles.
- Part 1 - Permissions Attribute (Metadata) Classes.
- Part 2 - Sample Metadata for project.
- Part 3 - The Helper Extension Methods.
- Part 4 - Limit Tables shown on Default page and List, Edit & Details etc.
- Part 5 - Generate Columns/Rows (using IAutoFieldGenerator)
- Part 6 - Miscellaneous bits
- Part 7 - Updating the ListDetails Page
- DynamicData - Limit the Filter Fields
- DynamicData - Automatic Column Update
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; } }
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; }
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
DynamicData - Limit Tables shown on Default page and List, Edit & Details etc - Part 4
Articles in this Series
- Introduction - A DynamicData Attribute Based Permission Solution using User Roles.
- Part 1 - Permissions Attribute (Metadata) Classes.
- Part 2 - Sample Metadata for project.
- Part 3 - The Helper Extension Methods.
- Part 4 - Limit Tables shown on Default page and List, Edit & Details etc.
- Part 5 - Generate Columns/Rows (using IAutoFieldGenerator)
- Part 6 - Miscellaneous bits
- Part 7 - Updating the ListDetails Page
- DynamicData - Limit the Filter Fields
- DynamicData - Automatic Column Update
Limit Tables Shown on Start Page
See sample metadata in Listing 1
[TablePermissions(TablePermissionsAttribute.Permissions.DenyRead, "Sales")] public partial class Order_Detail {} [TablePermissions(TablePermissionsAttribute.Permissions.DenyRead, "Sales")] public partial class Employee {} [TablePermissions(TablePermissionsAttribute.Permissions.DenyRead, "Sales")] public partial class Shipper {}
Listing 1
Now on the Default.aspx.cs a generic piece of code can be added that will remove any table with a DenyRead attribute for one of the current users roles.
System.Collections.IList visibleTables = MetaModel.Default.VisibleTables; // remove tables from the list if DenyRead String[] roles = Roles.GetRolesForUser(); foreach (var table in MetaModel.Default.Tables) { var permissions = ((MetaTable)table).GetTablePermissions(roles); if (permissions.Contains(TablePermissionsAttribute.Permissions.DenyRead)) visibleTables.Remove(table); }
The code in Listing 2 simply checks the permissions on each table for the current users Roles and if a DenyRead is encountered then the table is removed form the visibleTables collection.
Some Error Handling for Pages Reached with Tables that are DenyRead
In Listing 3 a list of permission for the current users roles are acquired from the GetTablePermissions helper extension method and if it contains a DenyRead then the page is redirected to the Default.aspx page with an error message in the URL.
// get user permissions var tablePermissions = table.GetTablePermissions(Roles.GetRolesForUser()); // if table is denied read throw error if (tablePermissions.Contains(TablePermissionsAttribute.Permissions.DenyRead)) { Response.Redirect("~/Default.aspx?error=No access to " + table.Name); }
Listing 3
DynamicData Sorting - FilterUserControl and ForeignKey_Edit.ascx user controls
Sorting - FilterUserControl and ForeignKey_Edit.ascx user controls
To sort either the Filter on a list page of the FK drop down lists, you can use DisplayColumnAttribute which can takes upto 3 parameters ColumnName, SortColumn and SortDecending (bool)
Excerpt from the qucik starts page
"When a data field is a foreign key of another table, Dynamic Data displays this column as a link to the other table. Dynamic Data uses the first column of type string as the link text when the relationship between the tables is a one-to-one relationship.
To change the column that is displayed as the value for the foreign key field, you must apply the DisplayColumnAttribute attribute to the partial entity class of the relationship table. You must create a partial class with the name of the table to be able to apply the attribute."
[DisplayColumnAttribute("PostalCode", "PostalCode", false)] public partial class Customer { }
The above image show the fully overloaded constructor for this attribute:
- string displayColumn - the name of the column to show when using this table in a DropDownList.
- string sortColumn - the name of the column to sort the DropDownList by
- bool sortDescending - set to true changes the sort to descending
So by adding this to the Customer table you can change how it is displayed in filter list and FK DropDownLists.
Above shows the Customers filter and Customer FK drop down list on the Orders table List and Edit pages.
Sunday 25 May 2008
Permissions Attribute class Helper Extension Methods - Part 3
Articles in this Series
- Introduction - A DynamicData Attribute Based Permission Solution using User Roles.
- Part 1 - Permissions Attribute (Metadata) Classes.
- Part 2 - Sample Metadata for project.
- Part 3 - The Helper Extension Methods.
- Part 4 - Limit Tables shown on Default page and List, Edit & Details etc.
- Part 5 - Generate Columns/Rows (using IAutoFieldGenerator)
- Part 6 - Miscellaneous bits
- Part 7 - Updating the ListDetails Page
- DynamicData - Limit the Filter Fields
- DynamicData - Automatic Column Update
Helper Extension Methods
The attribute data is held in:
- MetaTable.Attributes
- MetaColumn.Attributes
- MetaForeignKeyColumn.ParentTable.Attribute
- MetaChildrenColumn.ChildTable.Attributes
Form the above list four extension methods will be needed on to deal with each type.
- What data type should the extension method operate on? The obvious type would be AttributeCollection but would move unnecessary casting in some of the other extension methods. The types will be MetaTable for the table permissions extension method and MetaColumn for the others and any casting will then be hidden in the extension methods themselves.
- What type should the extension method return? The obvious and correct type would be the TablePermissionsAttribute.Permissions or FieldPermissionsAttribute.Permissions types and since there could be multiple entries per class/property then a generic List<T> of the type should be returned.
- What other parameter will be need? A role of type String, but because a user can have several roles it should be roles of type String[] because this is what is returned by Roles.GetRolesForUser().
If a breakpoint is set at an instance of column of type MetaColumn (see Figure 1) it can be seen that the column.Attributes is of type System.ComponentModel.AttributeCollection and 1 & 2 are FieldPermissionsAttributes this means that the collection will need to be iterated through to pick out the FieldPermissionsAttributes.
Figure 1
Let us look at the extension method definition for MetaColumn.Attributes.
public static List<FieldPermissionsAttribute.Permissions> GetFieldPermissions(this MetaColumn column, String[] roles) { List<FieldPermissionsAttribute.Permissions> permissions = new List<FieldPermissionsAttribute.Permissions>();
//TODO: get attributes permissions
return permissions;
}
Listing 1
Calling the GetFieldPermissions extension method:
var fieldPermissions = column.GetFieldPermissions(Roles.GetRolesForUser());
In Listing 1 each attribute is checked to see if it has one of the users roles passed in from the GetRolesForUser() method of the Roles class. If a match is found then the permission is added to “permissions” local variable.
public static List<FieldPermissionsAttribute.Permissions> GetFieldPermissions(this MetaColumn column, String[] roles) { var permissions = new List<FieldPermissionsAttribute.Permissions>(); // you could put: // var attributes = column.Attributes; // but to make it clear what type we are using: System.ComponentModel.AttributeCollection attributes = column.Attributes; // check to see if any roles passed if (roles.Count() > 0) { // using Linq to Object to get // the permissions foreach role permissions = (from a in attributes.OfType<FieldPermissionsAttribute>() where a.HasAnyRole(roles) select a.Permission).ToList<FieldPermissionsAttribute.Permissions>(); } return permissions; }
Above in Listing 2 is the full helper extension method GetFieldPermissions three more extension methods are required to cater for:
- Table
- ChileTable
- ParentTable
MetaColumn Types. Below in Listing 3 are all the extension methods for the Permissions Attributes classes.
/// <summary> /// Permissions Attributes Helper Extension Methods /// </summary> public static class PermissionsAttributesHelper { /// <summary> /// Get a list of permissions for the specified role /// </summary> /// <param name="attributes"> /// Is a AttributeCollection taken /// from the column of a MetaTable /// </param> /// <param name="role"> /// name of the role to be matched with /// </param> /// <returns>A List of permissions</returns> public static List<FieldPermissionsAttribute.Permissions> GetFieldPermissions(this MetaColumn column, String[] roles) { var permissions = new List<FieldPermissionsAttribute.Permissions>(); // you could put: // var attributes = column.Attributes; // but to make it clear what type we are using: System.ComponentModel.AttributeCollection attributes = column.Attributes; // check to see if any roles passed if (roles.Count() > 0) { // using Linq to Object to get // the permissions foreach role permissions = (from a in attributes.OfType<FieldPermissionsAttribute>() where a.HasAnyRole(roles) select a.Permission).ToList<FieldPermissionsAttribute.Permissions>(); } return permissions; } /// <summary> /// Get a list of permissions for the specified role /// </summary> /// <param name="attributes"> /// Is a AttributeCollection taken from the column of a MetaTable /// </param> /// <param name="role">name of the role to be matched with</param> /// <returns>A List of permissions</returns> public static List<TablePermissionsAttribute.Permissions> GetTablePermissions(this MetaTable table, String[] roles) { var permissions = new List<TablePermissionsAttribute.Permissions>(); var attributes = table.Attributes; // check to see if any roles passed if (roles.Count() > 0) { // using Linq to Object to get // the permissions foreach role permissions = (from a in attributes.OfType<TablePermissionsAttribute>() where a.HasAnyRole(roles) select a.Permission).ToList<TablePermissionsAttribute.Permissions>(); } return permissions; } /// <summary> /// Get a list of permissions for the specified role /// </summary> /// <param name="attributes"> /// Is a AttributeCollection taken from the column of a MetaTable /// </param> /// <param name="role">name of the role to be matched with</param> /// <returns>A List of permissions</returns> public static List<TablePermissionsAttribute.Permissions> GetFkTablePermissions(this MetaColumn column, String[] roles) { var permissions = new List<TablePermissionsAttribute.Permissions>(); var foreignKeyColumn = column as MetaForeignKeyColumn; var attributes = foreignKeyColumn.ParentTable.Attributes; // check to see if any roles passed if (roles.Count() > 0) { // using Linq to Object to get // the permissions foreach role permissions = (from a in attributes.OfType<TablePermissionsAttribute>() where a.HasAnyRole(roles) select a.Permission).ToList<TablePermissionsAttribute.Permissions>(); } return permissions; } /// <summary> /// Get a list of permissions for the specified role /// </summary> /// <param name="attributes"> /// Is a AttributeCollection taken from the column of a MetaTable /// </param> /// <param name="role">name of the role to be matched with</param> /// <returns>A List of permissions</returns> public static List<TablePermissionsAttribute.Permissions> GetChildrenTablePermissions(this MetaColumn column, String[] roles) { var permissions = new List<TablePermissionsAttribute.Permissions>(); var childrenColumn = column as MetaChildrenColumn; var attributes = childrenColumn.Attributes; // check to see if any roles passed if (roles.Count() > 0) { // using Linq to Object to get // the permissions foreach role permissions = (from a in attributes.OfType<TablePermissionsAttribute>() where a.HasAnyRole(roles) select a.Permission).ToList<TablePermissionsAttribute.Permissions>(); } return permissions; } /// <summary> /// Returns a copy of the array of string /// all in lowercase /// </summary> /// <param name="strings">Array of strings</param> /// <returns>array of string all in lowercase</returns> public static String[] AllToLower(this String[] strings) { String[] temp = new String[strings.Count()]; for (int i = 0; i < strings.Count(); i++) { temp[i] = strings[i].ToLower(); } return temp; } }
Next putting it all together in the Generate Columns/Rows (using IAutoFieldGenerator)
Metadata for project - Part 2
Articles in this Series
- Introduction - A DynamicData Attribute Based Permission Solution using User Roles.
- Part 1 - Permissions Attribute (Metadata) Classes.
- Part 2 - Sample Metadata for project.
- Part 3 - The Helper Extension Methods.
- Part 4 - Limit Tables shown on Default page and List, Edit & Details etc.
- Part 5 - Generate Columns/Rows (using IAutoFieldGenerator)
- Part 6 - Miscellaneous bits
- Part 7 - Updating the ListDetails Page
- DynamicData - Limit the Filter Fields
- DynamicData - Automatic Column Update
Sample Metadata
For this project the Northwind database will be used, you can download it from here (it says it is for SQL 2000 but works fine on SQL 2005 and SQL 2008 although if using SQL 2008 the database will be upgraded and then will not run on SQL 2000 or SQL 2005).
Figure 1 has some tables out of the Northwind database that have been added in to the Linq to SQL model.
Figure 1
Listing 1 is the metadata on some Northwind model.
[MetadataType(typeof(OrderMetadata))] [TablePermissions(TablePermissionsAttribute.Permissions.DenyInserts, "Accounts")] public partial class Order { } public class OrderMetadata { [FieldPermissions(FieldPermissionsAttribute.Permissions.DenyEdit, "Sales", "Production")] [FieldPermissions(FieldPermissionsAttribute.Permissions.DenyRead, "Accounts")] public Object OrderDate { get; set; } [FieldPermissions(FieldPermissionsAttribute.Permissions.DenyEdit, "Sales", "Production")] [FieldPermissions(FieldPermissionsAttribute.Permissions.DenyRead, "Accounts")] public Object RequiredDate { get; set; } // entities [HideInFilter] public Object Employee { get; set; } } [TablePermissions(TablePermissionsAttribute.Permissions.DenyRead, "Sales")] public partial class Order_Detail { } [TablePermissions(TablePermissionsAttribute.Permissions.DenyDelete, "Sales")] [MetadataType(typeof(CustomerMetadata))] public partial class Customer { } public class CustomerMetadata { [ScaffoldColumn(false)] public Object UpdatedBy { get; set; } [ScaffoldColumn(false)] public Object UpdatedOn { get; set; } [ScaffoldColumn(false)] public Object CreatedBy { get; set; } [ScaffoldColumn(false)] public Object CreatedOn { get; set; } } [TablePermissions(TablePermissionsAttribute.Permissions.DenyRead, "Sales")] public partial class Employee { } [TablePermissions(TablePermissionsAttribute.Permissions.DenyRead, "Sales")] public partial class Shipper { }
Listing 1
Note: the table level metadata is on the partial class that is implemented from the DBML file this was to workaround a bug in DynamicData 5-12 release but it seems to be the correct place.
Next is the metadata helper classes...
DynamicData - Automatic Column Update
Articles in this Series
- Introduction - A DynamicData Attribute Based Permission Solution using User Roles.
- Part 1 - Permissions Attribute (Metadata) Classes.
- Part 2 - Sample Metadata for project.
- Part 3 - The Helper Extension Methods.
- Part 4 - Limit Tables shown on Default page and List, Edit & Details etc.
- Part 5 - Generate Columns/Rows (using IAutoFieldGenerator)
- Part 6 - Miscellaneous bits
- Part 7 - Updating the ListDetails Page
- DynamicData - Limit the Filter Fields
- DynamicData - Automatic Column Update
Automatic Column Update
The problem I'm trying to address is lets say you have the same columns on each table for simple auditing to check:
- Who created the record and when
- Who updated the record last and when
We will use the Northwind database for this test with a little tweaking.
We will use the Customers table in Northwind and add four columns:
- CreatedBy String
- CreatedOn DateTime
- UpdatedBy String
- UpdatedOn DateTime
These columns don’t need to be shown on any pages so scaffolding needs to be turned off see Listing 1
[MetadataType(typeof(CustomerMetadata))] public partial class Customer { } public class CustomerMetadata { [ScaffoldColumn(false)] public Object UpdatedBy { get; set; } [ScaffoldColumn(false)] public Object UpdatedOn { get; set; } [ScaffoldColumn(false)] public Object CreatedBy { get; set; } [ScaffoldColumn(false)] public Object CreatedOn { get; set; } } Listing 1
To automatically update this table each time a row is created or update the DataContext partial class will need to be extended see Listing 2 in the DataContext each table has a partial method that can be implemented. The two needed for adding auditing data are InsertTableName and UpdateTableName where TableName is the name of the table that is to have auditing added.
public partial class NorthwindDataContext : System.Data.Linq.DataContext { partial void InsertCustomer(Customer instance) { var user = HttpContext.Current.User; instance.CreatedBy = user.Identity.Name; instance.CreatedOn = DateTime.Now; instance.UpdatedBy = user.Identity.Name; instance.UpdatedOn = DateTime.Now; // finally send this to the DB this.ExecuteDynamicInsert(instance); } partial void UpdateCustomer(Customer instance) { var user = HttpContext.Current.User; instance.UpdatedBy = user.Identity.Name; instance.UpdatedOn = DateTime.Now; // finally send this to the DB this.ExecuteDynamicUpdate(instance); } } Listing 2
Now when a new record is created in the Customer table the four fields will be updated automatically and because the scaffolding metadata the columns will not be show in the DynamicData app.
Note: This will also work with Linq to SQL.
DynamicData Attribute Based Security - part 1
Articles in this Series
- Introduction - A DynamicData Attribute Based Permission Solution using User Roles.
- Part 1 - Permissions Attribute (Metadata) Classes.
- Part 2 - Sample Metadata for project.
- Part 3 - The Helper Extension Methods.
- Part 4 - Limit Tables shown on Default page and List, Edit & Details etc.
- Part 5 - Generate Columns/Rows (using IAutoFieldGenerator)
- Part 6 - Miscellaneous bits
- Part 7 - Updating the ListDetails Page
- DynamicData - Limit the Filter Fields
- DynamicData - Automatic Column Update
Permissions Attribute (Metadata) Classes
An attributes class inherits from System.Attribute and must have the following properties set, AttributeUsage with at least one of the 16 possible targets see below and also AllowMultiple set to true so that multiple occurrences of the attribute can decorate a class or property.
AttributeTargets
All | GenericParameter |
Assembly | Interface |
Class | Method |
Constructor | Module |
Delegate | Parameter |
Enum | Property |
Event | ReturnValue |
Field | Struct |
The AttributeTargets options that the PermissionsAttributes classes will use are class and Property.
Listing 1 shows a simplified attribute that can be applied to a class multiple times see Listing 2 where the attribute is applied to the Orders table once each for "Accounts" and "Sales" roles.
[AttributeUsage(AttributeTargets.class, AllowMultiple = true)] public class TableDenyWriteAttribute: System.Attribute { ... } Listing 1
Note: By convention, the name of the attribute class ends with the word Attribute. While not required, this convention is recommended for readability. When the attribute is applied, the inclusion of the word Attribute is optional.
[TableDenyWrite ("Sales")] [TableDenyWrite ("Accounts")] public partial class Order { } Listing 2
Two types of attribute are required for the permissions attribute, Class (Entity/Table) and Property(Field/Column). Looking at Listing 2 this is all right for a simplistic approach but will make extracting the roles for each permission a bit clunky. So a better approach would be to have an attribute that is more flexible see Listing 3. The effective permissions for the class Order shown in Listing 3 are listed in Table 1.
[TablePermissions(TablePermissionsAttribute.Permissions.DenyInserts, "Sales")] [TablePermissions(TablePermissionsAttribute.Permissions.DenyDelete, "Sales", "Production",)] [TablePermissions(TablePermissionsAttribute.Permissions.DenyRead, "Accounts", "HR")] public partial class Order { } Listing 3
Roles | Insert | Delete | Read |
Accounts | 1n | n | n |
HR | n | n | n |
Sales | n | n | y |
Production | y | n | y |
Table 1
1. Note you will not be able to do an insert if the table cannot be viewed.
Listing 4 contain the finished attribute classes.
The changes in the finished classes include:
- A permission parameter
- The role parameter has become roles and is a string array
- Each attribute class has an enum which contain the table and column permissions.
- Helper method HasRole which return true if the role is present.
- Helper method HasAnyRole which return true if any of the roles passed in are present.
using System; using System.Collections.Generic; using System.Linq; using System.Web.DynamicData; [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] public class TablePermissionsAttribute : System.Attribute { // this property is required to work with "AllowMultiple = true" ref David Ebbo // As implemented, this identifier is merely the Type of the attribute. However, // it is intended that the unique identifier be used to identify two // attributes of the same type. public override object TypeId { get { return this; } } /// <summary> /// Constructor /// </summary> /// <param name="permission"></param> /// <param name="roles"></param> public TablePermissionsAttribute(Permissions permission, params String[] roles) { this._permission = permission; this._roles = roles; } private String[] _roles; public String[] Roles { get { return this._roles; } set { this._roles = value; } } private Permissions _permission; public Permissions Permission { get { return this._permission; } set { this._permission = value; } } /// <summary> /// helper method to check for roles in this attribute /// the comparison is case insensitive /// </summary> /// <param name="role"></param> /// <returns></returns> public Boolean HasRole(String role) { // call extension method to convert array to lower case for compare String[] rolesLower = _roles.AllToLower(); return rolesLower.Contains(role.ToLower()); } /// <summary> /// helper method to check for roles in this attribute /// the comparison is case insensitive /// </summary> /// <param name="roles"></param> /// <returns></returns> public Boolean HasAnyRole(String[] roles) { // call extension method to convert array to lower case for compare foreach (var role in roles.AllToLower()) { if (_roles.AllToLower().Contains(role.ToLower())) return true; } return false; } /// <summary> /// list of Deny permissions as the default is read write on everything /// this model apply the most severe permission restriction /// </summary> public enum Permissions { DenyRead, DenyEdit, DenyInserts, DenyDelete, DenyDetails, DenySelectItem, // Don't know wether this will be any use??? } } [AttributeUsage(AttributeTargets.Property, AllowMultiple = true)] public class FieldPermissionsAttribute : System.Attribute { // this property is required to work with "AllowMultiple = true" ref David Ebbo // As implemented, this identifier is merely the Type of the attribute. However, // it is intended that the unique identifier be used to identify two // attributes of the same type. public override object TypeId { get { return this; } } // Constructor public FieldPermissionsAttribute(Permissions permission, params String[] roles) { this._permission = permission; this._roles = roles; } private String[] _roles; public String[] Roles { get { return this._roles; } set { this._roles = value; } } private Permissions _permission; public Permissions Permission { get { return this._permission; } set { this._permission = value; } } /// <summary> /// helper method to check for roles in this attribute /// the comparison is case insensitive /// </summary> /// <param name="role"></param> /// <returns></returns> public Boolean HasRole(String role) { // call extension method to convert array to lower case for compare String[] rolesLower = _roles.AllToLower(); return rolesLower.Contains(role.ToLower()); } /// <summary> /// helper method to check for roles in this attribute /// the comparison is case insensitive /// </summary> /// <param name="roles"></param> /// <returns></returns> public Boolean HasAnyRole(String[] roles) { // call extension method to convert array to lower case for compare foreach (var role in roles.AllToLower()) { if (_roles.AllToLower().Contains(role.ToLower())) return true; } return false; } /// <summary> /// list of Deny permissions as the default is read write on everything /// this model apply the most severe permission restriction /// </summary> public enum Permissions { DenyRead, DenyEdit, } } Listing 4
Next step is the metadata helper classes.