Showing posts with label Attributes. Show all posts
Showing posts with label Attributes. Show all posts

Thursday, 10 September 2015

Automatic Totals on your List page in Dynamic Data

So the idea here is add an attribute to your table (TotalsAttribute in this case) and then have a total appear on your list page.

Note: Note this will only work on Numeric columns int, Decimal, float etc.

Figure 1 shows what we are going to achieve, the columns here don’t make sense to total but it’s only an example for real i would add a computed column that multiplied the unit cost with the qty in stock and total that but here it’s just to show how it works with decimal and int values.

finished-page

Figure 1 – Footer row with totals.

The Attribute

We will need an attribute because we want this to happen automatically and not have to create a custom page, for this I have decided to add a class level attribute called TotalsAttribute you can see the code in Listing 1.

[AttributeUsage(AttributeTargets.Class)]
public class TotalsAttribute : Attribute
{
public String[] Columns { get; set; }

public TotalsAttribute()
{
Columns = new String[0];
}

public TotalsAttribute(params String[] columns)
{
Columns = columns;
}
}

Listing 1 – TotalsAttribute

All we are doing here is keeping an array of column names that we want totals on, most of the time it will be single column, but it’s nice to have the option of multiple.

[Totals("UnitPrice", "UnitsInStock", "UnitsOnOrder")]
[MetadataType(typeof(Product.Metadata))]
public partial class Product
{
internal sealed class Metadata
{
public Int32 ProductID { get; set; }

public String ProductName { get; set; }

[DataType(DataType.Currency)]
public Nullable<int> UnitPrice { get; set; }

public Nullable<int> UnitsInStock { get; set; }

//... other column removed for simplicity
}
}

Listing 2 – example of attribute in use.

In the example [Totals("UnitPrice", "UnitsInStock", "UnitsOnOrder")] in Listing 2 we are telling the system that we want totals on three columns “UnitPrice”, “UnitsInStock”, “UnitsOnOrder”.

The Custom Code in the List page

There are two things we need in the page first we will need to check if there are totals on this table and if so wire it all up. In Listing 3 we have all the code we need to test if there are totals for the current Table and if so wire up the Row DataBound even handler which you can see in Listing 4.

In Listing 3 we single get the attribute from the table and test to see if what we got is not null, if not we can the wire up the event handler. But also we need to turn on the footer.

!Important: I found this bit missing from most of the articles I found whilst searching for examples of how to do this; everything works without this, it just doesn't display. The bit you must have is GridView1.ShowFooter = true;
Note: I am using some custom extension methods to get the attribute, these are in the root of the application and the file is called “AttributeExtensionMethods.cs”
public partial class List : System.Web.UI.Page
{
 protected TotalsAttribute totalsAttribute;
 protected MetaTable table;
 protected void Page_Init(object sender, EventArgs e)
 {
 table = DynamicDataRouteHandler.GetRequestMetaTable(Context);
 GridView1.SetMetaTable(table, table.GetColumnValuesFromRoute(Context));
 GridDataSource.EntityTypeFilter = table.EntityType.Name;
 // get the attribute
 totalsAttribute = table.GetAttribute<TotalsAttribute>();
 // if the attribute is not null then we have some totals
 if (totalsAttribute != null && totalsAttribute.Columns.Count() > 0)
 {
 // show the footer
 GridView1.ShowFooter = true;
 // wire up the row data bound event
 GridView1.RowDataBound += OnRowDataBound;
 }
 }
// rest of code behind removed for simplicity

Listing 3 – testing if we have any totals for this table.

Now all we need the workhorse code the stuff that is going to total up and then display the totals in the footer. See Listing 4 I have tried to put a lot of comments in there to help but here’s a brief explanation of what it does:

The code in the event handler is split into two sections one for the DataRow and one for the Footer you can see there are encased in two if statements. Also note we have a global variable “totals” this is used to keep a total of each column we are totalling and is a dictionary of Decimal values.

The DataRow

Here we iterate through the totals columns from the attribute and sum up each one, you will notice that I am testing if the column is a valid column by checking the metadata to see if it is an int or an floating point this stops us having a nasty error.

// NOTE: if you are using a column generator (IAutoFieldGenerator) 
// the this may not work if it re-orders the displayed columns
protected Dictionary<String, Decimal> totals = new Dictionary<String, Decimal>();
protected void OnRowDataBound(object sender, GridViewRowEventArgs e)
{
 // this will only be wired up and called if there are totals so we don't need to test.
 // Get a List<String> of column names if the order that they appear in the GridView1
 var displayedColumns = table.GetScaffoldColumns(DataBoundControlMode.ReadOnly, ContainerType.List).ToList();
 // if this is a data row get the totals
 if (e.Row.RowType == DataControlRowType.DataRow)
 {
 foreach (var column in totalsAttribute.Columns)
 {
 // get the MetaColumn
 var metaColumn = displayedColumns.First(c => c.Name == column);
 // check this column is a valid column to total i.e. int Decimal, float etc.
 if (metaColumn.IsFloatingPoint || metaColumn.IsInteger)
 {
 // initialize variable if not present
 if (!totals.ContainsKey(column))
 totals.Add(column, 0);
 // add to total
 totals[column] += Convert.ToDecimal(DataBinder.Eval(e.Row.DataItem, column));
 }
 }
 }
 // if we are on the footer row render the totals.
 if (e.Row.RowType == DataControlRowType.Footer)
 {
 // set total description name
 e.Row.Cells[0].Text = "Total:";
 // add alignment style
 e.Row.Cells[0].CssClass = "right";
 foreach (var column in totalsAttribute.Columns)
 {
 // get index of column plus offset of 1 for the command button column
 var index = displayedColumns.FindIndex(c => c.Name == column) + 1;
 var metaColumn = displayedColumns.First(c => c.Name == column);
 if (metaColumn.IsFloatingPoint || metaColumn.IsInteger)
 {
 // for the Footer, display the running totals
 e.Row.Cells[index].Text = metaColumn.FormatValue(totals[column]);
 // add alignment style
 e.Row.Cells[index].CssClass = "numeric";
 }
 }
 }
}

Listing 4 – OnRowDataBound event handler and global totals variable.

Finally the second section the Footer we we simply render the totals to the footer

Note: The plus 1 I am adding to the index, is to account for the Command column with the Edit buttons etc.

Sample Code

[フレーム]

Thursday, 31 March 2011

Custom Entity Templates – Dynamic Data 4

In Dynamic Data 4 we have Entity Templates these are great for customizing layout of tables (see Walkthrough: Customizing Table Layout Using Entity Templates).  At the moment you will have to create a Custom Entity template for each table, but not only that you will need to create a set of three (Template, Template_Edit and Template_Insert) and that can be useful to have different layout for each view.

But what if you want something different from the Default, (see Grouping Fields on Details, Edit and Insert with Dynamic Data 4, VS2010 and .Net 4.0 RC1 my first custom Entity Template which then had to replace the Default and also had to have all three defined) but not on all your tables just on a selected few?

I was inspired by Brian Pritchard’s post on the forum: How to reduce code duplication when using Entity Template's for only having to create a single template that would switch between Read-Only, Edit and Insert modes.

I want to go a little further:

