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

Tuesday, 9 September 2008

Dynamic Data Futures – Part 3 – AnyColumnAutocomplete Filter

  1. Part 1 - Getting Dynamic Data Futures filters working in a File Based Website.
  2. Part 2 - Create the AnyColumn filter from the Dynamic Data Futures Integer filter.
  3. Part 3 – Creating the AnyColumnAutocomplete filter.

Creating the AnyColumnAutocomplete filter

This version of the Autocomplete filter was adapted in response to this article on the Dynamic Data forum by levalencia.

The issue is that the Autocomplete filter and AutocompleteFilter web service is designed to work with Foreign Key columns, I thought this would be like the previous article in this series and be quite simple, instead it turned out to be a bit more complicated smile_thinking.

Making the AnyColumnAutocomplete FieldTemplate.

The issue initially was that the Autocomplete filter was assuming MetaForeignKeyColumn fkColumn and then referencing the ParentTable from it this meant that we needed a new FieldTemplate no just some tweaking.

protected void Page_Init(object sender, EventArgs e)
{
    var fkColumn = Column as MetaForeignKeyColumn;

    //// dynamically build the context key so the web service knows which table we're talking about
    autoComplete1.ContextKey = AutocompleteFilterService.GetContextKey(fkColumn.ParentTable);
    ...
}

Listing 1 – Autocomplete filter Page_Init

And then in the AutocompleteFilter web service has no reference to the column this meant that either we create a new web service of modify the current I decided to modify it as there were only a few new methods needed to handle the non foreign key columns.

So without any further ado here’s the changes to the FieldTemplate to make the AnyColumnAutocomplete filter.

We remove the line:

var fkColumn = Column as MetaForeignKeyColumn;

and replace all referenced to fkColumn.PartentTable with Column.Table or Column

e.g. change

MetaTable parentTable = fkColumn.ParentTable;
to
MetaTable parentTable = Column.Table;

And change the following line

autoComplete1.ContextKey = AutocompleteFilterService.GetContextKey(fkColumn.ParentTable);

to

autoComplete1.ContextKey = AutocompleteFilterService.GetContextKey(Column);

And that's the changes to make the AnyColumnAutocomplete filter.

Now the changes to the AutocompleteFilter web service

The first change to the AutocompleteFilter web service was to add an overloaded GetContextKey that takes a MetaTable as remember this line from above:

autoComplete1.ContextKey = AutocompleteFilterService.GetContextKey(Column);

So the overloaded method takes a MetaColumn as it’s parameter not MetaTable:

public static string GetContextKey(MetaColumn column)
{
    return String.Format("{0}#{1}#{2}", column.Table.DataContextType.FullName, column.Table.Name, column.Name);
}

Listing 2 – GetContextKey method

Note: That from the column it gets similar properties as the original but also get the Column.Name

Next the changes to the GetCompletionList which returns the list of results.

[System.Web.Services.WebMethod]
[System.Web.Script.Services.ScriptMethod]
public string[] GetCompletionList(string prefixText, int count, string contextKey) {
    MetaTable table = GetTable(contextKey);

    IQueryable queryable = BuildFilterQuery(table, prefixText, count);

    return queryable.Cast<object>().Select(row => CreateAutoCompleteItem(table, row)).ToArray();
}

Listing 3 – GetCompletionList original method

[System.Web.Services.WebMethod]
[System.Web.Script.Services.ScriptMethod]
public string[] GetCompletionList(string prefixText, int count, string contextKey)
{
    MetaTable table;
    String[] param = contextKey.Split('#');

    if (param.Length > 2)
    {
        table = GetTable(contextKey);
        var list = BuildFilterQuery(table, prefixText, count, param[2]);

        return list;
    }
    else
    {
        table = GetTable(contextKey);
        IQueryable queryable = BuildFilterQuery(table, prefixText, count);

        return queryable.Cast<object>().Select(row => CreateAutoCompleteItem(table, row)).ToArray();
    }
}

