Monday, 4 July 2011

Custom Field Templates On NuGet

In this article I am going to build some custom field templates;

  1. File Upload field template
  2. Image Upload field template
  3. Image Select field template
  4. Date picker field template

CustomFieldTemplates

Figure 1 – Our proposed field templates

So first of all we need specify what we want each to do.

File Upload Field Template Requirements
  • Be able to upload and file type (PDF, ZIP, DOC, XLS etc.).
  • Restrict types to be uploaded.
  • access uploaded file via hyperlink (enable or disable this).
  • Store in folder in site.
  • Replace old file if new file replaces it during edit (you would need to maintain deleting items using some business logic as the field template is not called during the deletion process).
Image Upload Field Template Requirements
  • Be able to upload and image type (jpeg, gif, png, etc.).
  • Restrict which image types can be uploaded (i.e. restricts to jpeg or png etc.).
  • Store in folder in site.
  • Replace old file if new file replaces it during edit (you would need to maintain deleting items using some business logic as the field template is not called during the deletion process).
Image Select Field Template Requirements
  • Choose from a set of images .
  • Use images a folder in site.
Ajax Date picker
  • Select a date using a popup date picker from the Ajax Control toolkit.

At another time we will do one based on the jQuery UI date picker.

Building the Field Templates

We could just pass the information we want using the UIHint attribute and it’s control parameters collection but there is no design time intellisense, so we will build an attribute that all three field template can use. We will call it the Upload attribute it will need to store the following information;

  • Folder to store images
  • Folder to store file icons
  • Size to display Icon or Image (Height and Width)
  • Whether to display an hyperlink or not.
  • Acceptable file type for image and file upload.
  • Image extension for icons.
The Upload Attribute
/// <summary>
/// Upload attribute defines values for the upload
/// field templates
/// </summary>
/// <remarks></remarks>
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)]
public class UploadAttribute : Attribute
{
    #region Properties
    /// <summary>
    /// Gets or sets the height to display the image, 
    /// if only one of the two dimensions are specified
    /// then the aspect ration will be retained.
    /// </summary>
    /// <value>The height.</value>
    /// <remarks></remarks>
    public int Height { get; set; }

    /// <summary>
    /// Gets or sets the width to display the image, 
    /// if only one of the two dimensions are specified
    /// then the aspect ration will be retained.
    /// </summary>
    /// <value>The width.</value>
    /// <remarks></remarks>
    public int Width { get; set; }

    /// <summary>
    /// Gets or sets the uploads folder.
    /// </summary>
    /// <value>The uploads folder.</value>
    /// <remarks></remarks>
    public String UploadFolder { get; set; }

    /// <summary>
    /// Gets or sets the icons folder.
    /// </summary>
    /// <value>The icons folder.</value>
    /// <remarks></remarks>
    public String ImagesFolder { get; set; }

    /// <summary>
    /// Gets or sets a value indicating whether [show hyperlink].
    /// </summary>
    /// <value><c>true</c> if [show hyperlink]; otherwise, <c>false</c>.</value>
    /// <remarks></remarks>
    public Boolean ShowHyperlink { get; set; }

    /// <summary>
    /// Gets or sets the file types.
    /// </summary>
    /// <value>The file types.</value>
    /// <remarks></remarks>
    public String[] FileTypes { get; set; }

    /// <summary>
    /// Gets or sets the image extension.
    /// </summary>
    /// <value>The image extension.</value>
    /// <remarks></remarks>
    public String ImageExtension { get; set; }
    #endregion

    /// <summary>
    /// Initializes a new instance of the <see cref="T:System.Attribute"/> class
    /// setting the Height to 50 and the folder to "~/images".
    /// </summary>
    /// <remarks></remarks>
    public UploadAttribute()
    {
        // set a default value of 50 and constrain aspect ratio.
        Height = 50;
        // set default images folder
        UploadFolder = "~/images";
    }
}

Listing 1 – Upload Attribute

The attribute in Listing 1 handles all the properties we want so we can build each of the field templates.

Upload File Field Template

