Showing posts with label Metadata. Show all posts
Showing posts with label Metadata. Show all posts

Tuesday, 2 June 2009

HTML Editor FieldTemplate for Dynamic Data Using AJAX Control Toolkit Editor

I decided to create an HTML Edit FieldTemplate after the AJAX Control Toolkit HTML Editor was released and a post (Edit.aspx page hanging on text entry) on the ASP.Net Dynamic Data Forum was posted.

For this we will need two FieldTemplates:

  1. Html.ascx
  2. Html_Edit.ascx

The first of the two Html.ascx will display the HTML rendered correctly and the second will use the AJAX Control Toolkit HTML Editor.

The Normal FieldTemplate

<%@ Control 
    Language="C#" 
    Inherits="DD_HTMLEditor.Html_Field" 
    Codebehind="Html.ascx.cs" %>

<asp:Literal 
    runat="server" 
    ID="Literal1" 
    Text="<%# FieldValueEditString %>" />

Listing 1 – Html.ascx

using System;
using System.Web.UI;

namespace DD_HTMLEditor
{
    public partial class Html_Field : System.Web.DynamicData.FieldTemplateUserControl
    {
        public override Control DataControl
        {
            get
            {
                return Literal1;
            }
        }
    }
}

Listing 2 – Html.ascx.cs

As you can see this is a direct copy of the Text.ascx FieldTemplate the only change is to Literal1 in the ascx file instead of FieldValueString we are using FieldValueEditString.

Description Field Showing HTML

Figure 1 – Description Field Showing HTML

The Edit FieldTemplate

<%@ Control 
    Language="C#" 
    Inherits="DD_HTMLEditor.Html_EditField" 
    Codebehind="Html_Edit.ascx.cs" %>
    
<%@ Register 
    Assembly="AjaxControlToolkit" 
    Namespace="AjaxControlToolkit.HTMLEditor"
    TagPrefix="cc1" %>

<cc1:Editor 
    ID="Editor1" 
    Content='<%# FieldValueEditString %>'
    runat="server" />
    
<asp:RequiredFieldValidator 
    runat="server" 
    ID="RequiredFieldValidator1" 
    CssClass="droplist" 
    ControlToValidate="Editor1" 
    Display="Dynamic" 
    Enabled="false" />
    
<asp:RegularExpressionValidator 
    runat="server" 
    ID="RegularExpressionValidator1" 
    CssClass="droplist" 
    ControlToValidate="Editor1" 
    Display="Dynamic" 
    Enabled="false" />
    
<asp:DynamicValidator 
    runat="server" 
    ID="DynamicValidator1" 
    CssClass="droplist" 
    ControlToValidate="Editor1" 
    Display="Dynamic" />

Listing 3 – Html_Edit.ascx file

using System;
using System.Collections.Specialized;
using System.Web.UI;

namespace DD_HTMLEditor
{
    public partial class Html_EditField : System.Web.DynamicData.FieldTemplateUserControl
    {
        protected void Page_Load(object sender, EventArgs e)
        {
            SetUpValidator(RequiredFieldValidator1);
            RequiredFieldValidator1.Text = "*";
            SetUpValidator(RegularExpressionValidator1);
            RegularExpressionValidator1.Text = "*";
            SetUpValidator(DynamicValidator1);
            DynamicValidator1.Text = "*";
        }

        protected override void ExtractValues(IOrderedDictionary dictionary)
        {
            dictionary[Column.Name] = ConvertEditedValue(Editor1.Content);
        }

        public override Control DataControl
        {
            get
            {
                return Editor1;
            }
        }
    }
}

Listing 4 – Html_Edit.ascx.cs file

Here again it’s just the Text_Edit FieldTemplate with a few minor changes (shown in BOLD ITALIC). First we added the AJAC Control Toolkit HTML Editor control to replace the TextBox control and added a reference to the FieldTemplate.

Note:

You could also add a reference for the AJAX Control Toolkit to the web.config

<pages>
    <controls>
        <add tagPrefix="ajaxToolkit"
            namespace="AjaxControlToolkit"
            assembly="AjaxControlToolkit"/>
    </controls>
</pages>

Description Field in Edit mode

Figure 2 – Description Field in Edit mode

Next Steps

Next we need to add some of the configurability of the HTML Editor via a custom Attribute.
using System;
using System.Linq;
using System.Web.DynamicData;
using AjaxControlToolkit.HTMLEditor;

namespace DD_HTMLEditor
{
    /// <summary>
    /// Attribute to identify which column to use as a 
    /// parent column for the child column to depend upon
    /// </summary>
    [AttributeUsage(AttributeTargets.Property)]
    public class HtmlEditorAttribute : Attribute
    {
        /// <summary>
        /// Default Contructor
        /// </summary>
        public HtmlEditorAttribute()
        {
        }

        /// <summary>
        /// Active editing panel (Design, Html, Preview) on control loaded 
        /// </summary>
        public AjaxControlToolkit.HTMLEditor.ActiveModeType ActiveMode { get; set; }

        /// <summary>
        /// If true, editing panel is focused and cursor is set 
        /// inside it ("Design" or "HTML text") on initial load 
        /// or editing panel change
        /// </summary>
        public Boolean AutoFocus { get; set; }

        /// <summary>
        /// A css class override used to define a custom look 
        /// and feel for the HTMLEditor. See the HTMLEditor 
        /// Theming section for more details
        /// </summary>
        public String CssClass { get; set; }

        /// <summary>
        /// Sets the path of additional CSS file used for 
        /// HTMLEditor's content rendering in "Design" panel. 
        /// If not set, the default CSS file is used which is 
        /// embedded as a WebResource and is a 
        /// part of the Toolkit assembly
        /// </summary>
        public String DesignPanelCssPath { get; set; }