Listing 4 – GetCompletionList modified method

In the modified version we split the contectKey into param local variable and check the number of entries to determine whether it’s dealing with ForeignKey column or any other column.

Also note the minor change to the GetTable method where the number of parameters is check and added a check for 2 OR 3 paramers.

Debug.Assert(param.Length == 2  param.Length == 3, String.Format("The context key '{0}' is invalid", contextKey));

After this I have added another overloaded method for the BuildFilterQuery which also takes the column name.

private static String[] BuildFilterQuery(
    MetaTable table,
    string prefixText,
    int maxCount,
    String columnName)
{
    var column = table.GetColumn(columnName);

    // query = {Table(Customer)}
    var query = table.GetQuery();

    // row
    // entityParam = {row}
    var entityParam = Expression.Parameter(column.Table.EntityType, "row");

    // row.DisplayName
    //var property = Expression.Property(entityParam, columnName);
    //property = {row.City}
    var property = Expression.Property(entityParam, column.EntityTypeProperty);

    // row => row.Property
    // columnLambda = {row => row.City}
    var columnLambda = Expression.Lambda(property, entityParam);

    // "prefix"
    // constant = {"Lo"}
    var constant = Expression.Constant(prefixText);

    // row.DisplayName.StartsWith("prefix")
    // startsWithCall = {row.City.StartsWith("Lo")}
    var startsWithCall = Expression.Call(
        property,
        typeof(string).GetMethod("StartsWith", new Type[] { typeof(string) }),
        constant);

    // row => row.DisplayName.StartsWith("prefix")
    // whereLambda = {row => row.City.StartsWith("Lo")}
    var whereLambda = Expression.Lambda(startsWithCall, entityParam);

    // Customers.Where(row => row.DisplayName.StartsWith("prefix"))
    // whereCall = {Table(Customer).Where(row => row.City.StartsWith("Lo"))}
    var whereCall = Expression.Call(
        typeof(Queryable),
        "Where",
        new Type[] { table.EntityType },
        query.Expression,
        whereLambda);

    // query.Select(row => row.Property)
    // selectCall = {Table(Customer).Where(row => row.City.StartsWith("Lo")).Select(row => row.City)}
    var selectCall = Expression.Call(
        typeof(Queryable),
        "Select",
        new Type[] { query.ElementType, columnLambda.Body.Type },
        whereCall,
        columnLambda);

    // query.Select(row => row.Property).Distinct
    // distinctCall = {Table(Customer).Where(row => row.City.StartsWith("Lo")).Select(row => row.City).Distinct()}
    var distinctCall = Expression.Call(
        typeof(Queryable),
        "Distinct",
        new Type[] { column.EntityTypeProperty.PropertyType },
        selectCall);

    // Customers.Where(row => row.DisplayName.StartsWith("prefix")).Take(20)
    // distinctCall = {Table(Customer).Where(row => row.City.StartsWith("Lo")).Select(row => row.City).Distinct().Take(20)}
    var takeCall = Expression.Call(
        typeof(Queryable),
        "Take",
        new Type[] { typeof(String) },
        distinctCall,
        Expression.Constant(maxCount));

    var result = query.Provider.CreateQuery(takeCall);
    List<String> list = new List<string>();
    foreach (var item in result)
    {
        list.Add(AutoCompleteExtender.CreateAutoCompleteItem(item.ToString(), item.ToString()));
    }

    return list.ToArray();
    //return query.Provider.CreateQuery(distinctCall);
}

Listing 5 – overloaded BuildFilterQuery method

Listing 5 essentially does what Sample 1 does; which get the first two cities from the Customer table where they begin with “Br” and makes sure they are distinct.

var DC = new NWDataContext();
var q = (from c in DC.Customers
        where c.City.StartsWith("Br")
        select c.City).Distinct().Take(2);

Sample 1 – getting the first 2 customer whose City starts with “Br”

And then returns an array that is ready to pass back to the AjaxToolkit Autocomplete control.