To start off expand the DynamicData folder to see the FieldTemplate folder and right click and choose Add->New Item… you can now choose “Dynamic Data Field” (this will create a new field template pretty much the same as the Text field template) enter the name “UploadFile” and you will see your new blank field template.

Dynamic Data Field

Figure 2 – Dynamic Data Field

I have decided that I want nothing to show apart form the File Upload control until there is a file uploaded

WithoutData WithData
Figure 3 – Insert mode Figure 4 – Edit mode
In Figures 2 & 3 we can see both modes we only see an image and link/text when data is already present. So I have put the mark-up for this in a place holder.
<asp:PlaceHolder 
    ID="PlaceHolder1" 
    runat="server" 
    Visible="false">
    <asp:Image ID="Image1" runat="server" />&nbsp;
    <asp:Label ID="Label1" runat="server" Text="<%# FieldValueString %>" />
    <asp:HyperLink ID="HyperLink1" runat="server"><%# FieldValueString %></asp:HyperLink>
</asp:PlaceHolder><br />
<asp:FileUpload 
    ID="FileUpload1" 
    runat="server" 
    CssClass="DDTextBox" 
    Width="150px"/>

Listing 2 – Upload File field template mark-up

Next we remove the current validators and replace with a custom validator.

<asp:CustomValidator 
    ID="CustomValidator1" 
    runat="server" 
    ControlToValidate="FileUpload1" 
    onservervalidate="CustomValidator1_ServerValidate">
</asp:CustomValidator>

Listing 4 – Custom validator

In the code behind we need to setup the validator and the fileupload

protected void Page_Load(object sender, EventArgs e)
{
    CustomValidator1.Text = "*";
    SetUpValidator(CustomValidator1);

    // get attributes
    uploadAttribute = MetadataAttributes.GetAttribute<UploadAttribute>();
    if (uploadAttribute == null)
    {
        // no attribute thrw an error
        throw new InvalidOperationException("FileUpload must have valid uploadAttribute applied");
    }
    else
    {
        // add tooltip describing what file types can be uploaded.
        FileUpload1.ToolTip = String.Format("Upload {0} files", uploadAttribute.FileTypes.ToCsvString());
    }
}

Listing 5 – Page_Load

Here we setup the custom validator and get the Upload attribute, if the attribute is missing we throw an InvalidOperationException and if the attribute is there we set the FileUpload’s tooltip to a helpful message.

protected void CustomValidator1_ServerValidate(object source, ServerValidateEventArgs args)
{
    if (FileUpload1.HasFile)
    {
        // get files name
        var fileName = FileUpload1.FileName;

        // get files extension without the dot
        String fileExtension = FileUpload1.GetFileExtension();

        // check file has an allowed file extension
        if (!uploadAttribute.FileTypes.Contains(fileExtension))
        {
            args.IsValid = false;
            CustomValidator1.ErrorMessage = String.Format("{0} is not a valid upload file type (only {1} are supported).",
                FileUpload1.FileName, 
                uploadAttribute.FileTypes.ToCsvString());
        }
    }
    else if (Column.IsRequired && String.IsNullOrEmpty(Label1.Text))
    {
        args.IsValid = false;
        CustomValidator1.ErrorMessage = Column.RequiredErrorMessage;
    }
}

Listing 6 – CustomValidator1_ServerValidate method