  1. Override the Default Entity Template with some sort of UIHint attribute.
  2. An only have to specify one template to keep things DRY
  3. Detect if an Edit or Insert version of a template exists and use that.

So now we know out goal, let’s layout the ingredients for this recipe:

  1. An attribute to allow us to change the default Entity Template for any given Table.
  2. Custom entity factory to allow us to override normal entity behaviour like Brian Pritchard’s.
  3. A Custom dynamic Entity Template.

The Attribute

I thought I would be able to use UIHint at class level but alas we have to hand craft our own, I want some of the same feature of UIHint specifically the Control Parameters collection so that we can pass extra information into our custom Entity Templates with out creating a plethora extra attribute each specific to it’s one Entity Template. (I’ve used the Control Parameters collection before in Dynamic Data Custom Field Template – Values List, Take 2.

The Attribute is relatively straight forward, the only complication is the BuildControlParametersDictionary method which takes the Object array passed in using the params key word into a Key, Value Dictionary with some validation. Note we have also set this attribute to be only useable at Class level.

[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class EntityUIHintAttribute : Attribute
{
 private IDictionary _controlParameters;
 public IDictionary ControlParameters
 {
 get { return this._controlParameters; }
 }
 /// 
 /// Gets or sets the UI hint.
 /// 
 /// The UI hint.
 public String UIHint { get; private set; }
 public EntityUIHintAttribute(string uiHint) : this(uiHint, new object[0]) { }
 public EntityUIHintAttribute(string uiHint, params object[] controlParameters)
 {
 UIHint = uiHint;
 _controlParameters = BuildControlParametersDictionary(controlParameters);
 }
 public override object TypeId
 {
 get { return this; }
 }
 private IDictionary BuildControlParametersDictionary(object[] objArray)
 {
 IDictionary dictionary = new Dictionary();
 if ((objArray != null) && (objArray.Length != 0))
 {
 if ((objArray.Length % 2) != 0)
 throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, "Need even number of control parameters.", new object[0]));
 for (int i = 0; i < objArray.Length; i += 2)
 {
 object obj2 = objArray[i];
 object obj3 = objArray[i + 1];
 if (obj2 == null)
 throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, "Control parameter key is null.", new object[] { i }));
 string key = obj2 as string;
 if (key == null)
 throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, "Control parameter key is not a string.", new object[] { i, objArray[i].ToString() }));
 if (dictionary.ContainsKey(key))
 throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, "Control parameter key occurs more than once.", new object[] { i, key }));
 dictionary[key] = obj3;
 }
 }
 return dictionary;
 }
}

Listing 1 – EntityUIHintAttribute

Now we need a method to use this attribute to change the default Entity Template, this factory need to do two things:

  1. Change the Default Entity Template based on out new EntityUIHint attribute.
  2. Intercept the template mode so that single Entity Template can be used with out having to have three versions.
public class AdvancedEntityTemplateFactory : System.Web.DynamicData.EntityTemplateFactory
{
 public override string BuildEntityTemplateVirtualPath(string templateName, DataBoundControlMode mode)
 {
 var path = base.BuildEntityTemplateVirtualPath(templateName, mode);
 var editPath = base.BuildEntityTemplateVirtualPath(templateName, DataBoundControlMode.Edit);;
 var defaultPath = base.BuildEntityTemplateVirtualPath(templateName, DataBoundControlMode.ReadOnly); ;
 if (File.Exists(HttpContext.Current.Server.MapPath(path)))
 return path;
 if (mode == DataBoundControlMode.Insert && File.Exists(HttpContext.Current.Server.MapPath(editPath)))
 return editPath;
 if (mode != DataBoundControlMode.ReadOnly && File.Exists(HttpContext.Current.Server.MapPath(defaultPath)))
 return defaultPath;
 return path;
 }
 public override EntityTemplateUserControl CreateEntityTemplate(MetaTable table, DataBoundControlMode mode, string uiHint)
 {
 var et = table.GetAttribute();
 if (et != null && !String.IsNullOrEmpty(et.UIHint))
 return base.CreateEntityTemplate(table, mode, et.UIHint);
 return base.CreateEntityTemplate(table, mode, uiHint);
 }
 public override string GetEntityTemplateVirtualPath(MetaTable table, DataBoundControlMode mode, string uiHint)
 {
 var et = table.GetAttribute();
 if (et != null && !String.IsNullOrEmpty(et.UIHint))
 return base.GetEntityTemplateVirtualPath(table, mode, et.UIHint);
 return base.GetEntityTemplateVirtualPath(table, mode, uiHint);
 }
}

Listing 2 – AdvancedEntityTemplateFactory

Listing 1 shows us out AdvancedEntityTemplateFactory, we fulfil task 1. in the methods CreateEntityTemplate and GetEntityTemplateVirtualPath where we check for the presence of a EntityUIHintAttribute and if we find one then set the name of the template to the UIHint property.

Task 2. is dealt with in the BuildEntityTemplateVirtualPath where we check to see if the file exists, if so we just return the path as is, otherwise we strip out the  _Edit or _Insert from the path and return.

The last thing we need is to wire up the AdvancedEntityTemplateFactory in Global.asax.cs

DefaultModel.EntityTemplateFactory = new AdvancedEntityTemplateFactory();

just before the RegisterContext in RegisterRoutes method.

The Custom Entity Template

Multi Column Entity Template

Figure 1 – Multi Column Entity Template