        /// <summary>
        /// Sets the path of CSS file used for HTMLEditor's 
        /// content rendering in "Design" and "Preview" panels. 
        /// If not set, the default CSS file is used which is 
        /// embedded as a WebResource and is a part of the 
        /// Toolkit assembly
        /// </summary>
        public String DocumentCssPath  { get; set; }

        /// <summary>
        /// A css class override used to define a custom look for the 
        /// HTMLEditor's "HTML text" mode panel. See the HTMLEditor 
        /// Theming section for more details
        /// </summary>
        public String HtmlPanelCssClass  { get; set; }

        /// <summary>
        /// If true, Tab key navigation is suppressed inside 
        /// HTMLEditor control 
        /// </summary>
        public Boolean IgnoreTab  { get; set; }

        /// <summary>
        /// If true, HTMLEditor's content is cleaned up on 
        /// initial load. MS Word specific tags are removed 
        /// </summary>
        public Boolean InitialCleanUp  { get; set; }

        /// <summary>
        /// If true, JavaScript code is suppressed 
        /// in HTMLEditor's content 
        /// </summary>
        public Boolean NoScript  { get; set; }

        /// <summary>
        ///  If true, all Unicode characters in 
        ///  HTML content are replaced with &#code;
        /// </summary>
        public Boolean NoUnicode { get; set; }

        /// <summary>
        /// The client-side script that executes after 
        /// active mode (editing panel) changed
        /// </summary>
        public String OnClientActiveModeChanged { get; set; }

        /// <summary>
        /// The client-side script that executes before 
        /// active mode (editing panel) changed
        /// </summary>
        public String OnClientBeforeActiveModeChanged { get; set; }

        /// <summary>
        /// If true, no white spaces inserted on Tab key 
        /// press in Design mode. Default Tab key navigation 
        /// is processing in this case 
        /// </summary>
        public Boolean SuppressTabInDesignMode { get; set; }

        /// <summary>
        /// Sets the height of the body of the HTMLEditor 
        /// </summary>
        public int Height { get; set; }

        /// <summary>
        /// Sets the width of the body of the HTMLEditor 
        /// </summary>
        public int Width { get; set; }
    }

    public static partial class HelperExtansionMethods
    {
        /// <summary>
        /// Get the attribute or a default instance of the attribute
        /// if the Column attribute do not contain the attribute
        /// </summary>
        /// <typeparam name="T">
        /// Attribute type
        /// </typeparam>
        /// <param name="table">
        /// Column to search for the attribute on.
        /// </param>
        /// <returns>
        /// The found attribute or a default 
        /// instance of the attribute of type T
        /// </returns>
        public static T GetAttributeOrDefault<T>(this MetaColumn column) where T : Attribute, new()
        {
            return column.Attributes.OfType<T>().DefaultIfEmpty(new T()).FirstOrDefault();
        }
    }
}

Listing 5 – HtmlEditorAttribute

var htmlAttr = Column.GetAttributeOrDefault<HtmlEditorAttribute>();
if (htmlAttr.ActiveMode != ActiveModeType.Design)
    Editor1.ActiveMode = htmlAttr.ActiveMode;
if (htmlAttr.AutoFocus)
    Editor1.AutoFocus = true;
if (htmlAttr.CssClass != null)
    Editor1.CssClass = htmlAttr.CssClass;
if (htmlAttr.DesignPanelCssPath != null)
    Editor1.DesignPanelCssPath = htmlAttr.DesignPanelCssPath;
if (htmlAttr.DocumentCssPath != null)
    Editor1.DocumentCssPath = htmlAttr.DocumentCssPath;
if (htmlAttr.Height > 0)
    Editor1.Height = htmlAttr.Height;
if (htmlAttr.HtmlPanelCssClass != null)
    Editor1.HtmlPanelCssClass = htmlAttr.HtmlPanelCssClass;
if (htmlAttr.IgnoreTab)
    Editor1.IgnoreTab = true;
if (htmlAttr.InitialCleanUp)
    Editor1.InitialCleanUp = true;
if (htmlAttr.NoScript)
    Editor1.NoScript = true;
if (htmlAttr.NoUnicode)
    Editor1.NoUnicode = true;
if (htmlAttr.OnClientActiveModeChanged != null)
    Editor1.OnClientActiveModeChanged = htmlAttr.OnClientActiveModeChanged;
if (htmlAttr.OnClientBeforeActiveModeChanged != null)
    Editor1.OnClientBeforeActiveModeChanged = htmlAttr.OnClientBeforeActiveModeChanged;
if (htmlAttr.SuppressTabInDesignMode)
    Editor1.SuppressTabInDesignMode = true;
if (htmlAttr.Width > 0)
    Editor1.Width = htmlAttr.Width;

Listing 6 – Attribute setting for Html_Edit.ascx

using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using AjaxControlToolkit.HTMLEditor;

namespace DD_HTMLEditor.Models
{
    [MetadataType(typeof(CategoryMD))]
    public partial class Category 
    {
        public partial class CategoryMD
        {
            public object CategoryID {get;set;}
            public object CategoryName {get;set;}

            [UIHint("Html")]
            [HtmlEditor(
                ActiveMode=ActiveModeType.Preview,
                Width=200,
                Height=100
                )]
            public object Description {get;set;}
            public object Picture {get;set;}
            public object Products {get;set;}
        }
    }
}

Listing 7 – Metadata

Above we have the attribute Listing 5 and the code Listing 6 to set the properties of the Html Editor in the Html_Edit.ascx FieldTemplate’s Page_Load event, and finally Listing 7 sample metadata.

Attribute from the Metadata applied

Figure 3 – Attribute from the Metadata applied

Not Rocket Science but a cool Html Editor (Thanks AJAX Toolkit Guys).

download

Wednesday, 28 January 2009