Listing 6 is the Custom validator method here all we are concerned with is making sure we have a valid file to upload, so we check the file type via its file extension and if it matched one from out Upload attribute the file can be uploaded but if not we invalidate the page and add a custom error to our Custom validator. Also if the field is a required field then we check to see if there is a file present either from an edit or from the FileUpload control.

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.GetFileExtension();

    if (uploadAttribute.ShowHyperlink)
    {
        Label1.Visible = false;
        // open in new window
        HyperLink1.Target = "_blank";
        HyperLink1.Text = FieldValueString.GetFileNameTitle();
        HyperLink1.NavigateUrl = VirtualPathUtility.AppendTrailingSlash(uploadAttribute.UploadFolder) + FieldValueString;
    }
    else
    {
        HyperLink1.Visible = false;
        Label1.Text = FieldValueString;
    }

    // show the icon
    if (!String.IsNullOrEmpty(extension))
    {
        // set the file type image
        if (!String.IsNullOrEmpty(uploadAttribute.ImagesFolder))
        {
            // get file type image from folder specified in
            Image1.ImageUrl = String.Format("{0}{1}.{2}",
                VirtualPathUtility.AppendTrailingSlash(uploadAttribute.ImagesFolder),
                extension,
                uploadAttribute.ImageExtension);
        }

        Image1.AlternateText = extension + " file";

        // set width
        if (uploadAttribute.Width > 0)
            Image1.Width = uploadAttribute.Width;

        // set height
        if (uploadAttribute.Height > 0)
            Image1.Height = uploadAttribute.Height;
    }
    else
    {
        // if file has no extension then hide image
        Image1.Visible = false;
    }
}

Listing 7 – OnDataBinding event

In the OnDataBinding event Listing 7 we first of all check to see if the FieldValue is null if it is we just return. If it’s not null then we show the place holder. New we have to determine if we are showing an Hyperlink or a Label for the name of the file. And lastly we check to see if we can show a icon to represent the uploaded file type.

Note: The OnDataBinding event is the only place where we have access to the FieldValue in the field template code behind.
protected override void ExtractValues(IOrderedDictionary dictionary)
{
    // make sure file is valid
    if (FileUpload1.HasFile && Page.IsValid)
    {
        // make sure we have the folder to upload the file to
        var uploadFolder = Server.MapPath(VirtualPathUtility.AppendTrailingSlash(uploadAttribute.UploadFolder));
        if(!Directory.Exists(uploadFolder))
            Directory.CreateDirectory(uploadFolder);

        // upload the file
        FileUpload1.SaveAs(uploadFolder + FileUpload1.FileName);

        // update the field with the filename
        dictionary[Column.Name] = ConvertEditedValue(FileUpload1.FileName);
    }
}

Listing 8 – ExtractValues method

We make sure we have a file to upload and the there was no error from the custom validator, check that the upload folder exists and create it if not. We then save the file to the upload folder and update the data field.

Upload Image Field Template
// show the uploaded image
Image1.ImageUrl = String.Format("{0}{1}", 
    VirtualPathUtility.AppendTrailingSlash(uploadAttribute.UploadFolder), 
    FieldValueString);

// add alternate text
Image1.AlternateText = FieldValueString.GetFileNameTitle();

Listing 9 – OnDataBinding event

There only a few minor differences between UploadImage and UploadFile and they are in the section that displays the icon, in the UploadImage instead of displaying an Icon representing the file type we display the actual image.

Select Image Field Template

Here we are using a RadioButtonList and most of the work is done in the Page_Load Listing 10

protected void Page_Load(object sender, EventArgs e)
{
    // get attributes
    var uploadAttribute = MetadataAttributes.GetAttribute<UploadAttribute>();
    if (uploadAttribute == null)
        throw new InvalidOperationException("FileUpload must have valid uploadAttribute applied");

    SetUpValidator(RequiredFieldValidator1);
    SetUpValidator(DynamicValidator1);

    // if no images folder return
    var dirInfo = new DirectoryInfo(Server.MapPath(uploadAttribute.ImagesFolder));
    if (!dirInfo.Exists)
    {
        RadioButtonList1.Visible = false;
        return;
    }

    if (RadioButtonList1.Items.Count == 0)
    {
        // get a list of images in the ImageUrlAttribute folder
        var imagesFolder = ResolveUrl(uploadAttribute.ImagesFolder);
        var files = dirInfo.GetFiles(String.Format("*.{0}", uploadAttribute.ImageExtension));

        foreach (FileInfo file in files)
        {
            // size image to uploadAttribute
            var imgString = new StringBuilder();

            imgString.Append(
                String.Format("<img src='{0}' alt='{1}' ",
                    imagesFolder + file.Name,
                    file.Name.GetFileNameTitle()
               ));

            if (uploadAttribute.Width > 0)
                imgString.Append(String.Format("width='{0}' ", uploadAttribute.Width));

            if (uploadAttribute.Height > 0)
                imgString.Append(String.Format("height='{0}' ", uploadAttribute.Height));

            imgString.Append(" />");

            // embed image in the radio button
            var listItem = new ListItem(imgString.ToString(), file.Name);
            listItem.Attributes.Add("title", file.Name.GetFileNameTitle());

            this.RadioButtonList1.Items.Add(listItem);
        }
    }
}