This entity template will be a multi column temp[late designed to give you a little more screen for your money Big Grin I have decided to pass the main parameters in via the EntityUIHint attribute’s Control Parameters, in Figure 2 you can see them in pairs.

Control Parameters

Figure 2 – Control Parameters

“Columns”, 3 sets the number of column the MultiColumn entity template will show.

The next two pairs are the Title and Field CSS classes.

I’ve also decided to add the ability for some columns to span more then one cell in the table.

[AttributeUsage(AttributeTargets.Property)]
public class MultiColumnAttribute : Attribute
{
 /// 
 /// Gets or sets the column span.
 /// 
 /// The column span.
 public int ColumnSpan { get; private set; }
 public static MultiColumnAttribute Default = new MultiColumnAttribute();
 public MultiColumnAttribute() 
 { 
 ColumnSpan = 1;
 }
 public MultiColumnAttribute(int columnSpan)
 {
 ColumnSpan = columnSpan;
 }
}

Listing 3 – MultiColumnAttribute

The use of Default for when we use the DefaultIfEmpty method in Linq (see my Writing Attributes and Extension Methods for Dynamic Data more info) this allows us to get an attribute even if one is not specified so now with thi sline of code

var totalNoOfCells = metaColumns.Select(c => c.GetAttributeOrDefault<MultiColumnAttribute>().ColumnSpan).Sum();

we can get the total number of cell required with some columns having a span of more than on column see Figure 1.

Finally our Multi Column entity template is completed in Listing 4

public partial class MultiColumnEntityTemplate : System.Web.DynamicData.EntityTemplateUserControl
{
 private const string COLUMNS = "Columns";
 private const string TITLE_CSS_CLASS = "TitleCssClass";
 private const string FIELD_CSS_CLASS = "FieldCssClass";
 protected override void OnLoad(EventArgs e)
 {
 // get columns from table
 var metaColumns = Table.GetScaffoldColumns(Mode, ContainerType).ToList();
 // do not render any HTML table if there are no columns returned
 if (metaColumns.Count == 0)
 return;
 // default the HTML table columns and CSS class names
 int columns = 2;
 String titleCssClass = String.Empty;
 String fieldCssClass = String.Empty;
 // Get the CssClass for the title & Field from the attribute
 var entityUHint = Table.GetAttribute();
 if (entityUHint != null)
 {
 if (entityUHint.ControlParameters.Keys.Contains(COLUMNS))
 columns = (int)entityUHint.ControlParameters[COLUMNS];
 if (entityUHint.ControlParameters.Keys.Contains(TITLE_CSS_CLASS))
 titleCssClass = entityUHint.ControlParameters[TITLE_CSS_CLASS].ToString();
 if (entityUHint.ControlParameters.Keys.Contains(FIELD_CSS_CLASS))
 fieldCssClass = entityUHint.ControlParameters[FIELD_CSS_CLASS].ToString();
 }
 // start in the left column
 int col = 0;
 // create the header & data cells
 var headerRow = new HtmlTableRow();
 if (!String.IsNullOrEmpty(titleCssClass))
 headerRow.Attributes.Add("class", titleCssClass);
 var dataRow = new HtmlTableRow();
 if (!String.IsNullOrEmpty(fieldCssClass))
 dataRow.Attributes.Add("class", fieldCssClass);
 // step through each of the columns to be added to the table
 foreach (var metaColumn in metaColumns)
 {
 // get the MultiColumn attribute for the column
 var multiColumn = metaColumn.GetAttributeOrDefault();
 if (multiColumn.ColumnSpan > columns)
 throw new InvalidOperationException(String.Format("MultiColumn attribute specifies that this 
 field occupies {0} columns, but the EntityUIHint attribute for the class only allocates {1} 
 columns in the HTML table.", multiColumn.ColumnSpan, columns));
 // check if there are sufficient columns left in the current row
 if (col + multiColumn.ColumnSpan > columns)
 {
 // save this header row
 this.Controls.Add(headerRow);
 headerRow = new HtmlTableRow();
 if (!String.IsNullOrEmpty(titleCssClass))
 headerRow.Attributes.Add("class", titleCssClass);
 // save this data row
 this.Controls.Add(dataRow);
 dataRow = new HtmlTableRow();
 if (!String.IsNullOrEmpty(fieldCssClass))
 dataRow.Attributes.Add("class", fieldCssClass);
 // need to start a new row
 col = 0;
 }
 // add the header cell
 var th = new HtmlTableCell();
 var label = new Label();
 label.Text = metaColumn.DisplayName;
 //if (Mode != System.Web.UI.WebControls.DataBoundControlMode.ReadOnly)
 // label.PreRender += Label_PreRender;
 th.InnerText = metaColumn.DisplayName;
 if (multiColumn.ColumnSpan > 1)
 th.ColSpan = multiColumn.ColumnSpan;
 headerRow.Cells.Add(th);
 // add the data cell
 var td = new HtmlTableCell();
 var dynamicControl = new DynamicControl(Mode);
 dynamicControl.DataField = metaColumn.Name;
 dynamicControl.ValidationGroup = this.ValidationGroup;
 td.Controls.Add(dynamicControl);
 if (multiColumn.ColumnSpan > 1)
 td.ColSpan = multiColumn.ColumnSpan;
 dataRow.Cells.Add(td);
 // record how many columns we have used
 col += multiColumn.ColumnSpan;
 }
 this.Controls.Add(headerRow);
 this.Controls.Add(dataRow);
 }
}

Listing 4 – MultiColumnEntityTemplate

Updated: MultiColumnEntityTemplate updated by Phil Wigglesworth who kindly found a bug and fixed it, the bug was shown when there were no enough columns left on a row, this caused a crash. So thanks again to Phil.
Also we will need some styles to make out new Multi Column entity template look good.

TR.SmallTitle
{
 background-color: #F7F7FF;
}
TR.SmallTitle TD
{
 font-size: 0.8em !important;
 font-weight: bold;
 background-color: #F7F7FF;
 padding: 2px !important;
}

Listing 5 – CSS styles

Hope this expand your use of Entity Templates.

P.S. I’ll do an updated version of the Grouping entity template.

Download

[フレーム]

Saturday, 24 July 2010

Conditional Row Highlighting in Dynamic Data

There are occasions when you want to highlight a row in the GridView (I usually want this based on a Boolean field) so here’s what you do.

First of all we need some way of telling the column to do this an I usually use an attribute see Listing 1 it have two properties one for the value when we want the CSS class to be applied, and the other the CSS class to apply.

[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)]
public class RowHighlightingAttribute : Attribute
{
 /// <summary>
 /// Initializes a new instance of the <see cref="RowHighlightingAttribute"/> class.
 /// </summary>
 /// <param name="valueWhenTrue">The value when true.</param>
 /// <param name="cssClass">The CSS class.</param>
 public RowHighlightingAttribute(String valueWhenTrue, String cssClass)
 {
 ValueWhenTrue = valueWhenTrue;
 CssClass = cssClass;
 }
 /// <summary>
 /// Gets or sets the value when true.
 /// </summary>
 /// <value>The value when true.</value>
 public String ValueWhenTrue { get; set; }
 /// <summary>
 /// Gets or sets the CSS class.
 /// </summary>
 /// <value>The CSS class.</value>
 public String CssClass { get; set; }
}

Listing 1 – RowHighlightingAttribute

Next we need a way of applying the CSS class based on the condition, see Listing 2.

/// <summary>
/// Highlights the row.
/// </summary>
/// <param name="fieldTemplate">The field template.</param>
public static void HighlightRow(this FieldTemplateUserControl fieldTemplate)
{
 // get the attribute
 var rowHighlighting = fieldTemplate.MetadataAttributes.GetAttribute<RowHighlightingAttribute>();
 // make sure the attribute is not null
 if (rowHighlighting != null)
 {
 // get the GridViewRow, note this will not
 // be present in a DetailsView.
 var parentRow = fieldTemplate.GetContainerControl<GridViewRow>();
 if (parentRow != null 
 && rowHighlighting.ValueWhenTrue == fieldTemplate.FieldValueString)
 {
 // apply the CSS class appending if a class is already applied.
 if (String.IsNullOrWhiteSpace(parentRow.CssClass))
 parentRow.CssClass += " " + rowHighlighting.CssClass;
 else
 parentRow.CssClass = rowHighlighting.CssClass;
 }
 }
}

Listing 2 – HighlightRow extension method

Now to add the extension method to a field template, we will apply it to the Boolean read-only field template.

protected override void OnDataBinding(EventArgs e)
{
 base.OnDataBinding(e);
 object val = FieldValue;
 if (val != null)
 CheckBox1.Checked = (bool)val;
 // apply highlighting
 this.HighlightRow();
}

Listing 3 – Apply highlighting.

For the sample I’ve also added it to the Text.ascx.cs field template.

Adding some attributes

Metadata applied

Figure 1 - Metadata applied

You could also us this technique on other values, but this will do for this sample.

Row Highlighting applied

Figure 2 – Row Highlighting applied.

So you can see with a little bit of work you can add conditional row level highlighting to Dynamic Data.

Download

[フレーム]

Monday, 15 February 2010

Grouping Fields on Details, Edit and Insert with Dynamic Data 4, VS2010 and .Net 4.0 RC1

Whilst writing my last article over the week end I noticed the new Display attribute see Figure 1 the first one that intrigued me was the GroupName parameter, so the first thing I did was add some GroupName to some metadata on a Northwind table.

Display attribute parameters

Figure 1 – Display attribute parameters

[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; }
 [Display(Order = 0,GroupName = "Dates")]
 public Object OrderDate { get; set; }
 [Display(Order = 1,GroupName = "Dates")]
 public Object RequiredDate { get; set; }
 [Display(Order = 2,GroupName = "Dates")]
 public Object ShippedDate { get; set; }
 [Display(Order = 4,GroupName = "Ship Info")]
 public Object ShipVia { get; set; }
 [Display(Order = 5,GroupName = "Ship Info")]
 public Object Freight { get; set; }
 [Display(Order = 3,GroupName = "Ship Info")]
 public Object ShipName { get; set; }
 [Display(Order = 6,GroupName = "Ship Info")]
 public Object ShipAddress { get; set; }
 [Display(Order = 7,GroupName = "Ship Info")]
 public Object ShipCity { get; set; }
 [Display(Order = 8,GroupName = "Ship Info")]
 public Object ShipRegion { get; set; }
 [Display(Order = 9,GroupName = "Ship Info")]
 public Object ShipPostalCode { get; set; }
 [Display(Order = 10,GroupName = "Ship Info")]
 public Object ShipCountry { get; set; }
 // Entity Ref 
 [Display(Order = 12,GroupName = "Other Info")]
 public Object Customer { get; set; }
 // Entity Ref 
 [Display(Order = 13,GroupName = "Other Info")]
 public Object Employee { get; set; }
 // Entity Set 
 [Display(Order = 14,GroupName = "Other Info")]
 public Object Order_Details { get; set; }
 // Entity Ref 
 [Display(Order = 11,GroupName = "Ship Info")]
 public Object Shipper { get; set; }
 }
}