Making Individual Tables Read Only – Dynamic Data

These are two thing you can do to make your table Read Only.

  1. Add an attribute for instance the ReadOnlyAttribute or your own custom attribute
  2. Using Routing to restrict access to the Edit or Insert pages

The first solution I covered in this article Writing Attributes and Extension Methods for Dynamic Data so I’ll just show the Routing example here.

// Setting a table to Read only
var visibleTables = from t in MetaModel.Default.Tables
                    where t.Scaffold == true
                    select t;

var readOnlyTables = new StringBuilder();
foreach (var table in visibleTables)
{
    var isReadOnly = table.Attributes.OfType<ReadOnlyAttribute>().
DefaultIfEmpty(new ReadOnlyAttribute(false)).
FirstOrDefault();
    if (isReadOnly.IsReadOnly)
        readOnlyTables.Append(table.Name + "|");
}

routes.Add(new DynamicDataRoute("{table}/{action}.aspx")
{
    Constraints = new RouteValueDictionary(new
    {
        action = "Edit|Insert",
        table = readOnlyTables.ToString().Substring(0, readOnlyTables.Length - 1)
    }),
    ViewName = "Details",
    Model = model
});

Listing 1 – Routing example.

[ReadOnly(true)]
public partial class Employee {}

Listing 2 – sample attribute.

In Listing 1 we see three things going on:

  1. We get a list of visible tables using the Scaffold attribute to find out if the table should be shown.
  2. Next we loop through the visible tables and if the table is set to ReadOnly then we add it to the readOnlyTables StringBuilder to build a list of read only tables (e.g. “Products|Customers”).
  3. Build a route that constrains the these tables to only get to the Details page.
Note: That this route must appear before the default routes.

Happy coding and remember your a PC HappyWizard but for us coders I prefer the short lived “Live to Code and Code to Live”

