Thursday, 24 December 2009

A Popup Insert control for Dynamic Data (UPDATED)

Well the Popup Control can be used for used for any Web Forms application but for this sample we will use it to give ForeignKey_Edit FieldTemplates a popup Insert page. This will allow you to insert a new item in to the ForeignKeyColumns ParentTable and then set the ForeignKey DropdownList to the new item.

First of all since we are going to be creating a couple of Server Controls may I make a recommendation for Nikhil Kothari and Vandana Datye’s book:

Developing Microsoft ASP.NET Server Controls and Components

Developing Microsoft ASP.NET Server Controls and Components

I found the book very thorough and detailed.

The Popup Controls

I found several articles that offered a popup solution with a return value(s) but decided that none of then offered the solution I wanted for using with Dynamic Data. This is what I wanted:

  • A simple control to drop onto the page that would show a button.
  • Send a return value back to the page that created the popup.
  • and then act on the value returned.

Seems simple enough doesn't it but as always if someone else has done it they want money for it Surprised so I thought I’ll build one and here we go.

Note: I don’t plan to say what is already said in Nikhil and Vandana’s book so this won’t be a Server Controls Tutorial just an explanation of what these controls do (after all the book does it very well).

First of all lets explain what we want to build:

  • PopupButton control
  • PopupClient control
  • JavaScript to do the popup
function NAC_OpenPopup(popupUrl, returnParameter, returnControl, title, width, height) {
    ///<summary>Open a new window in the centre of the screen using the passed in URL.</summary>
    ///<field name="popupUrl" type="string">The popup window's URL as a string.</field>
    ///<field name="returnParameter" type="string">The QueryString Field.</field>
    ///<field name="returnControl" type="string">The name of the control to pass in the url.</field>
    ///<field name="title" type="string">The name of the window as a string.</field>
    ///<field name="width" type="Number" integer="true">The width of the the window as an int.</field>
    ///<field name="height" type="Number" integer="true">The height of the the window as an int.</field>
    
    // pass the id of the return control
    popupUrl = popupUrl + "?" + returnParameter + "=" + returnControl;
    
    // calculate the x and y positions to center the popup.
    var x = 0;
    var y = 0;
    x = (screen.availWidth - 12 - width) / 2;
    y = (screen.availHeight - 48 - height) / 2;

    // setup the features
    var features =
        "'screenX=" + x +
        ",screenY=" + y +
        ",width=" + width +
        ",height=" + height +
        ",top=" + y +
        ",left=" + x +
        ",status=no" +
        ",resizable=no" +
        ",scrollbars=yes" +
        ",toolbar=no" +
        ",location=no" +
        ",modal=yes" + 
        ",menubar=no'";
    
    // open the new window
    var NewWindow = window.open(popupUrl, title, features);
    // set focus to the new window
    NewWindow.focus();
    // return the new window so we can detect the close
    return NewWindow;
}

Listing 1 – NAC_OpenPopup JavaScript function

function NAC_PopupButton1func() {
    // get new popup window
    var NAC_PopupButton1Var = NAC_OpenPopup(
        'Page.aspx',
        'ReturnValue',
        'PopupButton1',
        'Popup',
        '600',
        '600');
    // wait till windows closes
    while (!NAC_PopupButton1Var.closed) { }
    // do postback
    __doPostBack('PopupButton1', '');
}

Listing 2 – This JavaScript function is generated on the page for each PopupButton

// get the return value saved in client
var value = document.getElementById("PopupClient1").value;
// if the value is not empty return it to calling page
if (value != "") {
    // pass value to returnControl
    window.opener.document.getElementById("PopupButton1").value = value
    // call postback on parent window
    window.opener.__doPostBack('PopupButton1', '');
    // close the window
    window.close();
}

Listing 3 – This JavaScript function is generated on the page for each PopupClient

So this is how the these scripts work together; we call the NAC_OpenPopup function from the PopupButton control when the button is clicked and then wait for the popped up window to close and then do a postback to the OnTextChanged event handler. Meanwhile in the client window we have hooked code on the form changed to save the value to the calling window’s PopupButton and then close the window.

using System;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Security.Permissions;
using System.Text;
using System.Web;
using System.Web.UI;

namespace NotAClue.Web.UI.WebControls
{
    [AspNetHostingPermission(SecurityAction.Demand, Level = AspNetHostingPermissionLevel.Minimal)]
    [AspNetHostingPermission(SecurityAction.InheritanceDemand, Level = AspNetHostingPermissionLevel.Minimal)]
    [ParseChildren(true)]
    [PersistChildren(false)]
    [DefaultEvent("TextChanged")]
    [ToolboxData("<{0}:PopupButton runat=\"server\"></{0}:PopupButton>")]
    public class PopupButton : System.Web.UI.WebControls.Button, IPostBackDataHandler
    {
        private String linkButtonId;