Listing 1 – GroupName metadata.

Well when I ran the app and get Figure 2 I was a little disappointed, I’d expected that at least the field would be grouped together by group name (maybe this was not the intended use but I was determined to make it work) but better still would have been with a separator containing the group name.

GroupingBefore

Figure 2 – Orders table with GroupName metadata

So I set about “making it so” (to quote Captain Picard) the first step was to group the fields so I looked at the new EntityTemplates.

<asp:EntityTemplate runat="server" ID="EntityTemplate1">
 <ItemTemplate>
 <tr class="td">
 <td class="DDLightHeader">
 <asp:Label 
 runat="server"
 OnInit="Label_Init" />
 </td>
 <td>
 <asp:DynamicControl 
 runat="server"
 OnInit="DynamicControl_Init" />
 </td>
 </tr>
 </ItemTemplate>
</asp:EntityTemplate>

Listing 2 – Default.ascx entity template

public partial class DefaultEntityTemplate : EntityTemplateUserControl
{
 private MetaColumn currentColumn;
 protected override void OnLoad(EventArgs e)
 {
 foreach (MetaColumn column in Table.GetScaffoldColumns(Mode, ContainerType))
 {
 currentColumn = column;
 Control item = new _NamingContainer();
 EntityTemplate1.ItemTemplate.InstantiateIn(item);
 EntityTemplate1.Controls.Add(item);
 }
 }
 protected void Label_Init(object sender, EventArgs e)
 {
 Label label = (Label)sender;
 label.Text = currentColumn.DisplayName;
 }
 protected void DynamicControl_Init(object sender, EventArgs e)
 {
 DynamicControl dynamicControl = (DynamicControl)sender;
 dynamicControl.DataField = currentColumn.Name;
 }
 public class _NamingContainer : Control, INamingContainer { }
}

Listing 3 – Default.ascx.cs entity template code behind.

If you look at my old Custom PageTemplates Part 4 - Dynamic/Templated FromView sample, I implemented the ITemplate interface for generating FormView and also ListView templates dynamically, and I remember at the time David Ebbo commenting on this and how he was working on something a little more flexible and then Entity Templates were unveiled in one of the early previews; but this is considerably more flexible than my sample was. I think we will be able to extend this greatly in the future but for now I’ll be happy with making grouping work.

So the first thing I did was tweak the default entity template to order by groups.

protected override void OnLoad(EventArgs e)
{
 // get a list of groups ordered by group name
 var groupings = from t in Table.GetScaffoldColumns(Mode, ContainerType)
 group t by t.GetAttributeOrDefault<DisplayAttribute>().GroupName into menu
 orderby menu.Key
 select menu.Key;
 // loop through the groups
 foreach (var groupId in groupings)
 {
 // get columns for this group
 var columns = from c in Table.GetScaffoldColumns(Mode, ContainerType)
 where c.GetAttributeOrDefault<DisplayAttribute>().GroupName == groupId
 orderby c.GetAttributeOrDefault<DisplayAttribute>().GetOrder()
 select c;
 // add fields
 foreach (MetaColumn column in columns)
 {
 currentColumn = column;
 Control item = new _NamingContainer();
 EntityTemplate1.ItemTemplate.InstantiateIn(item);
 EntityTemplate1.Controls.Add(item);
 }
 }
}

Listing 4 – extended default entity template stage 1

So what I did in listing 4 was get a list of all the groups sorted by group name, and then loop through the groups getting the column for each group; then generate the groups fields. Visually this does not produce much of a difference than the initial display.

Grouping with Sort

Figure 3 – Grouping with Sort.

Now we can see the groups coming together, next we need to add the visual aspect.

A little surgery on the ascx part of the entity template is required to get this to work. In Listing 5 you can see that I have added some runat=”server” properties to the TD’s of the template.

<asp:EntityTemplate runat="server" ID="EntityTemplate1">
 <ItemTemplate>
 <tr class="td">
 <td class="DDLightHeader" runat="server">
 <asp:Label 
 runat="server" 
 OnInit="Label_Init" />
 </td>
 <td runat="server">
 <asp:DynamicControl 
 runat="server" 
 OnInit="DynamicControl_Init" />
 </td>
 </tr>
 </ItemTemplate>
</asp:EntityTemplate>

Listing 5 – modified default.ascx Entity Template.

Moving to the Default entity templates code behind in Listing 6 I have added the code to add a separator, but it will need some modification as at the moment is just a repeat of one of the columns.