And here’s the Metadata classes

[MetadataType(typeof(Customer_MD))]
public partial class Customer
{
    public class Customer_MD
    {
        [Filter(FilterControl = "AnyColumnAutocomplete")]
        public object City { get; set; }
    }
}

Listing 6 – metadata classes

Website project file (not including Dynamic Data Futures project)

Hope this helps smile_teeth

Saturday, 6 September 2008

Dynamic Data and Field Templates - An Advanced FieldTemplate with a GridView/DetailsView Project ***UPDATED***

  1. The Anatomy of a FieldTemplate.
  2. Your First FieldTemplate.
  3. An Advanced FieldTemplate.
  4. A Second Advanced FieldTemplate.
  5. An Advanced FieldTemplate with a GridView.
  6. An Advanced FieldTemplate with a DetailsView.
  7. An Advanced FieldTemplate with a GridView/DetailsView Project.

Here is the file based website zipped up no Northwind database you’ll need to provide that yourself as I’m on SQL Server 2008 now :D

UPDATED: I’ve updated the GridView/DetailsView Project it now has ParentDetails and the ChildrenGrid FieldTemplate’s
ParentDetails
no longet support Insert but does support Update as before. Before you try it test it with the Northwind database and this project.

Changes to the project both FieldTemplate now support an attribute that sets which columns to display and now share the IAutoFieldGenerator.

Hope this helps [:D]

Friday, 5 September 2008

Dynamic Data Futures – Part 2 - AnyColumn Filter

  1. Part 1 - Getting Dynamic Data Futures filters working in a File Based Website.
  2. Part 2 - Create the AnyColumn filter from the Dynamic Data Futures Integer filter.
  3. Part 3 – Creating the AnyColumnAutocomplete filter.

Create the AnyColumn filter from the Dynamic Data Futures Integer filter

No need to create a new filter once you've got the filters from Dynamic Data Future working in your site all you need to do is add the filter attribute Integer to the property you want filtered see below:

[MetadataType(typeof(Customer_MD))]
public partial class Customer
{
    public class Customer_MD
    {
        [Filter(FilterControl = "Integer")]
        public object ContactName { get; set; }
    }
}

I was thinking I had to some stuff to make the Integer filter to work with other column type and you don't when I look back at my code smile_embaressed

Hope this helps smile_teeth

Thursday, 4 September 2008

Dynamic Data Compound Column *** UPDATED 2008/11/08 ***

I answered this Re: Display multiple fields in custom FieldTemplateUserControl question in the Dynamic Data forum whilst away and didn’t have the time to do a full write up of it so here it is.

What the question was:

Adult wrote “I want to display multiple fields in one custom FieldTemplateUserControl. For example i have property X and Y. Currently they show on separate row when i edit or insert.
Do i need to create new property to return anonymous class with just this 2 property and in custom control,read values.What about inserting? Anyone done it?”

So here’s the partial class I added to deal with the new property Coordinate:

using System.ComponentModel;
using System.ComponentModel.DataAnnotations;

[MetadataType(typeof(TestPointMD))]
public partial class TestPoint
{
    [ScaffoldColumn(true)]
    public Point Coordinate
    {
        get
        {
            return new Point(this.X, this.Y);
        }
        set
        {
            this.X = value.X;
            this.Y = value.Y;
        }
    }

    public class TestPointMD
    {
        public object Id { get; set; }
        public object Name { get; set; }
        [ScaffoldColumn(false)]
        public object X { get; set; }
        [ScaffoldColumn(false)]
        public object Y { get; set; }
    }
}

[Serializable]
public class Point
{
    public Point(int x, int y)
    {
        X = x;
        Y = y;
    }

    public int X { get; set; }
    public int Y { get; set; }

    public String ToString()
    {
        return X + ", " + Y;
    }
}

Listing 1 – Partial methods for my model