        private static readonly object EventTextChanged = new object();

        /// <summary>
        /// Occurs when [text changed].
        /// </summary>
        [Category("Action")]
        [Description("Raised when text changes")]
        public event EventHandler TextChanged
        {
            add { Events.AddHandler(EventTextChanged, value); }
            remove { Events.RemoveHandler(EventTextChanged, value); }
        }

        /// <summary>
        /// Gets or sets the width of the window.
        /// </summary>
        /// <value>The width of the window.</value>
        [Browsable(true)]
        [Bindable(true)]
        [Localizable(false)]
        [Category("Appearance")]
        [DefaultValue(300)]
        public int WindowWidth { get; set; }

        /// <summary>
        /// Gets or sets the height of the window.
        /// </summary>
        /// <value>The height of the window.</value>
        [Browsable(true)]
        [Bindable(true)]
        [Localizable(false)]
        [Category("Appearance")]
        [DefaultValue(300)]
        public int WindowHeight { get; set; }

        /// <summary>
        /// Gets or sets the popup window's title.
        /// </summary>
        /// <value>The popup window title.</value>
        [Browsable(true)]
        [Bindable(true)]
        [Localizable(false)]
        [Category("Appearance")]
        public String Title { get; set; }

        /// <summary>
        /// Gets or sets the values.
        /// </summary>
        /// <value>The values.</value>
        [Browsable(true)]
        [Bindable(true)]
        [Localizable(false)]
        [Category("Appearance")]
        [Description("The values passed back from the popup.")]
        public String ReturnValues { get; set; }

        /// <summary>
        /// Gets or sets the query string field.
        /// </summary>
        /// <value>The query string field.</value>
        [Browsable(true)]
        [Bindable(true)]
        [Localizable(false)]
        [Category("Appearance")]
        [Description("Gets or sets the QueryString Field string")]
        [DefaultValue("ReturnValue")]
        public String QueryStringField { get; set; }

        /// <summary>
        /// Initializes a new instance of the <see cref="PopupButton"/> class.
        /// </summary>
        public PopupButton()
        {
            QueryStringField = "ReturnValue";
            WindowHeight = 300;
            WindowWidth = 300;
        }

        protected override void OnInit(EventArgs e)
        {
            linkButtonId = this.UniqueID + "_Button";

            // Define the name and type of the client scripts on the page.
            string scriptName = "NotAClue.Web.UI.WebControls.ResourceFiles.NAC_OpenPopup.js";
            Type scriptType = this.GetType();

            // Get a ClientScriptManager reference from the Page class.
            ClientScriptManager csm = Page.ClientScript;

            // Check to see if the Client Script Include is already registered.
            if (!csm.IsClientScriptIncludeRegistered(scriptType, scriptName))
            {
                // include main Flash Content script
                string urlJS = csm.GetWebResourceUrl(scriptType, scriptName);
                csm.RegisterClientScriptInclude(scriptType, scriptName, urlJS);
            }

            base.OnInit(e);
        }

        // override RenderBeginTag so no tag is output
        public override void RenderBeginTag(HtmlTextWriter writer)
        {
            //base.RenderBeginTag(writer);
        }