protected override void OnLoad(EventArgs e)
{
 // get a list of groups ordered by group name
 var groupings = from t in Table.GetScaffoldColumns(Mode, ContainerType)
 group t by t.GetAttributeOrDefault<DisplayAttribute>().GroupName into menu
 orderby menu.Key
 select menu.Key;
 // loop through the groups
 foreach (var groupId in groupings)
 {
 // get columns for this group
 var columns = from c in Table.GetScaffoldColumns(Mode, ContainerType)
 where c.GetAttributeOrDefault<DisplayAttribute>().GroupName == groupId
 orderby c.GetAttributeOrDefault<DisplayAttribute>().GetOrder()
 select c;
 // add group separator
 if (!String.IsNullOrEmpty(groupId))
 {
 groupHeading = true;
 currentColumn = columns.First();
 groupName = groupId;
 Control item = new _NamingContainer();
 EntityTemplate1.ItemTemplate.InstantiateIn(item);
 EntityTemplate1.Controls.Add(item);
 }
 // add fields
 foreach (MetaColumn column in columns)
 {
 groupHeading = false;
 currentColumn = column;
 Control item = new _NamingContainer();
 EntityTemplate1.ItemTemplate.InstantiateIn(item);
 EntityTemplate1.Controls.Add(item);
 }
 }
}

Listing 6 – final version of the OnLoad handler.

For the final tweaks of the visual of the separator we will done some extra manipulation in the two Init handlers of the Label and the DynamicControl.

For the Label wee need to change the text so I have added a class field groupHeading as a Boolean which if you look in Listing 6 I’m setting to true when it is a group and false when a field.

protected void Label_Init(object sender, EventArgs e)
{
 if (!groupHeading)
 {
 Label label = (Label)sender;
 label.Text = currentColumn.DisplayName;
 }
 else
 {
 Label label = (Label)sender;
 label.Text = groupName;
 var parentCell = label.GetParentControl<HtmlTableCell>();
 parentCell.ColSpan = 2;
 parentCell.Attributes.Add("class", "DDGroupHeader");
 }
}

Listing 7 – Label_Init handler

So in Listing 7 you can see that we do the standard thing if it is a filed, but do some custom stuff if it is a group heading. I first get the parent control (see Listing 8 for source) of type HtmlTableCell (we can get this because we set it to runat=”server”). Once we have the parent cell we can manipulate it; first of all we set it’s colspan attribute to 2 and change the CSS class to "DDGroupHeader" to make it stand out.

/// <summary>
/// Gets the parent control.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="control">The control.</param>
/// <returns></returns>
public static T GetParentControl<T>(this Control control) where T : Control
{
 var parentControl = control.Parent;
 // step up through the parents till you find a control of type T
 while (parentControl != null)
 {
 var p = parentControl as T;
 if (p != null)
 return p;
 else
 parentControl = parentControl.Parent;
 }
 return null;
}

Listing 8 – GetParentControl extension method.

.DDGroupHeader
{
	font-weight: bold;
	font-style: italic;
	background-color: Silver;
}

Listing 9 – DDGroupHeader CSS snippet

The next thing is to hide the DynamicControl and it’s HtmlTableCell so the label can span both columns.  Now if we are in a group header then we hide the DynamicControl, get the parent cell and also hide it, letting the label’s cell span both rows.

protected void DynamicControl_Init(object sender, EventArgs e)
{
 DynamicControl dynamicControl = (DynamicControl)sender;
 dynamicControl.DataField = currentColumn.Name;
 if (groupHeading)
 {
 // hide Dynamic Control maybe overkill
 dynamicControl.Visible = false;
 // get the parent cell
 var parentCell = dynamicControl.GetParentControl<HtmlTableCell>();
 // hide the cell
 parentCell.Visible = false;
 }
}

Listing 10 – DynamicControl_Init handler

Note: The DynamicControl must have it’s DataField set otherwise it will throw an error.

Grouping with visual separators

Figure 4 – Grouping with visual separators.

Now we have separators working, “made so” I would think, well only partially with this version you have to repeat all the above with a few minor changed for Edit and Insert EntityTemplates, but that is in the sample.

Note: Remember this sample is with Visual Studio 2010 and .Net 4.0 RC1

Download

[フレーム]

As always have fun coding

Monday, 25 January 2010

Conditional UIHint for Dynamic Data v2 and .Net 4.0

Still not sure if I should call the .Net 4.0 v2 Big Grin

For the original article see Conditional UIHint from back in October last year, what we want to achieve is change the Field Template in use by page i.e. lets say we have Foreign Key column which would use the default ForeignKey field template but I wanted to use a custom field template during edit currently I would need to create two custom field templates one for edit and one for read only modes. With the ConditionalUIHint I can just apply the attribute like so:

[ConditionalUIHint("MyForeignKey", PageTemplate.Edit)]

And then in Edit mode it would use my custom field template.

As I said in my previous post on this for DDv1 in .Net 4.0 we can create our own custom Meta Classes (MetaModel, MetaTable and MetaColumn etc.); now I said if we could override the UIHint property of the MetaColumn, MetaChildrenColumnand MetaForeignKeyColumn’s then this sample could be made cooler i.e. to get this in DD all we would have to do is change this

private static MetaModel s_defaultModel = new MetaModel();
public static MetaModel DefaultModel
{
 get { return s_defaultModel; }
}

Listing 1 – getting metamodel in Global.asax.cs

to this

// get the custom metamodel
private static MetaModel s_defaultModel = new CustomMetaModel();
public static MetaModel DefaultModel
{
 get { return s_defaultModel; }
}

Listing 2 – getting custom metamodel in Global.asax.cs

so all we have to do is use our own custom metamodel in place of the default Dynamic Data MetaModel cool and if you read A Great Buried Sample in Dynamic Data Preview 4 – Dynamic Data Futures you can use this to control all sorts of things centrally in Dynamic Data without touching each page. Applause

If you look back to the A Great Buried Sample in Dynamic Data Preview 4 – Dynamic Data Futures article you will see that there were five files that made up the Custom Meta classes:

Meta Classes

Image 1 - Meta Classes

Due to low budget on imagination this post they will all be prefixed with CustomBig Grin

We will look at the three extension methods first. the first extension method replaces my old method of finding out which page template we are in:

/// <summary>
/// page extension
/// </summary>
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)
{
 // get pages path
 var path = page.AppRelativeVirtualPath;
 // trim it so we just have the page name
 var pageName = path.Substring(path.LastIndexOf("/") + 1, 
 path.Length - path.LastIndexOf("/") - EXTENSION.Length - 1);
 PageTemplate pageTemplate;
 // extract the page template from the page name
 if (Enum.TryParse<PageTemplate>(pageName, out pageTemplate))
 return pageTemplate;
 else
 return PageTemplate.Unknown;
}

Listing 3 – GetPageTemplate extension method

here I’m getting the page template by extracting the page name excluding extension and then matching it to my PageTemplate enum Listing 4 and returning PageTemplate.Unkown if no match is found (after all I may be using some PageTemplates other that the default ones).

[Flags]
public enum PageTemplate
{
 // standard page templates
 Details = 0x01,
 Edit = 0x02,
 Insert = 0x04,
 List = 0x08,
 ListDetails = 0x10,
 // default if unknown
 Unknown = 0xff
}

Listing 4 – PageTemplate enum

And then we have two more extension methods that are used in each Meta Column type to return the ConditionalUIHint:

/// <summary>
/// Gets the Conditional UIHint.
/// </summary>
/// <param name="column">The column.</param>
/// <param name="baseUIHint">The base UIHint.</param>
/// <returns>a UIHint string</returns>
public static String GetUIHintConditionally(this MetaColumn column, String baseUIHint)
{
 // need to get the current page template
 var page = (System.Web.UI.Page)System.Web.HttpContext.Current.CurrentHandler;
 var pageTemplate = page.GetPageTemplate();
 
 var conditionalUIHint = column.GetAttribute<ConditionalUIHintAttribute>();
 if (conditionalUIHint != null && 
 conditionalUIHint.HasConditionalUIHint(pageTemplate))
 return conditionalUIHint.UIHint;
 else
 return baseUIHint;
}
/// <summary>
/// Determines whether [has conditional UI hint] 
/// [the specified conditional UI hint].
/// </summary>
/// <param name="conditionalUIHint">The conditional UI hint.</param>
/// <param name="currentPage">The current page.</param>
/// <returns>
/// <c>true</c> if [has conditional UI hint] 
/// [the specified conditional UI hint]; otherwise, <c>false</c>.
/// </returns>
private static Boolean HasConditionalUIHint(
 this ConditionalUIHintAttribute conditionalUIHint, 
 PageTemplate currentPage)
{
 return (conditionalUIHint.PageTemplates & currentPage) == currentPage;
}

Listing 5 – GetUIHintConditionally and HasConditionalUIHint

In Listing 5 we have GetUIHintConditionally and HasConditionalUIHint the first get the UIHint and if the current page template matches one in the ConditionalUIHint the ConditionalUIHint is returned. The HasConditionalUIHint is used to determine if there is a page template match.

public class CustomMetaColumn : MetaColumn
{
 public CustomMetaColumn(
 MetaTable table, 
 ColumnProvider 
 columnProvider) : 
 base(table, columnProvider) { }
 protected override void Initialize() { base.Initialize(); }
 /// <summary>
 /// Gets the name of the field template 
 /// specified for the data field.
 /// </summary>
 /// <value></value>
 /// <returns>
 /// The name of the field template 
 /// specified for the data field.
 /// </returns>
 public override string UIHint
 {
 get
 {
 return this.GetUIHintConditionally(base.UIHint);
 }
 }
}

Listing 6 – CustomMetaColumn (Children and ForeignKey are basically the same)

Now all we need in each Meta Column type is to call the GetUIHintConditionally with the current UIHint and if the conditions are met then the ConditionalUIHint will replace the default one.

Each of the other  Meta classes is there just to facilitate this and are very basic overrides of the base classes.

Also my standard Attribute extension methods are in there.

Downloads

[フレーム]

Happy coding

Friday, 4 December 2009

Hiding Foreign Key or Children Columns Globally in Dynamic Data

A continuation of Hiding Foreign Key column Globally in Dynamic Data. Kharot here was asking in this tread Conditional display of Foreign key navigation for a way to do this same thing with the Children FieldTemplate (it took me a while to catch on Embarrassed) and so here it is, it’s basically the same as previouly written but just a few changes to the attribute and the Extension method used in the previous example.

/// <summary>
/// Checks if either the Foreign Key or 
/// Children navigation fields the are hidden.
/// </summary>
/// <param name="column">The current MetaColumn.</param>
/// <returns>
/// true if either the Foreign Key or Children 
/// navigation field are set to hidden at table level
/// </returns>
public static Boolean FkIsHidden(this MetaColumn column)
{
 var fkColumn = column as MetaForeignKeyColumn;
 if (fkColumn != null)
 return fkColumn.ParentTable.
 GetAttributeOrDefault<HideFKColumnAttribute>().
 ForeignKeyFieldIsHidden;
 
 var childrenColumn = column as MetaChildrenColumn;
 if (childrenColumn != null)
 return childrenColumn.ChildTable.
 GetAttributeOrDefault<HideFKColumnAttribute>().
 ChildrenFieldIsHidden;
 return false;
}

Listing 1 – the Extension method

All I’ve done here is test for the column being a hidden in either the Parent or Children tables.

/// <summary>
/// Hides the ForeignKey or Children Navigation Column
/// </summary>
[AttributeUsage(AttributeTargets.Class)]
public class HideFKColumnAttribute : Attribute
{
 /// <summary>
 /// Gets or sets a value indicating whether [foreign key field hidden].
 /// </summary>
 /// <value>
 /// <c>true</c> if [foreign key field hidden]; otherwise, <c>false</c>.
 /// </value>
 public Boolean ForeignKeyFieldIsHidden { get; set; }
 /// <summary>
 /// Gets or sets a value indicating whether [children field hidden].
 /// </summary>
 /// <value><c>true</c> if [children field hidden]; otherwise, <c>false</c>.</value>
 public Boolean ChildrenFieldIsHidden { get; set; }
 /// <summary>
 /// Initializes a new instance of the <see cref="HideFKColumnAttribute"/> class.
 /// </summary>
 public HideFKColumnAttribute()
 {
 ForeignKeyFieldIsHidden = false;
 ChildrenFieldIsHidden = false;
 }
}

Listing 2 – HideFKColumnAttribute

Here I’ve added a new property and done a little refactoring to make things make sense and since it’s a Foreign Key relationship I’ve left the attribute with the same name.

[HideFKColumn(
 ForeignKeyFieldIsHidden = true,
 ChildrenFieldIsHidden = true)]
public partial class Employee
{
 // code omitted for brevity
}

Listing 3 – the metadata

I’ve made no changes to the IAutoFieldGenerator so things should just work, you can now hide the Foreign Key or Children columns globally.

Download

[フレーム]

Remember to have fun coding Happy Wizzard

Tuesday, 14 July 2009

Securing Dynamic Data Preview 4 Refresh – Part 2

Continuing from the previous article Securing Dynamic Data Preview 4 Refresh – Part 1 we will proceed to complete the second two items in the to do list below:

Things we will need to Do

  • Dynamic Data Route Handler
  • Remove Delete Link from List and Details pages
  • Secure Meta model classes
  • Make columns read only using Entity Templates.

Secure Meta model classes

It would have been nice if I could have overridden the Scaffold and IsReadOnly methods of the MetaColumn class for this there would have been less code peppered around Dynamic Data but we can hope for the future.

public class SecureMetaModel : MetaModel
{
 /// <summary>
 /// Creates the metatable.
 /// </summary>
 /// <param name="provider">The metatable provider.</param>
 /// <returns></returns>
 protected override MetaTable CreateTable(TableProvider provider)
 {
 return new SecureMetaTable(this, provider);
 }
}

Listing 1 – SecureMetaModel class

public class SecureMetaTable : MetaTable
{
 /// <summary>
 /// Initializes a new instance of the <see cref="SecureMetaTable"/> class.
 /// </summary>
 /// <param name="metaModel">The meta model.</param>
 /// <param name="tableProvider">The table provider.</param>
 public SecureMetaTable(
 MetaModel metaModel, 
 TableProvider tableProvider) :
 base(metaModel, tableProvider) { }
 protected override void Initialize()
 {
 base.Initialize();
 }
 /// <summary>
 /// Gets the scaffold columns.
 /// </summary>
 /// <param name="mode">The mode.</param>
 /// <param name="containerType">Type of the container.</param>
 /// <returns></returns>
 public override IEnumerable<MetaColumn> GetScaffoldColumns(
 DataBoundControlMode mode, 
 ContainerType containerType)
 {
 return from column in base.GetScaffoldColumns(mode, containerType)
 where column.SecureColumnVisible()
 select column;
 }
}