Listing 10 – Page_Load

Here we get a list of file filtered by the file extension supplied by the Upload attribute the we populate the RadioButtonList with this list. it get a little clever as we add some html mark-up into the Text value of each ListItem added to the RadioButtonList showing the Image to choice.

Note: Setting the images folder up you will need to place your selection of images for the user to choose from into the folder and name them appropriately as the file name will be used for the tool tip and be stored in the DB as the value for the selection.
Date Picker  Field Template

All I have done here is add an Ajax Control Toolkit CalendarExtender to the page and set it up.

<asp:TextBox 
    ID="TextBox1" 
    runat="server" 
    CssClass="DDTextBox" 
    Text="<%# FieldValueEditString %>" 
    Columns="20">
</asp:TextBox>
<ajaxToolkit:CalendarExtender 
    ID="TextBox1_CalendarExtender" 
    runat="server" 
    Enabled="True"
    TargetControlID="TextBox1">
</ajaxToolkit:CalendarExtender>

Listing 11 – Date with Calendar Extender

private readonly static String DATE_FORMAT = CultureInfo.CurrentCulture.DateTimeFormat.ShortDatePattern

protected void Page_Load(object sender, EventArgs e)
{
    TextBox1_CalendarExtender.Format = DATE_FORMAT;
    TextBox1.ToolTip = Column.Description;

    SetUpValidator(RequiredFieldValidator1);
    SetUpValidator(RegularExpressionValidator1);
    SetUpValidator(DynamicValidator1);
    SetUpCustomValidator(DateValidator);
}

Listign 12 – Page_Load

All I’ve had to do is make sure the CalendarExtender matches the localised date time format, we now get a nice data picker.

Sample Metadata
[MetadataTypeAttribute(typeof(File.FileMetadata))]
public partial class File
{
    internal sealed class FileMetadata
    {
        public int ID { get; set; }
        public string Description { get; set; }

        [Upload(
            FileTypes = new String[] { "doc", "xls" },
            UploadFolder = "~/FileUploads/",
            ImagesFolder = "~/images/",
            ShowHyperlink = true,
            ImageExtension = "png",
            Height = 30)]
        [UIHint("UploadFile")]
        public string FileName { get; set; }

        [Upload(
            FileTypes = new String[] { "png", "jpg", "jpeg" },
            UploadFolder = "~/ImageUploads/",
            ShowHyperlink = true,
            ImageExtension = "png",
            Height = 30)]
        [UIHint("UploadImage")]
        public string ImageName { get; set; }

        [Upload(
            ImagesFolder = "~/ImageSelection/",
            ImageExtension = "png",
            Height = 30)]
        [UIHint("SelectImage")]
        public string Selection { get; set; }

        [DataType(DataType.Date)]
        public DateTime DateCreated { get; set; }

    }
}

Listing 13 – sample metadata

Download NuGet Package

Dynamic Data Custom Field Templates - 1.0

5 comments:

Damoon Amanabadi said...

Thanks Steve,

ricocsharp said...

I'm a fan of DD because of its productivity advantages mainly, now i'm also using in the presentation layer of my web app Ext.net, so i discovered non examples of mergering this two great technologies Steve do have any ideas on that?

Stephen J. Naughton said...

Hi ricocsharp, I'm not sure what you are asking?

Steve

Miloš said...

Hi Steve, how can user delete an uploaded file in edit mode?

Stephen J. Naughton said...

That I'd a good idea however I'm in hospital at the moment, and can't look at it right now, there are two things I can think of;
1. As a delete button.
2. During upload delete old file, I store files in folders unique to the record so I can empty the folder on new file upload.

Steve
I will post on my blog when I am recovered from my operation.