        /// <summary>
        /// Renders the control to the specified HTML writer.
        /// </summary>
        /// <param name="writer">
        /// The <see cref="T:System.Web.UI.HtmlTextWriter"/> 
        /// object that receives the control content.
        /// </param>
        protected override void Render(HtmlTextWriter writer)
        {
            //base.Render(writer);
            //declare the function and variable names
            var popupId = this.UniqueID.Replace("$", "");
            var functionName = "NAC_" + popupId + "Func";
            var variableName = "NAC_" + popupId + "Var";

            StringBuilder script = new StringBuilder();
            script.Append("\n<script language=\"JavaScript\" type=\"text/javascript\">\n");
            script.Append("//<![CDATA[\n");
            script.Append("function " + functionName + "() {\n");
            script.Append("var " + variableName + " = NAC_OpenPopup('" +
                PostBackUrl + "', '" +
                QueryStringField + "', '" +
                this.UniqueID + "', '" +
                Title.Replace(" ", "") + "', '" +
                WindowWidth + "', '" +
                WindowHeight + "');\n");
            //script.Append("while (!" + variableName + ".closed) { }\n");
            //script.Append(Page.ClientScript.GetPostBackEventReference(this, "") + ";\n");
            script.Append("//]]>\n");
            script.Append("}\n");
            script.Append("\n</script>\n");

            writer.Write(script.ToString());

            // render button
            writer.AddAttribute(HtmlTextWriterAttribute.Id, linkButtonId);
            writer.AddAttribute(HtmlTextWriterAttribute.Name, linkButtonId);

            // set CSS styling
            if (!String.IsNullOrEmpty(CssClass))
                writer.AddAttribute(HtmlTextWriterAttribute.Class, CssClass);

            writer.AddAttribute(HtmlTextWriterAttribute.Onclick, "javascript:" + functionName + "();");
            writer.RenderBeginTag(HtmlTextWriterTag.Button);
            writer.Write(Text);
            writer.RenderEndTag();

            // hidden field
            //<input type="hidden" name="HiddenField1" id="HiddenField1" />
            writer.AddAttribute(HtmlTextWriterAttribute.Id, this.UniqueID);
            writer.AddAttribute(HtmlTextWriterAttribute.Name, this.UniqueID);
            writer.AddAttribute(HtmlTextWriterAttribute.Type, "hidden");
            writer.RenderBeginTag(HtmlTextWriterTag.Input);
            writer.RenderEndTag();
        }

        // override RenderEndTag so no tag is output
        public override void RenderEndTag(HtmlTextWriter writer)
        {
            //base.RenderEndTag(writer);
        }

        #region IPostBackDataHandler Members
        /// <summary>
        /// When implemented by a class, processes 
        /// postback data for an ASP.NET server control.
        /// </summary>
        /// <param name="postDataKey">
        /// The key identifier for the control.
        /// </param>
        /// <param name="postCollection">
        /// The collection of all incoming name values.
        /// </param>
        /// <returns>
        /// true if the server control's state 
        /// changes as a result of the postback; 
        /// otherwise, false.
        /// </returns>
        public bool LoadPostData(string postDataKey, NameValueCollection postCollection)
        {
            // get the return value into the property
            ReturnValues = postCollection[this.UniqueID];
            return true;
        }

        /// <summary>
        /// When implemented by a class, signals the 
        /// server control to notify the ASP.NET application
        /// that the state of the control has changed.
        /// </summary>
        public void RaisePostDataChangedEvent()
        {
            // raise the OnTextChanged event
            OnTextChanged(EventArgs.Empty);
        }

        /// <summary>
        /// Raises the <see cref="E:TextChanged"/> event.
        /// </summary>
        /// <param name="e">
        /// The <see cref="System.EventArgs"/> 
        /// instance containing the event data.
        /// </param>
        protected virtual void OnTextChanged(EventArgs e)
        {
            EventHandler textChangedHandler = (EventHandler)Events[EventTextChanged];
            // raise the event if a handler is attached.
            if (textChangedHandler != null)
                textChangedHandler(this, e);
        }
        #endregion
    }
}

Listing 4 – PopupButton class

using System;
using System.ComponentModel;
using System.Security.Permissions;
using System.Text;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;

namespace NotAClue.Web.UI.WebControls
{
    [AspNetHostingPermission(SecurityAction.Demand, Level = AspNetHostingPermissionLevel.Minimal)]
    [AspNetHostingPermission(SecurityAction.InheritanceDemand, Level = AspNetHostingPermissionLevel.Minimal)]
    [Designer("NotAClue.Web.UI.WebControls.PopupClientDesigner, NotAClue.Web.UI.WebControls")]
    [ToolboxData("<{0}:PopupClient runat=\"server\"></{0}:PopupClient>")]
    public class PopupClient : WebControl
    {
        /// <summary>
        /// holds the name of the control 
        /// that we retrun a value to
        /// </summary>
        private String returnValuesControl;

        /// <summary>
        /// Gets or sets the values.
        /// </summary>
        /// <value>The values.</value>
        [Browsable(true)]
        [Bindable(true)]
        [Localizable(false)]
        [Category("Appearance")]
        [Description("Gets or sets the return values")]
        public String ReturnValues { get; set; }

        /// <summary>
        /// Gets or sets the query string field.
        /// </summary>
        /// <value>The query string field.</value>
        [Browsable(true)]
        [Bindable(true)]
        [Localizable(false)]
        [Category("Appearance")]
        [Description("Gets or sets the QueryString Field string")]
        [DefaultValue("ReturnValue")]
        public String QueryStringField { get; set; }

        /// <summary>
        /// Initializes a new instance of the 
        /// <see cref="PopupClient"/> class.
        /// </summary>
        public PopupClient()
        {
            QueryStringField = "ReturnValue";
        }