Listing 2 – SecureMetaTable class

/// <summary>
/// Secures the column visible.
/// </summary>
/// <param name="column">The column.</param>
/// <returns></returns>
public static Boolean SecureColumnVisible(this MetaColumn column)
{
 var userRoles = Roles.GetRolesForUser();
 var activeDenys = column.GetColumnPermissions(userRoles);
 if (activeDenys.Contains(ColumnDeny.Read))
 return false;
 else
 return true;
}

Listing 3 – SecureColumnVisible extension method

[Flags]
public enum ColumnDeny
{
 Read = 1,
 Write = 2,
}

Listing 4 – ColumnDeny enum

So in the above four listings we have the simplified solution to hide columns based on user roles.

How it works

Listing 1 is required to let us include the SecureMetaTable in the default model Listing 2 is the SecureMetaTable all we do here is filter the columns based on user roles see Listing 3 SecureColumnVisible which hides columns based on ColumnDeny.Read this very easily and cleanly done thanks to the ASP.Net team and letting us derive from the meta classes.

Make columns read only using Entity Templates

Now to make columns read only in Edit or Insert modes based on use roles, for this we will modify tow of the default EntityTemplates Default_Edit.ascx.cs and Default_Insert.ascx.cs in these template we add the code in listing 5 to the DynamicControl_Init event handler.

if (currentColumn.SecureColumnReadOnly())
 dynamicControl.Mode = DataBoundControlMode.ReadOnly;
else
 dynamicControl.Mode = DataBoundControlMode.Edit; //Insert for the insert template
Listing 5 – Code to make a field read only in Edit or Insert modes
protected void DynamicControl_Init(object sender, EventArgs e)
{
 DynamicControl dynamicControl = (DynamicControl)sender;
 dynamicControl.DataField = currentColumn.Name;
 if (currentColumn.SecureColumnReadOnly())
 dynamicControl.Mode = DataBoundControlMode.ReadOnly;
 else
 dynamicControl.Mode = DataBoundControlMode.Edit; //Insert for the insert template
}

Listing 6 – finished DynamicControl_Init for the Edit Entity Template

/// <summary>
/// Secures the column read only.
/// </summary>
/// <param name="column">The column.</param>
/// <returns></returns>
public static Boolean SecureColumnReadOnly(this MetaColumn column)
{
 var userRoles = Roles.GetRolesForUser();
 var activeDenys = column.GetColumnPermissions(userRoles);
 if (activeDenys.Contains(ColumnDeny.Write))
 return true;
 else
 return false;
}

Listing 7 – SecureColumnReadOnly extension method

/// <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<ColumnDeny> GetColumnPermissions(this MetaColumn column, String[] roles)
{
 var permissions = new List<ColumnDeny>();
 // 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<SecureColumnAttribute>()
 where a.HasAnyRole(roles)
 select a.Permission).ToList();
 }
 return permissions;
}

Listing 8 – GetColumnPermissions extension method

The above code Listings 5 & 6 simply test to see if the column is restricted to read only based on users roles and uses Listing 7 & 8 extension methods to achieve this.

And finally to make this work with Dynamic Data we need to modify the Global.asax

public class Global : System.Web.HttpApplication
{
 private static MetaModel s_defaultModel = new SecureMetaModel();
 public static MetaModel DefaultModel
 {
 get
 {
 return s_defaultModel;
 }
 }
 public static void RegisterRoutes(RouteCollection routes)
 {
 DefaultModel.RegisterContext(
 typeof(Models.NorthwindEntities), 
 new ContextConfiguration() { ScaffoldAllTables = true });
 routes.Add(new DynamicDataRoute("{table}/{action}.aspx")
 {
 Constraints = new RouteValueDictionary(new { action = "List|Details|Edit|Insert" }),
 RouteHandler = new SecureDynamicDataRouteHandler(),
 Model = DefaultModel
 });
}
 void Application_Start(object sender, EventArgs e)
 {
 RegisterRoutes(RouteTable.Routes);
 }
}

Listing 9 – adding the SecureMetaModel to the Globalasax

Once we have a declared  our metamodel property in the Global.asax we can just reference it for the app by using it to register the context.

Download

Note: This has an ASPNETDB database which requires SQL Express 2008 and a connection to Northwind you will need to edit the connection string for Northwind.
[フレーム]

I’m working on better hyperlinks for another post to follow shortly, these hyperlinks will offer the option of:

  • Hiding the link if it is disabled
  • Showing plain text if disabled

via an property at design time.

Monday, 13 July 2009

Securing Dynamic Data Preview 4 Refresh – Part 1

This article is another stab at creating a simple framework to add a simple security layer to Dynamic Data. This time I’ve based my first level of security on the sample posted by Veloce here Secure Dynamic Data Site. And getting column level security using the Great Buried Sample in Dynamic Data Preview 4 – Dynamic Data Futures I found last month.

Like my previous security layers for Dynamic Data I’m going to use a permissive system because you have to add these attributes to the metadata classes (and that can be a laborious task) and so I though is would be better under these circumstances to just remove access at individual tables and columns, rather than having to add attributes to every table and column to set the security level.

Things we will need to Do

  • Dynamic Data Route Handler
  • Remove Delete Link from List and Details pages
  • Secure Meta model classes
  • Make columns read only using Entity Templates.

Dynamic Data Route Handler

Firstly again we must thank Veloce for his Secure Dynamic Data Site see his blog for morebits (pun intended) of Dynamic Data goodness. So what I decided to do was cut out a load of stuff from his example and pare it down to something that is easy to modify and understand.

/// <summary>
/// The SecureDynamicDataRouteHandler enables the 
/// user to access a table based on the following:
/// the Roles and TableDeny values assigned to 
/// the SecureTableAttribute.
/// </summary>
public class SecureDynamicDataRouteHandler : DynamicDataRouteHandler
{
 public SecureDynamicDataRouteHandler() { }
 /// <summary>
 /// Creates the handler.
 /// </summary>
 /// <param name="route">The route.</param>
 /// <param name="table">The table.</param>
 /// <param name="action">The action.</param>
 /// <returns>An IHttpHandler</returns>
 public override IHttpHandler CreateHandler(
 DynamicDataRoute route,
 MetaTable table,
 string action)
 {
 var usersRoles = Roles.GetRolesForUser();
 var tableRestrictions = table.Attributes.OfType<SecureTableAttribute>();
 // if no permission exist then full access is granted
 if (tableRestrictions.Count() == 0)
 return base.CreateHandler(route, table, action);
 foreach (var tp in tableRestrictions)
 {
 if (tp.HasAnyRole(usersRoles))
 {
 // if any action is denied return no route
 if ((tp.Restriction & TableDeny.Read) == TableDeny.Read)
 return null;
 if ((tp.Restriction & TableDeny.Write) == TableDeny.Write &&
 ((action == "Edit") || (action == "Insert")))
 return null;
}
 }
 return base.CreateHandler(route, table, action);
 }
}