Important: The only caveat here is that the Delete button still works. :( But you could catch that in Business logic if you didn’t want to edit each page.

Thursday, 25 September 2008

Dynamic Data Custom PageTemplates with Ajax Control Toolkit Tabs UPDATED: 2008/09/27

These articles are now under the title of Custom PageTemplates:

Custom PageTemplates Part 1 - Custom PageTemplates with Ajax Control Toolkit Tabs Custom PageTemplates Part 2 - A variation of Part 1 with the Details and SubGrid in Tabs Custom PageTemplates Part 3 - Dynamic/Templated Grid with Insert (Using ListView) Custom PageTemplates Part 4 - Dynamic/Templated FromView

I’ve been working for a little while now to get a generic page with a DetailsView showing the parent record and a set of tabs showing all the child records similar to Figure 1.

A Detail/Edit page with all ChildrenColumn as sub grids in a tab control 

Figure 1 – A Detail/Edit page with all ChildrenColumn as sub grids in a tab control

I wanted something that was easy to implement on your own custom page and reused my existing FieldTemplate ParentGrid, I tried several methods to achieve this and I’m going to show the two most successful methods.

Both methods use a FormView as the control to embed the tabs and grids:

  • Creating an external ItemTemplate and load it into the FormView at runtime
  • Have a dynamic ItemTemplate that implements ITemplate.

Making the Custom Page with a FormView

For this sample I’m going to make a custom generic Edit page, so first we need to make a copy of the Edit.aspx page and rename it to EditSubGridViews.aspx rename the class as well to EditSubGridViews in both the aspx file and the code behind.

<asp:UpdatePanel ID="UpdatePanel2" runat="server">
    <ContentTemplate>
    
        <h3>Sub GridViews</h3>
        
        <asp:FormView 
            ID="FormView1" 
            runat="server" 
            DataSourceID="DetailsDataSource">
            <ItemTemplate>
            </ItemTemplate>
        </asp:FormView>
        
    </ContentTemplate>
</asp:UpdatePanel>

Listing 1 – The added the FormView and UpdatePanel

Add Listing 1’s code after the end of the current UpdatePanel.

Note: The DataSourceID="DetailsDataSource" is set to the same data source as the DetailsView1

In the code behind add the following line to the Page_Load event handler.

DynamicDataManager1.RegisterControl(FormView1);

This just registers the FormView with the DynamicDataManager no real magic here.

// load item template
table = DetailsDataSource.GetTable();
String itemTemplate = table.Model.DynamicDataFolderVirtualPath + "Templates/" + table.Name + ".ascx";
if (File.Exists(Server.MapPath(itemTemplate)))
    FormView1.ItemTemplate = LoadTemplate(itemTemplate);
// note if no template is loaded then the FormView will show nothing

Listing 2 – code to load the ItemTemplate

This is the magic, you can create your own custom template as a UserControl to include just the ChildrenTables you want. Here we first of all get a reference to the table so that we reference the Model.DynamicDataFolderVirtualPath (which hold the virtual path to the Dynamic Data folder normally ~/DynamicData/) to build the path to our template. And if the template exists then load it into the FormView’s ItemTemplate. Obviously you could load any of the other FormView templates this way.

Modifying the Route for Edit

To use our new PageTemplate EditSubGridViews we could just have edited the current Edit page, but I thought it would be good to leave that as is and change the route.

// The following statement supports separate-page mode, where the List, Detail, Insert, and 
// Update tasks are performed by using separate pages. To enable this mode, uncomment the following 
// route definition, and comment out the route definitions in the combined-page mode section that follows.
routes.Add(new DynamicDataRoute("{table}/{action}.aspx") {
    Constraints = new RouteValueDictionary(new { action = "List|Details|Insert" }),
    Model = model
});

// add a route to EditSubGridViews
routes.Add(new DynamicDataRoute("{table}/EditSubGridViews.aspx")
{
    Action = PageAction.Edit,
    ViewName = "EditSubGridViews",
    Model = model
});

Listing 3 – Route to EditSubGridViews.aspx added to Global.asax 

In the default root in the Global.asax file remove Edit from action = "List|Details|Edit|Insert" and add the route to EditSubGridViews from Listing 3.

Create the FormView ItemTemplate

To create a template file for FormView all you need to do is create a new UserControl in the folder. The first thing to do is create the Templates folder under the DynamicData folder.

To create the folder from Visual Studio 2008 right click the DynamicData folder and click New Folder

Select New Folder from the context menu

Figure 2 – Select New Folder from the context menu

Now right click the new folder and click Add New Item…

Add New Item

Figure 3 – Add New Item

From the Add New Item dialogue box choose Web User Control and name it Employees.

Create the Web User Control

Figure 4 – Create the Web User Control

Templates folder under the DynamicData folder with the Employees template

Figure 5 – Templates folder under the DynamicData folder with the Employees template

Add the following mark-up to the new template:

<ajaxToolkit:TabContainer ID="TabContainer1" runat="server">
    <ajaxToolkit:TabPanel ID="TabPanel1" HeaderText="My Employees" runat="server">
        <ContentTemplate>
            <asp:DynamicControl ID="DynamicControl1" DataField="Employees" UIHint="ChildrenGrid" Mode="Edit" runat="server">
            </asp:DynamicControl>
        </ContentTemplate>
    </ajaxToolkit:TabPanel>
    <ajaxToolkit:TabPanel ID="TabPanel3" HeaderText="My Orders" runat="server">
        <ContentTemplate>
            <asp:DynamicControl ID="DynamicControl3" DataField="Orders" UIHint="ChildrenGrid" Mode="Edit" runat="server">
            </asp:DynamicControl>
        </ContentTemplate>
    </ajaxToolkit:TabPanel>
</ajaxToolkit:TabContainer>

Listing 4 – ItemTemplate showing Employees and Orders ChildrenGrids

Note: See this post series Dynamic Data and Advanced Field Templates for the ChildrenGrid details.

The important setting here in the DynamicControls are UIHint and DataField, (ChildrenGrid is a FieldTemplate from a previous series of articles on FieldTemplates on my blog) in these parameters we are setting:

  • UIHint setting the FieldTemplate to be used.
  • DataField which field from the table to bind the FieldTemplate to.

When you run the sample you will see something like Figure 6.

Example output from EditSubGridViews.aspx

Figure 6 – Example output from EditSubGridViews.aspx

Creating a Dynamic ItemTemplate that Implements ITemplate

ITemplate documentation can be found on the MSDN website here Creating Web Server Control Templates Programmatically.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.DynamicData;
using System.Web.UI.WebControls;
using AjaxControlToolkit;

/// <summary>
/// Creates an item template that renders any children columns in the passed in table as GridViews
/// </summary>
public class SubGridViewItemTemplate : ITemplate
{
    private MetaTable _table;
    private Page _page;

    public SubGridViewItemTemplate(MetaTable table, Page page)
    {
        _table = table;
        _page = page;
    }

    public void InstantiateIn(Control container)
    {
        IParserAccessor acessor = container;
        // get all the children columns
        var subGridTables = from c in _table.Columns.OfType<MetaChildrenColumn>()
                            select new SubDetails()
                            {
                                Column = c,
                                SubGridMetaData = c.Attributes.OfType<SubGridViewsAttribute>().FirstOrDefault(),
                                Order = c.Attributes.OfType<SubGridViewsAttribute>().FirstOrDefault() != null
                                && c.Attributes.OfType<SubGridViewsAttribute>().FirstOrDefault().Order > 0
                                ? c.Attributes.OfType<SubGridViewsAttribute>().FirstOrDefault().Order
                                : int.MaxValue,
                            };

        // sort the according to Order first and column name second
        // note if SubGridViewsAttribute is not allied or the attrivute
        // has no value for Order then just sort but column name
        subGridTables = from sg in subGridTables
                        orderby sg.Order, sg.Column.Name
                        select sg;

        // make sure there are some children columns
        if (subGridTables.Count() > 0)
        {
            // check if more than one children column present
            if (subGridTables.Count() > 1)
            {
                // create tab container to hold each children column
                var tabContainer = new TabContainer();
                tabContainer.ID = "tabContainer";

                // add event handler
                tabContainer.EnableViewState = true; // ***UPDATED 2008/09/27***

                // add the tab container to the page
                acessor.AddParsedSubObject(tabContainer);

                // add a tab panel for each children table
                foreach (SubDetails SubGridDetails in subGridTables)
                {
                    var tabPanel = new AjaxControlToolkit.TabPanel();
                    tabPanel.ID = "tp" + SubGridDetails.Column.Name;

                    // add the tab panel
                    tabContainer.Tabs.Add(tabPanel);

                    var subGridAttributes = SubGridDetails.Column.Attributes.OfType<SubGridViewsAttribute>().SingleOrDefault();
                    // set the Tab's name to be the tables display name 
                    // or table Name if no attribute is present
                    if (subGridAttributes != null && subGridAttributes.TabName.Length > 0)
                        tabPanel.HeaderText = subGridAttributes.TabName;
                    else
                        tabPanel.HeaderText = SubGridDetails.Column.ChildTable.DisplayName;

                    //Instantiate a DynamicControl for this Children Column
                    var childrenGrid = new DynamicControl(DataBoundControlMode.Edit)
                    {
                        ID = SubGridDetails.Column.Name,

                        // set UIHint
                        UIHint = "ChildrenGrid",

                        // set data field to column name
                        DataField = SubGridDetails.Column.Name
                    };

                    // add the DynamicControl to the tab panel
                    tabPanel.Controls.Add(childrenGrid);
                }
                // set the tab pannels index to 0 which
                // forces the first tab to be selected
                if (!_page.IsPostBack)
                    tabContainer.ActiveTabIndex = 0;
            }
            else
            {
                // if only one sub grid then don't bother with tabs
                SubDetails SubGridDetails = subGridTables.FirstOrDefault();
                var childrenGrid = new DynamicControl(DataBoundControlMode.Edit)
                {
                    ID = SubGridDetails.Column.Name,
                    UIHint = "ChildrenGrid",
                    DataField = SubGridDetails.Column.Name
                };

                // add the grid to the page
                acessor.AddParsedSubObject(childrenGrid);
            }
        }
        else
        {
            // if no children columns
            // add label to show no grids
            var label = new Label();
            label.Text = "There are no SubGrids";
            label.CssClass = "droplist";

            // add the label to the page
            acessor.AddParsedSubObject(label);
        }
    }

    private class SubDetails
    {
        /// <summary>
        /// Column to display
        /// </summary>
        public MetaChildrenColumn Column { get; set; }

        /// <summary>
        /// MetaData if any from the original column
        /// </summary>
        public SubGridViewsAttribute SubGridMetaData { get; set; }

        /// <summary>
        /// Holds the sort order value
        /// </summary>
        public int Order { get; set; }
    }
}

Listing 5 - SubGridViewItemTemplate

Listing 5 is the class that implements ITemplate – SubGridViewItemTemplate, to summarise this generates a template in code at runtime. In this case the template consists of a TabContainer from the Ajax Control Toolkit with a TabPanel for each ChildrenColumn found in the _table passed in to the class in the constructor. The TabPanel in turn contains a DynamicControl with it’s UIHint set to ChildrenGrid.

Personal Note: For me the key thing that Dynamic Data gives us is the DynamicField and DynamicControl which use FieldTemplates, these FielTemplates encapsulate all our field functionality in a central place. This is apart from all the other Dynamic Data goodness that we get from scaffolding etc. For this ITemplate class it remove ALL the complexity from the class and also makes it future proof, because who knows what fields my ChildrenGrid will face in the future? but I don’t need to worry be cause DynamicField and DynamicControl will handle it all.

So an explanation of the code seems appropriate here.

var subGridTables = from c in _table.Columns.OfType<MetaChildrenColumn>()
                    select new SubDetails()
                    {
                        Column = c,
                        SubGridMetaData = c.Attributes.OfType<SubGridViewsAttribute>().FirstOrDefault(),
                        Order = c.Attributes.OfType<SubGridViewsAttribute>().FirstOrDefault() != null
                        && c.Attributes.OfType<SubGridViewsAttribute>().FirstOrDefault().Order > 0 
                        ? c.Attributes.OfType<SubGridViewsAttribute>().FirstOrDefault().Order 
                        : int.MaxValue,
                    };

// sort the according to Order first and column name second
// note if SubGridViewsAttribute is not allied or the attrivute
// has no value for Order then just sort but column name
subGridTables = from sg in subGridTables
                orderby sg.Order, sg.Column.Name
                select sg;

Listing 6 – Getting the ChildrenColumns

These two Linq to Objects statements here firstly get a list of new objects and secondly sorts this list. It’s clear what the first two properties are in the SubDetails class but Order looks a little complex. What’s happening in setting Order is; if the SubGridViewsAttribute is not present or the Order property of SubGridViewsAttribute not set then the Order property of the new SubDetails object is set to int.MaxValue (this is because int can’t be null and by default is initialised to zero).

The next chunk of code is pretty straight forward it just loops over the subGridTables collection and create the appropriate TabContainer and fills it with TabPanels each with it’s own DynamicControl. If however there is only one ChilcrenColumn in the _table then it just adds a single DynamicControl and finally if there are no ChildrenColumns in the _table then it adds a Label that says There are no SubGrids.

Finally at the end of the class is the definition of the SubDetails class.

Adding the SubGridViewItemTemplate a FormView

The code to SubGridViewItemTemplate to a FormView is straight forward:

FormView1.ItemTemplate = new SubGridViewItemTemplate(table);
So to finish off the EditSubGridViews.aspx page we need to add a little more code:
protected void Page_Init(object sender, EventArgs e)
{
    DynamicDataManager1.RegisterControl(DetailsView1);
    DynamicDataManager1.RegisterControl(FormView1);

    // load item template
    table = DetailsDataSource.GetTable();
    String itemTemplate = table.Model.DynamicDataFolderVirtualPath + "Templates/" + table.Name + ".ascx";
    if (File.Exists(Server.MapPath(itemTemplate)))
    {
        FormView1.ItemTemplate = LoadTemplate(itemTemplate);
    }
    else
    {
        // generate the sub grid views if no template available
        table = DetailsDataSource.GetTable();
        FormView1.ItemTemplate = new SubGridViewItemTemplate(table);
    }
}

Listing 7 – adding the SubGridViewItemTemplate to EditSubGridViews.aspx

Now if you supply a custom ItemTemplate for a table then that will be used to format the FormView otherwise it will auto generate the ItemTemplate for the table.

I seem to have missed out the attribute class SubGridViewsAttribute so I'll list it here:

[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public class SubGridViewsAttribute : Attribute
{
    public int Order { get; set; }
    public String TabName { get; set; }
    public SubGridViewsAttribute()
    {
    }
}

Listing 7 - SubGridViewsAttribute

And now some sample metadata:

[MetadataType(typeof(EmployeeMD))]
public partial class Employee 
{ public class EmployeeMD { [SubGridViews( Order = 2, TabName = "My Employees")] public object Employees { get; set; } [SubGridViews( Order = 3, TabName = "My Territories")] public object EmployeeTerritories { get; set; } [SubGridViews( Order = 1, TabName = "My Orders")] public object Orders { get; set; } public object Employee1 { get; set; } } }

Listing 8 - sample metadata showing order and tab name

Now I think that’s neat smile_teeth

Project Download

*** UPDATES ***

UPDATED 2008/09/26 :I’ve just discovered an issue which shows itself when you click on a tab other than the first tab and then click edit on a grid item. When you do this after postback the tab index has been reset back to 0, if you click the tab you had previously selected the row you clicked edit on is in edit mode.
This is just to let you know I know about this BUG and am working on it.
I’ll be back soon with an update smile_teeth
UPDATED 2008/09/26 : Added tabContainer.AutoPostBack = true; to fix issue with tabs smile_teeth 
UPDATED 2008/09/27 : A better fix is to add tabContainer.EnableViewState = true; to fix issue with tabs smile_teeth 

Monday, 15 September 2008

Dynamic Data: Part 3-FileUpload FieldTemplates

  1. Part 1 - FileImage_Edit FieldTemplate.
  2. Part 2 - FileImage_Edit FieldTemplate.
  3. Part 3 - FileUpload FiledTemplate.

FileUpload and FileUpload_Edit FiledTemplates

I thought this would complement the DBImage and FileImage FieldTemplates and so I thought what would you want to be able to do:

  • Upload a file to a specified folder.
  • Download the said file once uploaded.
  • Display an image for the file.
  • Control the download capability via attributes and user Roles.
  • Handle errors such as wrong file type or when file is missing from upload folder.

The FileUpload Attributes

In this example I’m creating on attribute to hold all the parameters to do with FileUpload.

[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public sealed class FileUploadAttribute : Attribute
{
    /// <summary>
    /// where to save files
    /// </summary>
    public String FileUrl { get; set; }

    /// <summary>
    /// File tyoe to allow upload
    /// </summary>
    public String[] FileTypes { get; set; }

    /// <summary>
    /// image type to use for displaying file icon
    /// </summary>
    public String DisplayImageType { get; set; }

    /// <summary>
    /// where to find file type icons
    /// </summary>
    public String DisplayImageUrl { get; set; }

    /// <summary>
    /// If present user must be a member of one
    /// of the roles to be able to download file
    /// </summary>
    public String[] HyperlinkRoles { get; set; }

    /// <summary>
    /// Used to Disable Hyperlink (Enabled by default)
    /// </summary>
    public Boolean DisableHyperlink { get; set; }

    /// <summary>
    /// helper method to check for roles in this attribute
    /// the comparison is case insensitive
    /// </summary>
    /// <param name="role"></param>
    /// <returns></returns>
    public bool HasRole(String[] roles)
    {
        if (HyperlinkRoles.Count() > 0)
        {
            var hasRole = from hr in HyperlinkRoles.AsEnumerable()
                          join r in roles.AsEnumerable()
                          on hr.ToLower() equals r.ToLower()
                          select true;

            return hasRole.Count() > 0;
        }
        return false;
    }

}
Listing 1 – FileUploadAttribute

You will notice in the Listing 1 that all the properties are using c# 3.0’s new Automatic Properties feature less typing; just type prop and hit tab twice and there is you property ready to be filled in.

The second thing you will see is the HasRoles method on this attribute, which takes an array of roles and checks to see if HyperlinkRoles property has any matches. It does this by joining the two arrays together in a Linq to Object query and then selects true for each match in the join. I’m sure this is more readable that the traditional nested foreach loops, it’s certainly neater :D.

The FileUpload FieldTemplate

This FiledTemplate will show the filename and associated icon.

FileUpload with icon

Figure 1- File and associated Icon

<%@ Control
    Language="C#"
    AutoEventWireup="true"
    CodeFile="FileUpload.ascx.cs"
    Inherits="FileImage" %>

<asp:Image ID="Image1" runat="server" />&nbsp;
<asp:Label ID="Label1" runat="server" Text="<%# FieldValueString %>"></asp:Label>
<asp:HyperLink ID="HyperLink1" runat="server"></asp:HyperLink>&nbsp;
<asp:CustomValidator
    ID="CustomValidator1"
    runat="server"
    ErrorMessage="">
</asp:CustomValidator>

Listing 2 – FileUpload.ascx file

As you can see from Listing 1 there are Image, Label and Hyperlink controls on the page. The Label and Hyperlink are mutually exclusive if the conditions are right then a Hyperlink will show so that the file can be downloaded else just a Label will show with the filename.

using System;
using System.IO;
using System.Linq;
using System.Web.DynamicData;
using System.Web.Security;
using System.Web.UI;
using Microsoft.Web.DynamicData;

public partial class FileImage : FieldTemplateUserControl
{

    public override Control DataControl
    {
        get
        {
            return Label1;
        }
    }

    protected override void OnDataBinding(EventArgs e)
    {
        base.OnDataBinding(e);

        //check if field has a value
        if (FieldValue == null)
            return;

        // get the file extension
        String extension = FieldValueString.Substring(
            FieldValueString.LastIndexOf(".") + 1,
            FieldValueString.Length - (FieldValueString.LastIndexOf(".") + 1));

        // get attributes
        var fileUploadAttributes = MetadataAttributes.OfType<FileUploadAttribute>().FirstOrDefault();
        String fileUrl = fileUploadAttributes.FileUrl;
        String displayImageUrl = fileUploadAttributes.DisplayImageUrl;
        String displayImageType = fileUploadAttributes.DisplayImageType;


        // check the file exists else throw validation error
        String filePath;
        if (fileUploadAttributes != null)
            filePath = String.Format(fileUrl, FieldValueString);
        else
            // if attribute not set use default
            filePath = String.Format("~/files/{0}", FieldValueString);

        // show the relavent control depending on metadata
        if (fileUploadAttributes.HyperlinkRoles.Length > 0)
        {
            // if there are roles then check: 
            // if user is in one of the roles supplied
            // or if the hyperlinks are disabled 
            // or if the file does not exist
            // then hide the link
            if (!fileUploadAttributes.HasRole(Roles.GetRolesForUser())  fileUploadAttributes.DisableHyperlink  !File.Exists(Server.MapPath(filePath)))
            {
                Label1.Text = FieldValueString;
                HyperLink1.Visible = false;
            }
            else
            {
                Label1.Visible = false;
                HyperLink1.Text = FieldValueString;
                HyperLink1.NavigateUrl = filePath;
            }
        }
        else
        {
            // if either hyperlinks are disabled or the
            // file does not exist then hide the link
            if (fileUploadAttributes.DisableHyperlink  !File.Exists(Server.MapPath(filePath)))
            {
                Label1.Text = FieldValueString;
                HyperLink1.Visible = false;
            }
            else
            {
                Label1.Visible = false;
                HyperLink1.Text = FieldValueString;
                HyperLink1.NavigateUrl = filePath;
            }
        }

        // check file exists on file system
        if (!File.Exists(Server.MapPath(filePath)))
        {
            CustomValidator1.ErrorMessage = String.Format("{0} does not exist", FieldValueString);
            CustomValidator1.IsValid = false;
        }

        // show the icon
        if (!String.IsNullOrEmpty(extension))
        {
            // set the file type image
            if (!String.IsNullOrEmpty(displayImageUrl))
            {
                Image1.ImageUrl = String.Format(displayImageUrl, extension + "." + displayImageType);
            }
            else
            {
                // if attribute not set the use default
                Image1.ImageUrl = String.Format("~/images/{0}", extension + "." + displayImageType);
            }

            Image1.AlternateText = extension + " file";

            // if you apply dimentions from DD Futures
            var imageFormat = MetadataAttributes.OfType<ImageFormatAttribute>().FirstOrDefault();
            if (imageFormat != null)
            {
                // if either of the dims is 0 don't set it
                // this will mean that the aspect will remain locked
                if (imageFormat.DisplayWidth != 0)
                    Image1.Width = imageFormat.DisplayWidth;
                if (imageFormat.DisplayHeight != 0)
                    Image1.Height = imageFormat.DisplayHeight;
            }
        }
        else
        {
            // if file has no extension then hide image
            Image1.Visible = false;
        }
    }
}

Listing 3 – FileUpload.ascx.cs file

In Listing 3 you can see that everything goes on in the OnDataBinding event handler.

The FileUpload_Edit FieldTemplate

<%@ Control
    Language="C#"
    AutoEventWireup="true"
    CodeFile="FileUpload_Edit.ascx.cs"
    Inherits="FileImage_Edit" %>
   
<asp:PlaceHolder ID="PlaceHolder1" runat="server" Visible="false">
    <asp:Image ID="Image1" runat="server" />&nbsp;
    <asp:Label ID="Label1" runat="server" Text="<%# FieldValueString %>"></asp:Label>
    <asp:HyperLink ID="HyperLink1" runat="server"></asp:HyperLink>&nbsp;
</asp:PlaceHolder>
<asp:FileUpload ID="FileUpload1" runat="server" />&nbsp;
<asp:CustomValidator
    ID="CustomValidator1"
    runat="server"
    ErrorMessage="">
</asp:CustomValidator>

Listing 4 – FileUpload_Edit.ascx file

In Listing 4 the PlaceHolder control is used to hide the Image, Label and Hyperlink when in insert mode or when there is no value to be shown.

using System;
using System.Collections.Specialized;
using System.IO;
using System.Linq;
using System.Web.DynamicData;
using System.Web.Security;
using System.Web.UI;
using Microsoft.Web.DynamicData;

public partial class FileImage_Edit : FieldTemplateUserControl
{
    public override Control DataControl
    {
        get
        {
            return Label1;
        }
    }

    protected override void OnDataBinding(EventArgs e)
    {
        base.OnDataBinding(e);

        //check if field has a value
        if (FieldValue == null)
            return;

        // when there is already a value in the FieldValue
        // then show the icon and label/hyperlink
        PlaceHolder1.Visible = true;

        // get the file extension
        String extension = FieldValueString.Substring(
            FieldValueString.LastIndexOf(".") + 1,
            FieldValueString.Length - (FieldValueString.LastIndexOf(".") + 1));

        // get attributes
        var fileUploadAttributes = MetadataAttributes.OfType<FileUploadAttribute>().FirstOrDefault();
        String fileUrl = fileUploadAttributes.FileUrl;
        String displayImageUrl = fileUploadAttributes.DisplayImageUrl;
        String displayImageType = fileUploadAttributes.DisplayImageType;
        String filePath;

        // check the file exists else throw validation error
        if (fileUploadAttributes != null)
            filePath = String.Format(fileUrl, FieldValueString);
        else
            // if attribute not set use default
            filePath = String.Format("~/files/{0}", FieldValueString);

        // show the relavent control depending on metadata
        if (fileUploadAttributes.HyperlinkRoles.Length > 0)
        {
            // if there are roles then check: 
            // if user is in one of the roles supplied
            // or if the hyperlinks are disabled 
            // or if the file does not exist
            // then hide the link
            if (!fileUploadAttributes.HasRole(Roles.GetRolesForUser())  fileUploadAttributes.DisableHyperlink  !File.Exists(Server.MapPath(filePath)))
            {
                Label1.Text = FieldValueString;
                HyperLink1.Visible = false;
            }
            else
            {
                Label1.Visible = false;
                HyperLink1.Text = FieldValueString;
                HyperLink1.NavigateUrl = filePath;
            }
        }
        else
        {
            // if either hyperlinks are disabled or the
            // file does not exist then hide the link
            if (fileUploadAttributes.DisableHyperlink  !File.Exists(Server.MapPath(filePath)))
            {
                Label1.Text = FieldValueString;
                HyperLink1.Visible = false;
            }
            else
            {
                Label1.Visible = false;
                HyperLink1.Text = FieldValueString;
                HyperLink1.NavigateUrl = filePath;
            }
        }

        // check file exists on file system
        if (!File.Exists(Server.MapPath(filePath)))
        {
            CustomValidator1.ErrorMessage = String.Format("{0} does not exist", FieldValueString);
            CustomValidator1.IsValid = false;
        }

        // show the icon
        if (!String.IsNullOrEmpty(extension))
        {
            // set the file type image
            if (!String.IsNullOrEmpty(displayImageUrl))
            {
                Image1.ImageUrl = String.Format(displayImageUrl, extension + "." + displayImageType);
            }
            else
            {
                // if attribute not set the use default
                Image1.ImageUrl = String.Format("~/images/{0}", extension + "." + displayImageType);
            }

            Image1.AlternateText = extension + " file";

            // if you apply dimentions from DD Futures
            var imageFormat = MetadataAttributes.OfType<ImageFormatAttribute>().FirstOrDefault();
            if (imageFormat != null)
            {
                // if either of the dims is 0 don't set it
                // this will mean that the aspect will remain locked
                if (imageFormat.DisplayWidth != 0)
                    Image1.Width = imageFormat.DisplayWidth;
                if (imageFormat.DisplayHeight != 0)
                    Image1.Height = imageFormat.DisplayHeight;
            }
        }
        else
        {
            // if file has no extension then hide image
            Image1.Visible = false;
        }
    }

    protected override void ExtractValues(IOrderedDictionary dictionary)
    {
        // get attributes
        var fileUploadAttributes = MetadataAttributes.OfType<FileUploadAttribute>().FirstOrDefault();

        String fileUrl;
        String[] extensions;
        if (fileUploadAttributes != null)
        {
            fileUrl = fileUploadAttributes.FileUrl;
            extensions = fileUploadAttributes.FileTypes;

            if (FileUpload1.HasFile)
            {
                // get the files folder
                String filesDir = fileUrl.Substring(0, fileUrl.LastIndexOf("/") + 1);

                // resolve full path c:\... etc
                String path = Server.MapPath(filesDir);

                // get files extension without the dot
                String fileExtension = FileUpload1.FileName.Substring(
                    FileUpload1.FileName.LastIndexOf(".") + 1).ToLower();

                // check file has an allowed file extension
                if (extensions.Contains(fileExtension))
                {
                    // try to upload the file showing error if it fails
                    try
                    {
                        FileUpload1.PostedFile.SaveAs(path + "\\" + FileUpload1.FileName);
                        Image1.ImageUrl = String.Format(fileUploadAttributes.DisplayImageUrl, fileExtension + ".png");
                        Image1.AlternateText = fileExtension + " file";
                        dictionary[Column.Name] = FileUpload1.FileName;
                    }
                    catch (Exception ex)
                    {
                        // display error
                        CustomValidator1.IsValid = false;
                        CustomValidator1.ErrorMessage = ex.Message;
                    }
                }
                else
                {
                    CustomValidator1.IsValid = false;
                    CustomValidator1.ErrorMessage = String.Format("{0} is not a valid file to upload", FieldValueString);
                }
            }
        }
    }
}
Listing 5 - FileUpload_Edit.ascx.cs file

In Listing 5 the OnDataBinding event handler is pretty much the same as the FileUpload.ascs.cs file. Here its the ExtractValues method that does the work of uploading and displaying errors, i.e. if the file type of the file to be uploaded does not match a file type specified in the metadata or there is an error during the upload.

Helper Class FileUploadHelper

public static class FileUploadHelper
{
    /// <summary>
    /// If the given table contains a column that has a UI Hint with the value "DbImage", finds the ScriptManager
    /// for the current page and disables partial rendering
    /// </summary>
    /// <param name="page"></param>
    /// <param name="table"></param>
    public static void DisablePartialRenderingForUpload(Page page, MetaTable table)
    {
        foreach (var column in table.Columns)
        {
            // TODO this depends on the name of the field template, need to fix
            if (String.Equals(
                column.UIHint, "DBImage", StringComparison.OrdinalIgnoreCase)
                String.Equals(column.UIHint, "FileImage", StringComparison.OrdinalIgnoreCase)
                String.Equals(column.UIHint, "FileUpload", StringComparison.OrdinalIgnoreCase))
            {
                var sm = ScriptManager.GetCurrent(page);
                if (sm != null)
                {
                    sm.EnablePartialRendering = false;
                }
                break;
            }
        }
    }
}

Listing 6 - FileUploadHelper

This is just a modified version of the Dynamic Data Futures DisablePartialRenderingForUpload method the only difference is that I’ve added support for both my file upload capable FieldTemplates FileImage and FileUpload.

Finally Some Sample Metadata

[MetadataType(typeof(FileImageTestMD))]
public partial class FileImageTest : INotifyPropertyChanging, INotifyPropertyChanged
{
    public class FileImageTestMD
    {
        public object Id { get; set; }
        public object Description { get; set; }
        [UIHint("FileUpload")]
        [FileUpload(
            FileUrl = "~/files/{0}",
            FileTypes = new String[] { "pdf", "xls", "doc", "xps" },
            DisplayImageType = "png",
            DisableHyperlink = false,
            HyperlinkRoles=new String[] { "Admin", "Accounts" },
            DisplayImageUrl = "~/images/{0}")]
        [ImageFormat(22, 0)]
        public object filePath { get; set; }
    }
}

Listing 7 – sample metadata

The FileUpload Project

Note: Please note that the ASPNETDB.MDF supplied in this website is SQL 2008 Express and will not work with SQL 2005 and earlier, you will need to set your own up. Or you can just strip out the login capability from the site.master and web.config.

Enjoy smile_teeth