        protected override void OnInit(EventArgs e)
        {
            // get return values control
            try
            {
                returnValuesControl = Page.Request.QueryString[QueryStringField];
            }
            catch (Exception)
            {
                returnValuesControl = String.Empty;
            }
            base.OnInit(e);
        }

        // override the RenderBeginTag so no tag is output
        public override void RenderBeginTag(HtmlTextWriter writer)
        {
            //base.RenderBeginTag(writer);
        }

        /// <summary>
        /// Renders the control to the 
        /// specified HTML writer.
        /// </summary>
        /// <param name="writer">
        /// The <see cref="T:System.Web.UI.HtmlTextWriter"/> 
        /// object that receives the control content.
        /// </param>
        protected override void Render(HtmlTextWriter writer)
        {
            // hidden field
            writer.AddAttribute(HtmlTextWriterAttribute.Id, this.UniqueID);
            writer.AddAttribute(HtmlTextWriterAttribute.Name, this.UniqueID);
            writer.AddAttribute(HtmlTextWriterAttribute.Type, "hidden");
            writer.AddAttribute(HtmlTextWriterAttribute.Value, ReturnValues);
            writer.RenderBeginTag(HtmlTextWriterTag.Input);
            writer.RenderEndTag();

            StringBuilder script = new StringBuilder();
            script.Append("\n<script language=\"JavaScript\" type=\"text/javascript\">\n");
            script.Append("//<![CDATA[\n");
            script.Append("    var value = document.getElementById(\"" +
                this.UniqueID + "\").value;\n");

            script.Append("    if(value != \"\") {\n");
            script.Append("        window.opener.document.getElementById(\"" + returnValuesControl + "\").value = value\n");
            script.Append("        window.opener.__doPostBack('" + returnValuesControl + "', '');\n");
            script.Append("        window.close();\n");
            script.Append("//]]>\n");
            script.Append("    }\n");
            script.Append("\n</script>\n");

            writer.Write(script.ToString());

            //base.Render(writer);
        }

        // override the RenderEndTag so no tag is output
        public override void RenderEndTag(HtmlTextWriter writer)
        {
            //base.RenderEndTag(writer);
        }
    }
}

Listing 5 – PopupClient

Note: Just explaining the two controls would take an age so get the book mentioned above Open-mouthed

Integrating it into a Dynamic Data site

We will add an attribute to set whether the PopupButton is displayed and what size the popup windows should be.

/// <summary>
/// An attribute used to specify the filtering behaviour for a column.
/// </summary>
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)]
public class PopupInsertAttribute : Attribute
{
    public int WindowWidth { get; set; }
    
    public int WindowHeight { get; set; }

    public PopupInsertAttribute(int width, int height)
    {
        WindowWidth = width;
        WindowHeight = height;
    }
}

Listing 6 – the PopupInsertAttribute

First we will add the PopupButton to the ForeignKey_Edit field template.

protected void Page_Load(object sender, EventArgs e)
{
    if (DropDownList1.Items.Count == 0)
    {
        if (!Column.IsRequired)
            DropDownList1.Items.Add(new ListItem("[Not Set]", ""));

        PopulateListControl(DropDownList1);
    }
    // setup popup
    var popup = MetadataAttributes.GetAttribute<PopupInsertAttribute>();
    if (popup != null)
    {
        PopupButton1.PostBackUrl = ForeignKeyColumn.ParentTable.GetActionPath("PopupInsert");
        PopupButton1.Visible = true;
        PopupButton1.WindowWidth = popup.WindowWidth;
        PopupButton1.WindowHeight = popup.WindowHeight;
        PopupButton1.Title = ForeignKeyColumn.ParentTable.DisplayName;
    }
}

protected void PopupButton1_TextChanged(object sender, EventArgs e)
{
    string foreignkey = PopupButton1.ReturnValues;
    //if no value just return don't do anything
    if (String.IsNullOrEmpty(PopupButton1.ReturnValues))
        return;

    // reset the DDL
    DropDownList1.Items.Clear();
    PopulateListControl(DropDownList1);
    if (!Column.IsRequired)
        DropDownList1.Items.Add(new ListItem("[Not Set]", ""));

    ListItem item = DropDownList1.Items.FindByValue(foreignkey);
    if (item != null)
        DropDownList1.SelectedValue = foreignkey;
}

Listing 7 – Modified Page_Load and the TextChange event handlers for the ForeignKey_Edit field.

Now for the PopupInsert page first make a copy of the standard Insert page (if using a Web Application Project you must change the pages class name also)