Listing 1 consists of three parts:

  1. The partial class with the Coordinate property added.
  2. The Metadata class setting the ScaffoldColumn attribute to false for the individual column we don’t want to show.
  3. And last the Point class that we are using in the Coordinate property Note the Serializable attribute which is needed for the FieldTemplate to be able to return values.
Note: The lack of UIHint in the metadata or partial class as we are going to create a FieldTemplate called Point and Point_Edit.

The next part are the FieldTemplates Point and Point_Edit.

<%@ Control 
    Language="C#" 
    CodeFile="Point.ascx.cs" 
    Inherits="PointField" %>

<asp:Literal 
    runat="server" 
    ID="Literal1" />

Listing 2 – Point.ascx

using System;
using System.Web.UI;

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

    protected override void OnDataBinding(EventArgs e)
    {
        var p = FieldValue as Point;
        Literal1.Text = p.ToString();
        base.OnDataBinding(e);
    }
}

Listing 3 – Point.ascx.cs

Listings 2 and 3 are based on the Text.ascx FieldTemplate the main difference is that the value of the literal is set via the Point class’s ToString() method.

<%@ Control 
    Language="C#" 
    CodeFile="Point_Edit.ascx.cs" 
    Inherits="Point_EditField" %>

<asp:TextBox 
    ID="TextBoxX" 
    runat="server" 
    CssClass="droplist">
</asp:TextBox>
<asp:TextBox 
    ID="TextBoxY" 
    runat="server" 
    CssClass="droplist">
</asp:TextBox>

Listing 4 – Point_Edit.ascx

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

public partial class Point_EditField : System.Web.DynamicData.FieldTemplateUserControl
{
    protected void Page_Load(object sender, EventArgs e)
    {
        TextBoxX.MaxLength = Column.MaxLength;
        TextBoxY.MaxLength = Column.MaxLength;
        if (Column.MaxLength < 20)
        {
            TextBoxX.Columns = Column.MaxLength;
            TextBoxY.Columns = Column.MaxLength;
        }
        TextBoxX.ToolTip = Column.Description;
        TextBoxY.ToolTip = Column.Description;
    }

    protected override void OnDataBinding(EventArgs e)
    {
        var p = FieldValue as Point;
        if (p != null)
        {
            TextBoxX.Text = p.X.ToString();
            TextBoxY.Text = p.Y.ToString();
        }
        base.OnDataBinding(e);
    }