Listing 1 – SecureDynamicDataRouteHandler

This route handler is called each time a route is evaluated see listing 2 where we have added RouteHandler = new SecureDynamicDataRouteHandler() to the default route.

routes.Add(new DynamicDataRoute("{table}/{action}.aspx")
{
 Constraints = new RouteValueDictionary(new { action = "List|Details|Edit|Insert" }),
 RouteHandler = new SecureDynamicDataRouteHandler(),
 Model = DefaultModel
});

Listing 2 – Default route in Global.asax.cs

Now look at the CreateHandler method of Listing 1 1st we get users roles and the tables permissions, then if the table has no restrictions we just return a handler from the base DynamicDataRouteHandler class. After this we check each table restriction to see if this user is in one of the roles in the restriction, then is the user has one of the roles in the restriction we check the restrictions (most restricting first) for a match and then deny the route by returning null appropriately.

[Flags]
public enum TableDeny
{
 Read = 1,
 Write = 2,
 Delete = 4,
}
[Flags]
public enum DenyAction
{
 Delete = 0x01,
 Details = 0x02,
 Edit = 0x04,
 Insert = 0x08,
 List = 0x10,
}

Listing 3 – Security enums

Note: You don’t need to have the values appended to the enum i.e. Delete = 0x01 but it’s worth noting that if you don’t set the first value to 1 it will default to 0 and any failed result will match 0 i.e. tp.Restriction & DenyAction.List if enum then even it tp.Restriction does not AND with DenyAction.List the result will be 0

Listing 3 shows the security enums used in this sample however the DenyAction is not used I include it here as an option I considered in place of TableDeny enum. Let me explain you could replace the code inside the foreach loop of the route handler with Listing 4.

// alternate route handler code
if ((tp.Restriction & DenyAction.List) == DenyAction.List &&
 action == "List")
 return null;
if ((tp.Restriction & DenyAction.Details) == DenyAction.Details &&
 action == "Details")
 return null;
if ((tp.Restriction & DenyAction.Edit) == DenyAction.Edit &&
 action == "Edit")
 return null;
if ((tp.Restriction & DenyAction.Insert) == DenyAction.Insert &&
 action == "Insert")
 return null; 

Listing 4 – alternate route handler code

This would allow you to deny individual actions instead of Read or Write as in basic form of the route handler.

Note: You should also note the use of the [Flags] attribute on the enums as this allows this sort of declaration in the metadata:
[SecureTable(TableDeny.Write | TableDeny.Delete, "Sales")]
which is the reason why we have the test in the form of:
(tp.Restriction & DenyAction.List) == DenyAction.List 
and not
tp.Restriction == DenyAction.List
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public class SecureTableAttribute : 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 SecureTableAttribute(TableDeny 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 TableDeny _permission;
 public TableDeny Restriction
 {
 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());
 }
}

Listing 5 -  SecureTableAttribute

The TableDenyAttribute is strait forward two properties and a methods to check if a roles is in the Roles property.

public static class SecurityExtensionMethods
{
 /// <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;
 }
 /// <summary>
 /// helper method to check for roles in this attribute
 /// the comparison is case insensitive
 /// </summary>
 /// <param name="roles"></param>
 /// <returns></returns>
 public static Boolean HasAnyRole(this SecureTableAttribute tablePermission, String[] roles)
 {
 var tpsRoles = tablePermission.Roles.AllToLower();
 // call extension method to convert array to lower case for compare
 foreach (var role in roles)
 {
 if (tpsRoles.Contains(role.ToLower()))
 return true;
 }
 return false;
 }
}

Listing 6 – some extension methods

These extension methods in Listing 6 are used to make the main code more readable.

Remove Delete Link from List and Details pages

I’m including this with this first article because it will give you a complete solution at table level.

<%@ Control 
 Language="C#" 
 AutoEventWireup="true" 
 CodeBehind="DeleteButton.ascx.cs" 
 Inherits="DD_EF_SecuringDynamicData.DeleteButton" %>
<asp:LinkButton 
 ID="LinkButton1" 
 runat="server" 
 CommandName="Delete" Text="Delete"
 OnClientClick='return confirm("Are you sure you want to delete this item?");' />

Listing 7 – DeleteButton.ascx

public partial class DeleteButton : System.Web.UI.UserControl
{
 protected void Page_Load(object sender, EventArgs e)
 {
 var table = DynamicDataRouteHandler.GetRequestMetaTable(Context);
 var usersRoles = Roles.GetRolesForUser();
 var tableRestrictions = table.Attributes.OfType<SecureTableAttribute>();
 if (tableRestrictions.Count() == 0)
 return;
 foreach (var tp in tableRestrictions)
 {
 if (tp.HasAnyRole(usersRoles) &&
 (tp.Restriction & TableDeny.Delete) == TableDeny.Delete)
 {
 LinkButton1.Visible = false;
 LinkButton1.OnClientClick = null;
 LinkButton1.Enabled = false;
 }
 }
 }
}

Listing 8 – DeleteButton.ascx.cs

This user control in Listing 8 is used to replace the delete button on the List.aspx and Details.aspx pages, this code is very similar to the code in the route handler. We first check each restriction to see if the user is in one of its roles and then if the restriction is TableDeny.Delete and then disable the Delete button.

<ItemTemplate>
 <table id="detailsTable" class="DDDetailsTable" cellpadding="6">
 <asp:DynamicEntity runat="server" />
 <tr class="td">
 <td colspan="2">
 <asp:DynamicHyperLink 
 runat="server" 
 Action="Edit" 
 Text="Edit" />
 <uc1:DeleteButton 
 ID="DetailsItemTemplate1" 
 runat="server" />
 </td>
 </tr>
 </table>
</ItemTemplate>

Listing 9 – Details.aspx page

<Columns>
 <asp:TemplateField>
 <ItemTemplate>
 <asp:DynamicHyperLink 
 runat="server" 
 ID="EditHyperLink" 
 Action="Edit" 
 Text="Edit"/>&nbsp;
 <uc1:DeleteButton 
 ID="DetailsItemTemplate1" 
 runat="server" />&nbsp;
 <asp:DynamicHyperLink 
 ID="DetailsHyperLink" 
 runat="server" 
 Text="Details" />
 </ItemTemplate>
 </asp:TemplateField>
</Columns>

Listing 10 – List.aspx page

<%@ Register src="../Content/DeleteButton.ascx" tagname="DeleteButton" tagprefix="uc1" %>

You will also need to add this line after the Page directive at the top of both the List.aspx and Details.aspx pages.

Note: I will present a slightly more complex version of this hyperlink user control in the next article which will allow the any hyperlink  as an option to remain visible but be disabled.

And that’s it for this article next we will look at using the Great Buried Sample in Dynamic Data Preview 4 – Dynamic Data Futures I mentioned earlier to allow us to hide column based on user’s roles and also add the feature to make columns read only based on user’s roles.

Subscribe to: Comments (Atom)

AltStyle によって変換されたページ (->オリジナル) /