public partial class PopupInsert : System.Web.UI.Page
{
    protected MetaTable table;

    protected void Page_Init(object sender, EventArgs e)
    {
        DynamicDataManager1.RegisterControl(DetailsView1);

        // turn partial render off.
        var scriptManager = ScriptManager.GetCurrent(Page);
        if (scriptManager != null)
            scriptManager.EnablePartialRendering = false;
    }

    protected void Page_Load(object sender, EventArgs e)
    {
        table = DetailsDataSource.GetTable();
        Title = table.DisplayName;
    }

    protected void DetailsView1_ItemCommand(object sender, DetailsViewCommandEventArgs e)
    {
        if (e.CommandName == DataControlCommands.CancelCommandName)
        {
            // Response.Redirect(table.ListActionPath);
            // close popup cleanly making sure the return value is an empty string
            PopupClient1.ReturnValues = "";
            Response.Write("<script language='javascript'> { self.close() }</script>");
        }
    }

    protected void DetailsView1_ItemInserted(object sender, DetailsViewInsertedEventArgs e)
    {
        // don't respond to the DetailsView1_ItemInserted here anymore
        //if (e.Exception == null || e.ExceptionHandled)
        //{
        //    Response.Redirect(table.ListActionPath);
        //}
    }

    protected void DetailsDataSource_Inserted(object sender, LinqDataSourceStatusEventArgs e)
    {
        if (e.Exception == null || e.ExceptionHandled)
        {
            // get the PK and return it to the parent page
            PopupClient1.ReturnValues = table.GetPrimaryKeyString(e.Result);
        }
    }
}

Listing 8 – PopupInsert page.

Sample Metadata

[MetadataType(typeof(ProductMetadata))]
partial class Product
{
    partial class ProductMetadata
    {
        public Object ProductID { get; set; }
        public Object ProductName { get; set; }
        public Object SupplierID { get; set; }
        public Object CategoryID { get; set; }
        public Object QuantityPerUnit { get; set; }
        public Object UnitPrice { get; set; }
        public Object UnitsInStock { get; set; }
        public Object UnitsOnOrder { get; set; }
        public Object ReorderLevel { get; set; }
        public Object Discontinued { get; set; }
        // Entity Set 
        public Object Order_Details { get; set; }
        // Entity Ref 
        [PopupInsert(600, 500)]
        public Object Category { get; set; }
        // Entity Ref 
        public Object Supplier { get; set; }
    }
}

Listing 9 – sample metadata

An Explanation of how it all works

We add the PopupInsertAttribute to the FK column we want to be able to do an insert on, this causes the ForeignKey_Edit to display the New PopupButton which opens the PopupInsert page.

When you click either the Insert or cancel button this will cause the PopupInsert page to close wither returning an empty string or the ID of the new entity.

The TextChanged event is fired on the PopupButton in the ForeignKey_Edit field template where if there is a new value the drop down list is repopulated and the new item is selected.

Download

Full source for the PopupButton and PopupClient server controls is provided in the sample.

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

Wednesday, 2 December 2009

Really useful Software – ABCpdf .Net 7.0

Having trawled the net for most of the day here is a sensibly priced and versatile tool for creating and manipulating PDF documents see the Feature Chart for a list of features they just go on and on. For the project I’m working on now this is essential as I need to extract text from an uploaded PDF and store in the DB, and I need to provide an image of the PDF for use in reporting again it wins here too.

02-12-2009 16-37-58

Figure 1 – Text Extraction

02-12-2009 16-38-22

Figure 2 – Render/Rasterize to Image formats

In the above two figures you can see that in every area it exceeds I really don’t think I will ever need another PDF library.

Also the documentation seems real easy to follow see: Save method used for Redering to Image and GetText method for extracting Text from document

I will be reporting back as I get on with my project.

This is cool Open-moutheda must have if working with PDF’s I’m hoping I can apportion many stars to ABCpdf .Net 7.0 as I explore it many features.

Tuesday, 10 November 2009

Digsby is back

Hi there earlier this year I had Digsby on my blog so we could chat but a few of you said that you had issues will popups when leaving my site, so I took it off.

I’m not putting it back on, so if any of you have a popup issue please e-mail me or message me via Digsby to let me know.

Or message me with any questions you have about my articles Open-mouthed

Monday, 26 October 2009

Conditional UIHint

Long time no posts Open-mouthed but I’m posting again should have some DDv2 and ASP.Net 4.0 samples soon Open-mouthed

I just had need of a conditional UIHint for a project I was working and so here it is.