    protected override void ExtractValues(IOrderedDictionary dictionary)
    {
        int x;
        int y;
        int.TryParse(TextBoxX.Text, out x);
        int.TryParse(TextBoxY.Text, out y);
        var p = new Point(x, y);
        dictionary[Column.Name] = p;
    }

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

Listing 5 – Point_Edit.ascx.cs

Here in Listings 4 and 5 we create again from the Text_Edit FieldTemplate our Point_Edit FieldTemplate everything in this apart from the ExtractValues method is the same as the FieldTemplate it is based on but just doubled up on the two TextBoxes we have used. In the ExtractValues method we create and populate a new instance of the Point class set it’s X and Y properties and then put it in the dictionary passed into the method.

Point FieldTemplate in action

Figure 1 – Point FieldTemplate in action

This seems to work really well for this type of compound property. At a later date I would like to try this with Point being a User Defended Type in SQL Server 2005/2008, this would get rid of the need for the compound property and would mean only a new FieldTemplate was required.

UPDATED: Issue re: this post Grouping Columns into class to pass into FieldTemplate sporadic problem on the ASP.Net Dynamic Data Forum

The combined property does get assigned, but when the update check is on, it ends up being overwritten by the original values of the individual fields.  Basically, pretty random results.  I think it’s best to avoid using custom properties, especially since they’re not supported in EF.

See Scott’s sample http://www.codeplex.com/aspnet/Release/ProjectReleases.aspx?ReleaseId=14473.
UPDATED: See Rick’s post on on a simple two column display here Improving the FK field display: Showing two fields in Foreign Key columns with EF not quite a clever at this can be but if you want to display two column compunded together sutch as FirstName and LastName in the FK this this ones for you.

Some Pictures and Musings from my Vacation

Just got back from holiday/vacation in the south of France (Near St.Tropez, La Mole to be exact) and I thought I’d bore you with some of my panoramic pictures and let you to drool over them (and me too I wish I was back there).

We took a three day drive there and back and had two week in the sun and we hardly ever saw a cloud in the sky. It was the best holiday ever for sun, but the traffic was terrible you had to get up early if you wanted to get to a beach or to one of the local towns i.e. St.-Tropez, Sainte-Maxime. The camp site was nice and clean although the slides (according to my wife and my daughter) weren’t that brilliant (I went on once and got stuck a couple of time on the way down, I thought it may be my weight but my daughter Sarah said she did too and she is only 7½ stone)

Sadly we’re back in not so sunny England in fact it’s rained most days since we got back :(

Camp-Hôtel Pachacaïd near La Môle

Camp-Hôtel Pachacaïd near La Môle

Camp-Hôtel Pachacaïd view from top of the camp site

Camp-Hôtel Pachacaïd view from top of the camp site

Camp-Hôtel Pachacaïd Pool

Camp-Hôtel Pachacaïd Pool

Port De Grimaud plage

Port De Grimaud plage

DSC00783

Gassin looking out over St.Tropez Golfe

Grimaud Chatau 6

The view from Grimaud Châteaux

All pictures were taken with my Sony Ericsson K800i mobile phone and then turned into panoramic shots with MAGIX PanoramaStudio.

I’ve not worked out how to put the panoramic viewer into the blog yet :D

I’d like to thank my Mum for giving us the funds and the loan of her car to go away, without which we would have been stuck.

Anyway again I’ve got to get a job now as the funds are running out from my redundancy, it’s that or sign on for benefits. Still you’d have thought there would be a job out there for someone like me; who likes to show other people how to do things.

So if there are any potential employers here’s my e-mail address: steve@notaclue.net drop me an e-mail smile_teeth

For my next post I’m going to cover creating a Time Control to allow users to enter a time value like they can in some Windows Forms applications with spinners or by text and having automatic validation e.g. you will not be able to enter an invalid time like 24:00:60 PM it would revert to 12:00:59 PM.

I’ll be back soon :D

Wednesday, 3 September 2008

Dynamic Data and Field Templates - An Advanced FieldTemplate with a DetailsView ***UPDATED 2008/09/24***

  1. The Anatomy of a FieldTemplate.
  2. Your First FieldTemplate.
  3. An Advanced FieldTemplate.
  4. A Second Advanced FieldTemplate.
  5. An Advanced FieldTemplate with a GridView.
  6. An Advanced FieldTemplate with a DetailsView.
  7. An Advanced FieldTemplate with a GridView/DetailsView Project.

In this addition the FieldTemplates series I was asked to produce one that looked back up the relationship with the parent table to get some or all of the properties.

The basis for this FieldTemplate is the previous GridView_Edit FieldTemplate. This time I’m just going to post the files and then discuss the alterations, so here goes:

<%@ Control 
    Language="C#" 
    CodeFile="DetailsView_Edit.ascx.cs" 
    Inherits="DetailsView_EditField" %>

<asp:ValidationSummary ID="ValidationSummary1" 
    D="ValidationSummary1" 
    runat="server" 
    EnableClientScript="true"
    HeaderText="List of validation errors" />
    
<asp:DynamicValidator 
    runat="server" 
    ID="DetailsViewValidator" 
    ControlToValidate="DetailsView1"
    Display="None" />
    
<asp:DetailsView 
    ID="DetailsView1" 
    runat="server" 
    DataSourceID="DetailsDataSource"
    CssClass="detailstable"
    AutoGenerateDeleteButton="true"
    AutoGenerateEditButton="true"
    AutoGenerateInsertButton="true"
    FieldHeaderStyle-CssClass="bold">
    
</asp:DetailsView>

<asp:LinqDataSource 
    ID="DetailsDataSource" 
    runat="server" 
    EnableDelete="true">
</asp:LinqDataSource>

Listing 1 - DetailsView_Edit.ascx

using System;
using System.Data;
using System.Configuration;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
using System.Xml.Linq;
using System.Web.DynamicData;

public partial class ParentDetails_EditField : FieldTemplateUserControl
{
    protected MetaTable parentTable;
    protected MetaTable childTable;

    public Boolean EnableDelete { get; set; }
    //public Boolean EnableInsert { get; set; }
    public Boolean EnableUpdate { get; set; }

    public String[] DisplayColumns { get; set; }

    public ParentDetails_EditField()
    {
        // set default values
        EnableDelete = true;
        EnableUpdate = true;
        //EnableInsert = false;
    }

    protected void Page_Init(object sender, EventArgs e)
    {
        var attribute = Column.Attributes.OfType<ShowColumnsAttribute>().SingleOrDefault();

        if (attribute != null)
        {
            if (!attribute.EnableDelete)
                EnableDelete = false;
            if (!attribute.EnableUpdate)
                EnableUpdate = false;
            //if (!attribute.EnableInsert)
            //    EnableInsert = false;
            if (attribute.DisplayColumns.Length > 0)
                DisplayColumns = attribute.DisplayColumns;
        }

        var metaForeignKeyColumn = Column as MetaForeignKeyColumn;

        if (metaForeignKeyColumn != null)
        {
            childTable = metaForeignKeyColumn.Table;

            // setup data source
            DetailsDataSource.ContextTypeName = metaForeignKeyColumn.ParentTable.DataContextType.Name;
            DetailsDataSource.TableName = metaForeignKeyColumn.ParentTable.Name;

            // enable update, delete and insert
            DetailsDataSource.EnableDelete = EnableDelete;
            DetailsDataSource.EnableInsert = false; // EnableInsert;
            DetailsDataSource.EnableUpdate = EnableUpdate;
            DetailsView1.AutoGenerateDeleteButton = EnableDelete;
            DetailsView1.AutoGenerateInsertButton = false; // EnableInsert;
            DetailsView1.AutoGenerateEditButton = EnableUpdate;

            // get an instance of the MetaTable
            parentTable = DetailsDataSource.GetTable();

            // Generate the columns as we can't rely on 
            // DynamicDataManager to do it for us.
            DetailsView1.RowsGenerator = new FieldTemplateRowGenerator(parentTable, DisplayColumns);

            // setup the GridView's DataKeys
            String[] keys = new String[metaForeignKeyColumn.ParentTable.PrimaryKeyColumns.Count];
            int i = 0;
            foreach (var keyColumn in metaForeignKeyColumn.ParentTable.PrimaryKeyColumns)
            {
                keys[i] = keyColumn.Name;
                i++;
            }
            DetailsView1.DataKeyNames = keys;

            // enable AutoGenerateWhereClause so that the WHERE 
            // clause is generated from the parameters collection
            DetailsDataSource.AutoGenerateWhereClause = true;

            // doing the work of this above because we can't
            // set the DynamicDataManager table or where values
            //DynamicDataManager1.RegisterControl(DetailsView1, false);
        }
        else
        {
            // throw an error if set on column other than MetaChildrenColumns
            throw new InvalidOperationException("The GridView FieldTemplate can only be used with MetaChildrenColumns");
        }
    }

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

        // get the fk column
        var metaForeignKeyColumn = Column as MetaForeignKeyColumn;

        // get the association attributes associated with MetaChildrenColumns
        var association = metaForeignKeyColumn.Attributes.
            OfType<System.Data.Linq.Mapping.AssociationAttribute>().FirstOrDefault();

        if (metaForeignKeyColumn != null && association != null)
        {
            // get keys ThisKey and OtherKey into dictionary
            var keys = new Dictionary<String, String>();
            var seperator = new char[] { ',' };
            var thisKeys = association.ThisKey.Split(seperator);
            var otherKeys = association.OtherKey.Split(seperator);
            for (int i = 0; i < thisKeys.Length; i++)
            {
                keys.Add(thisKeys[i], otherKeys[i]);
            }

            // setup the where clause 
            // support composite foreign keys
            foreach (String fkName in metaForeignKeyColumn.ForeignKeyNames)
            {
                // get the current FK column
                var fkColumn = metaForeignKeyColumn.Table.GetColumn(fkName);
                // get the current PK column
                var pkColumn = metaForeignKeyColumn.ParentTable.GetColumn(keys[fkName]);

                // setup parameter
                var param = new Parameter();
                param.Name = pkColumn.Name;
                param.Type = pkColumn.TypeCode;

                // get the value for this FK column
                param.DefaultValue = GetColumnValue(fkColumn).ToString();

                // add the where clause
                DetailsDataSource.WhereParameters.Add(param);
            }
        }
    }
}

Listing 2 - DetailsView_Edit.ascx.cs ***UPDATED 2008/09/24***

UPDATED 2008/09/24: The OnDataBinding event handler has been updated to handle multiple PK-FK relationships.

As you can see from examining the above file the main thing is the change of the GridView to DetailsView, however you will notice that part of the code has been remove from the Page_Init the OnDataBinding. This is because we are coming at this from the other end the value for the ForeignKey to link the DetailsView to the parent control is no longer in the Request.QueryString and so we have to extract it in the OnDataBinding event handler as access to column values is not valid untill OnDataBinding.

You will also I’ve added some properties to enable features of the DetailsView either declaratively or via attributes:

[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public class DetailsViewTemplateAttribute : Attribute
{
    public DetailsViewTemplateAttribute(params String[] displayColumns)
    {
        DisplayColumns = displayColumns;
    }

    public String[] DisplayColumns { get; set; }
    public Boolean EnableDelete { get; set; }
    public Boolean EnableInsert { get; set; }
    public Boolean EnableUpdate { get; set; }
}

Listing 3 - DetailsViewTemplateAttribute.cs

[MetadataType(typeof(OrderMD))]
public partial class Order
{
    public class OrderMD
    {
        [UIHint("DetailsView")]
        [DetailsViewTemplate
            (
                "Title",
                "FirstName",
                "LastName", 
                "Region",
                "Extension", 
                EnableDelete=false, 
                EnableUpdate=false, 
                EnableInsert=false
            )]
        public object Employee { get; set; }
    }
}

Listing 4 – Northwind Partials and Metadata

As you can see from Listing 3 and Listing 4 you are able to enable or disable Update, Delete or Insert on the DetailsView FieldTemplate and also specify which columns you want to appear in the particular instance.

public class DetailsViewRowGenerator : IAutoFieldGenerator
{
    protected MetaTable _table;
    protected String[] _displayColumns;

    public DetailsViewRowGenerator(MetaTable table, String[] displayColumns)
    {
        _table = table;
        _displayColumns = displayColumns;
    }

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

        foreach (var column in _table.Columns)
        {
            // carry on the loop at the next column  
            // if scaffold table is set to false or DenyRead
            if (!column.Scaffold)
                continue;

            if (_displayColumns != null && !_displayColumns.Contains(column.Name))
                continue;

            DynamicField f = new DynamicField();

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

Listing 5 – DetailsViewRowGenerator (tagged on to the end of the DetailsView_Edit.ascx.cs file)

And finally the DetailsViewRowGenerator wether to show some or all of the columns in the parent table, the most important line here is:

if (_displayColumns != null && !_displayColumns.Contains(column.Name))
    continue;

which test first to see of any columns have been specified and if so the check to see if the current column is not present in the list and then drops the column appropriately, otherwise the IAutoFieldGenerator implementation is pretty much the same as the GridView FieldTemplate.

See if working below:

DetailsView_Edit FieldTemplate at work

Figure 1 - DetailsView_Edit FieldTemplate at work