[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public class ConditionalUIHintAttribute : Attribute
{
    public String UIHint { get; set; }
    public PageTemplate PageTemplates { get; private set; }

    public ConditionalUIHintAttribute() { }

    public ConditionalUIHintAttribute(String uiHint, PageTemplate pageTemplates)
    {
        UIHint = uiHint;
        PageTemplates = pageTemplates;
    }
}

Listing 1 – the  conditional UIHint attribute

We will need and IAutoColumnGenerator to generate the columns for us and here that is in Listing 2 I’ve simplified it and all the extra code extension methods are in the sample at the end of the article.

/// <summary>
/// Implements the IAutoFieldGenerator interface and 
/// supports advanced scenarios such as declarative 
/// column ordering, workaround for attribute 
/// localization issues.
/// </summary>
public class AdvancedFieldGenerator : IAutoFieldGenerator
{

    private MetaTable _table;
    private bool _multiItemMode;
    protected Page _page;

    /// <summary>
    /// Creates a new AdvancedFieldGenerator.
    /// </summary>
    /// <param name="table">The table this class generates fields for.</param>
    /// <param name="multiItemMode"><value>true</value> to indicate a 
    /// multi-item control such as GridView, <value>false</value> for 
    /// a single-item control such as DetailsView.
    /// </param>
    public AdvancedFieldGenerator(MetaTable table, bool multiItemMode, Page page)
    {
        if (table == null)
            throw new ArgumentNullException("table");

        _table = table;
        _multiItemMode = multiItemMode;
        _page = page;

    }

    public ICollection GenerateFields(Control control)
    {
        var template = _page.GetPageTemplate();
        var fields = from c in _table.GetVisibleColumns(_multiItemMode, template)
                     select new DynamicField()
                                  {
                                      DataField = c.Name,
                                      HeaderText = c.DisplayName,
                                      UIHint = c.ConditionalUIHint(template)
                                  };

        return fields.ToList();
    }
}

Listing 2 – IAutoColumnGenerator

The neat thing here is the ConditionalUIHint extension method that returns the ConditionalUIHint (see it in Listing 5) if the PageTemplate matches otherwise the standard UIHint is returned, here I’ve created a different Boolean field template called AltBoolean that is used only in Edit pages; you could also specify:

[ConditionalUIHint("AltBoolean", PageTemplate.Edit | PageTemplate.Insert)]

which would use the condition field template in both Edit and Insert mode, but in the sample metadata you will get a different field template for Edit and Insert pages to show off the feature.

[MetadataType(typeof(ProductMetadata))]
partial class Product
{
    partial class ProductMetadata
    {
        public Object ProductID { get; set; } 
        public Object ProductName { get; set; } 
        public Object SupplierID { get; set; } 
        public Object CategoryID { get; set; } 
        public Object QuantityPerUnit { get; set; } 
        public Object UnitPrice { get; set; } 
        public Object UnitsInStock { get; set; } 
        public Object UnitsOnOrder { get; set; } 
        public Object ReorderLevel { get; set; } 

        [ConditionalUIHint("AltBoolean", PageTemplate.Edit)]
        public Object Discontinued { get; set; } 
        // Entity Set 
        public Object Order_Details { get; set; } 
        // Entity Ref 
        public Object Category { get; set; } 
        // Entity Ref 
        public Object Supplier { get; set; } 
    }
}

Listing 4 – sample metadata

To use this in a page you would need to:

public partial class Details : System.Web.UI.Page
{
    protected MetaTable table;

    protected void Page_Init(object sender, EventArgs e)
    {
        DynamicDataManager1.RegisterControl(DetailsView1);
        table = DetailsDataSource.GetTable();
        DetailsView1.RowsGenerator = new AdvancedFieldGenerator(table, true, this);
    }

Listing 3 – Page_Init Details page

move the table assignment to the Page_Init and add the

DetailsView1.RowGenerator = new AdvancedFieldGenerator(table, true, Page);

to the Page_Init of each page you want to use it with. I’ve added this to every page in the sample.

Below are the extension methods attributes required to use the above code

/// <summary>
/// Conditionals the UI hint.
/// </summary>
/// <param name="column">The column.</param>
/// <param name="currentPage">The current page.</param>
/// <returns></returns>
public static String ConditionalUIHint(this MetaColumn column, PageTemplate currentPage)
{
    var conditionalUIHint = column.GetAttribute<ConditionalUIHintAttribute>();
    if (conditionalUIHint != null && conditionalUIHint.AlternateUIHint(currentPage))
        return conditionalUIHint.UIHint;
    else
        return column.UIHint;
}

/// <summary>
/// Alternates the UI hint.
/// </summary>
/// <param name="cUIHint">The c UI hint.</param>
/// <param name="currentPage">The current page.</param>
/// <returns></returns>
public static Boolean AlternateUIHint(this ConditionalUIHintAttribute cUIHint, PageTemplate currentPage)
{
    return (cUIHint.PageTemplates & currentPage) == currentPage;
}

Listing 5 – Extension methods

Updated: For ASP.Net 4.0 if MetaColumn.UIHint (see A Great Buried Sample in Dynamic Data Preview 4 and now ASP.Net 4.0 Beta 2) was virtual we could have overridden it to achieve the same thing without  having to add the AdvancedFieldGenerator to each page.

Download

All other extension methods are provided in the sample.

Have fun Open-mouthed

For the next post I think we may be able to use a FieldTemplateFatory like the EnumFieldTemplateFactory from the old Futures sample. And maybe something extra for ASP.Net 4.0 using the MetaModel classes Dont tell anyone

Tuesday, 28 July 2009

Dynamic Data Custom Field Template – Values List, Take 2

An alternative way of doing the Dynamic Data Custom Field Template – Values List this version uses the UIHint’s ControlParameters property to pass in values to the control.

[MetadataType(typeof(Value.Metadata))]
partial class Value
{
    internal class Metadata
    {
        public object Id { get; set; }
        [UIHint("ValuesList", null, "ValueList", "Y,N,N/A")]
        //[ValuesList("Y,N,N/A")]
        public object TestR { get; set; }
        [UIHint("ValuesList", null, "ValueList", "Y,N,Other")]
        //[ValuesList("Y,N,Other")]
        public object Test { get; set; }
    }
}

Listing 1 – Metadata using the UIHint’s ControlParameters

See the example of using UIHint’s ControlParameters in Listing 1. In the above example you can see that I am passing in the ValueList in the UIHint’s ControlParameters as a key value pair.

/// <summary>
/// Populates a list control with all the values from a parent table.
/// </summary>
/// <param name="listControl">The list control to populate.</param>
protected new void PopulateListControl(ListControl listControl)
{
    var uiHint = Column.GetAttribute<UIHintAttribute>();
    if (uiHint == null || uiHint.ControlParameters.Count == 0 || 
        !uiHint.ControlParameters.Keys.Contains("ValueList"))
        throw new InvalidOperationException(String.Format(
            "ValuesList for FieldTemplate {0} field is missing from UIHint",
            Column.Name));

    var valuesList = ValuesListAttribute(
        uiHint.ControlParameters["ValueList"].ToString());
    if (valuesList.Count() == 0)
        throw new InvalidOperationException(String.Format(
            "ValuesList for FieldTemplate {0} field, is empty",
            Column.Name));

    listControl.Items.AddRange(valuesList);
}

Listing 2 – PopulateListControl

In Listing 2 you can see the extraction of the ValuesList from the UIHint’s ControlParameters collection.

So Listings 1 & 2 will get you the same as the previous article but without the new attribute.

Take 2

In take two of this control binh said “But would you change a little to handle to have combobox with value and display value. The Value will be save to database and The Display value will be showed to user.” so I thought I would add this version here.

My personal view is this should be done either via an FK relationship or a Enum value. (Both my Your First FieldTemplate and Dynamic Data Preview 4 have an Enum FieldTemplate) but here goes.

In the Edit FieldTemplate this is all you need to do is modify the ValuesList method.

public ListItem[] GetValuesList(String values)
{
    var items = values.Split(new char[] { ',', ';' });
    var listItems = (from item in items
                     select new ListItem()
                     {
                         Text = item.Split(new char[] { '|' })[1],
                         Value = item.Split(new char[] { '|' })[0]
                     }).ToArray();
    return listItems;
}
Listing 3 – updated ValuesListAttribute
[MetadataType(typeof(Value.Metadata))]
partial class Value
{
    internal class Metadata
    {
        public object Id { get; set; }
        [UIHint("ValuesList", null, "ValueList", "1|Y,2|N,3|N/A")]
        public object TestR { get; set; }
        [UIHint("ValuesList", null, "ValueList", "1|Y,2|N,3|Other")]
        public object Test { get; set; }
    }
}

Listing 4 – sample metadata

Here you can see that the values and text are seperated by the pipe symbol ‘|’. However having done this you would now need to create a ValuesList (ReadOnly) FieldTemplate to get the ValuesList and the match the value in the field and then display the correct value, which in my opinion would be quite messy, so here I will stick to using Enums.

Download

Wednesday, 15 July 2009

Securing Dynamic Data Preview 4 Refresh – Part 3

Continuing from the previous article Securing Dynamic Data Preview 4 Refresh – Part 2  we will proceed to complete the series by finishing off the hyperlinks so you have the option of them all showing as disabled or all being hidden.

The reasoning behind this extra bit to the series (if you can call two articles a series) is I don’t like

  • Dead hyperlinks when you mouse over you get the hyperlink action i.e. underline.
  • I like consystancy i.e. some links hidden and some shown as dead I want it all to be the same.

So to complete this we will add a new web user control called DynamicHyperLink.ascx which will wrap the asp:DynamicHyperlink control.

%@ Control 
    Language="C#" 
    AutoEventWireup="true" 
    CodeBehind="DynamicHyperLink.ascx.cs" 
    Inherits="DD_EF_SecuringDynamicData.DynamicData.DynamicHyperLink" %>

<asp:DynamicHyperLink 
    ID="DynamicHyperLink1" 
    runat="server" 
    onprerender="DynamicHyperLink1_PreRender">
</asp:DynamicHyperLink>
<asp:Literal ID="Literal1" runat="server">&nbsp;</asp:Literal>

Listing 1 - DynamicHyperLink.ascx

public partial class DynamicHyperLink : System.Web.UI.UserControl
{
    public String Action { get; set; }
    public String Text { get; set; }
    public String CssClass { get; set; }
    public Boolean ShowText { get; set; }
    public String DisabledClass { get; set; }

    protected void Page_Init(object sender, EventArgs e)
    {
        DynamicHyperLink1.Action = Action;
        DynamicHyperLink1.Text = Text;
        DynamicHyperLink1.CssClass = CssClass;
    }

    /// <summary>
    /// Handles the PreRender event of the DynamicHyperLink1 control.
    /// </summary>
    /// <param name="sender">The source of the event.</param>
    /// <param name="e">
    /// The <see cref="System.EventArgs"/> 
    /// instance containing the event data.
    /// </param>
    protected void DynamicHyperLink1_PreRender(object sender, EventArgs e)
    {
        // the DynamicHyperLink is considered disabled if it has no URL
        if (String.IsNullOrEmpty(DynamicHyperLink1.NavigateUrl))
        {
            DynamicHyperLink1.Visible = false;
            Literal1.Visible = false;
            if (ShowText)
            {
                Literal1.Visible = true;
                Literal1.Text = String.Format(
                    "<span class=\"{0}\">{1}</span>&nbsp;", 
                    DisabledClass, 
                    Text);
            }
        }
    }
}

Listing 2 - DynamicHyperLink.ascx.cs

With the DynamicHyperLink web user control we simply have some properties to allow us to pass in the main properties of the asp:DynamicHyperLink Action, Text and CssClass. The last two properties ShowText and DisbledClass are used:

  • ShowText – to show the Text property in a span when the DynamicHyerLink is hidden.
  • DisbledClass – is the css class to use when ShowText is true.

So this is the way it works:

When the DynamicHyperLink’s NavigateUrl is and empty string or null then the DynamicHyperLink is in the disabled state. In the disabled state if ShowText is false then no control is displayed when in a disabled state, and when ShowText is true as span is displayed surrounding the Text property and having the DisabledCalss css as the span’s class.

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

Listing 3 – DeleteHyperLink.ascx

public partial class DeleteHyperLink : System.Web.UI.UserControl
{
    public Boolean ShowText { get; set; }
    public String CssClass { get; set; }
    public String DisabledClass { get; set; }

    protected void Page_Load(object sender, EventArgs e)
    {
        LinkButton1.CssClass = CssClass;

        // get restrictions for the current
        // users access to this table
        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)
        {
            // the LinkButton is considered disabled if delete is denied.
            if (tp.HasAnyRole(usersRoles) &&
                (tp.Restriction & TableDeny.Delete) == TableDeny.Delete)
            {
                LinkButton1.Visible = false;
                LinkButton1.OnClientClick = null;
                LinkButton1.Enabled = false;
                Literal1.Visible = false;
                if (ShowText)
                {
                    Literal1.Visible = true;
                    Literal1.Text = String.Format(
"<span class=\"{0}\">Delete</span>&nbsp;",
DisabledClass); } } } } }

Listing 4 – DeleteHyperLink.ascx.cs

This is pretty similar to the DynamicHyperLink.ascx the main difference being how the LinkButton is determined to be disabled, this is done by the foreach loop checking each restriction on the current table and checking to see if it is TableDeny.Delete and if so setting the disabled state.

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.

Happy coding HappyWizard

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.